Writing Tachyon Instructions
Integrating with Tachyon
Tachyon .NET Consumer SDK
Tachyon PowerShell Toolkit
Modules and Methods
The Tachyon Agent ships with a number of modules, and each module has a number of methods. Modules are simply a container, and provide a way of logically grouping methods together. Methods are operations which interact with the device and provide output data in the form of a table. A method may also take one or more parameters (see section below). If you have used any kind of object-oriented programming language before, these concepts should be familiar.
An example of a module may be the "Network" module; a corresponding method on that module may be "GetConnections". Calling this method returns a table of data which contains a row for each network connection, with corresponding columns for "IpAddress", "Port", etc. Some methods return only a single value.
The full list of methods is too long to include here, but can be found in the Tachyon Agent Methods reference.
In addition, the Agent language includes some cross-platform built-in functions – think of them like methods available in a "global" module. See Tachyon Agent Built-in Functions reference.
The Tachyon Agent’s architecture allows new modules (and therefore new capabilities) to be deployed after the Agent has been installed.
An individual method may take one or more parameters which will determine the behaviour of that method. For example, the "Users" module has a method named "GetLocalGroupMembers". This method takes a parameter named "GroupName" which is the name of the local group whose members should be queried.
A parameter may be mandatory, in which case you must provide a value for that parameter when calling the method, or optional, in which case it can be omitted.
A parameter has a corresponding data type, which determines the possible values that can be supplied for that parameter. For example, a parameter may be a "string", or an "integer", etc.
Method parameters should not be confused with instruction parameters. However, you can use instruction parameters as method parameters.
Method names and their parameter names are case-sensitive.
The output of a method is always a table of data.
The output can be assigned to a @table. Once data is in a @table, it can be processed using SQL, iterated over using FOREACH, and used as a parameter for subsequent method calls.
These concepts are discussed below.
The Tachyon Agent language integrates with SQLite – an open source, light-weight database engine with comprehensive support for the SQL querying syntax. SQLite is a popular, high-performance, feature-rich and light-weight database technology which is incredibly pervasive in modern IT – indeed, it is speculated that SQLite is one of, if not the most widely deployed software component in the world.
Although SQLite has full SQL support, including advanced concepts like views, triggers, pragmas, etc., the Agent Language supports only SQL SELECT statements. Statements such as UPDATE, INSERT and DELETE are not supported within the language. However, by using SQLite to its fullest, it is possible to perform very complex data processing. @tables within the Agent language manifest themselves as temporary tables which can be queried using SQL SELECT statements.
Documentation for SQLite is found on its website. There are other help sites and forums you can search for help and examples. The following are some useful pages:
- https://sqlite.org/lang_select.html - SELECT syntax
- https://sqlite.org/lang_expr.html - includes details of SQL operators
- https://sqlite.org/lang_corefunc.html - SQL core functions
SQL is only used to process @tables that are used as parameters and output of Methods. It is not possible to call Methods from within a SQL SELECT statement, therefore if you require some form of flow control then use the Agent Language FOREACH loop. Nor can you use a SQL SELECT statement as a Method parameter; instead use SQL to save a parameter as a @table and use that @table in the Method.
In addition to SQLite functions, the Agent language takes advantage of SQLite extensibility by providing "application defined functions" for use in SELECT statements. See Tachyon Agent SQL Functions reference.
Simple method execution
A Tachyon instruction is composed of one or more sequential statements in the Agent language, separated by a semi-colon. At its simplest, this might be a single method invocation:
This example executes the GetConnections method on the Network module.
The set of data returned from this instruction might look as follows:
Implicit in this statement is the "return" of the output data from this method call as the result of the instruction. Each Tachyon instruction returns a single set of data (in the form of a table), and it is this data that is sent back to the Tachyon Server and ultimately displayed in Tachyon Explorer. The data returned from an instruction is based on the last set of data the instruction produces.
For example, the following instruction:
… will retrieve the active network connections, but then immediately discard the output. It will then query the logged-on users, and return that output as the result of the instruction (as that is the set of data produced).
The output of this instruction might look as follows:
Although the Agent language permits this type of construct, it is clearly not useful in this context – two methods are executed, but only the data from the last is available. Things start to come together when the data from multiple method executions are combined using SQL. This is discussed in detail later.
Use of white-space
Although this example shows the two method statements on separate lines for readability, whitespace within the language is not treated as significant. The two statements could appear on the same line.
The Agent Language is case-sensitive with respect to names of modules, methods and parameters.
Although some aspects of the Agent Language are more permissive in terms of character case, it is recommended that you treat the entire language as case sensitive.
Strings and Escaping
String literals should be enclosed in double-quotes (").
Single-quotes (') are deprecated. Despite being permitted in SQLite, they should not be used. They are acceptable in the current version of Tachyon but this may change in the future due to potential difficulties with escaping.
The escape character is a backslash (\).
If a literal double-quote is required in a string then use the escape character (\").
If a literal escape character is required in a string then use backslash-backslash (\\).
The following is an example of both uses.
// Result: The Tachyon Agent log is found in "C:\ProgramData\1E\Tachyon\" by default. SELECT "The Tachyon Agent log is found in \"C:\\ProgramData\\1E\\Tachyon\\\" by default." AS Tip;
The following examples are different ways of achieving the same thing, depending on your preference for readability.
// Result: James says "I would like sausages today" every morning. @breakfast = SELECT "sausages" AS choice; SELECT "James says \"I would like " || choice || " today\" every morning." AS greeting FROM @breakfast; SELECT "James says " || CHAR(34) || "I would like " || choice || " today" || CHAR(34) || " every morning." AS greeting FROM @breakfast; SELECT PRINTF("James says \"I would like %s today\" every morning.", choice) AS greeting FROM @breakfast; SELECT PRINTF("James says %sI would like %s today%s every morning.", CHAR(34), choice, "\"") AS greeting FROM @breakfast;
Passing method parameters
Now we’ll consider passing parameters to a method. Parameters are passed by specifying their name and value within the parentheses following a method name. Some examples:
/* Pass a string parameter */ Users.GetLocalGroupMembers(GroupName: "Administrators"); /* Pass an integer parameter */ Agent.Sleep(Seconds: 5); /* Pass a string parameter and a boolean parameter */ FileSystem.GetFilesInFolder(Folder: "D:\\MyData\\MyPictures", Recursive: true);
Note the following about parameters:
- Parameter names are case-sensitive
- Parameters can be supplied in any order
- Parameters can be omitted if they are optional
- Parameter values have datatypes – strings are enclosed in double-quotes (and use the backslash character to escape either a literal double quote or a backslash itself); numbers and Booleans should not be enclosed in double-quotes
For Tachyon Agent versions 3.2 and onwards, a boolean can also be supplied as an sqlite3
integer. In such a case, zero will be interpreted as
false, and any non-zero integer will be interpreted as
realwill not be accepted. For example:v3.3+
// Recursion on FileSystem.GetFilesInFolder(Folder:"c:\\potato", Recursive: true) FileSystem.GetFilesInFolder(Folder:"c:\\potato", Recursive: -15) FileSystem.GetFilesInFolder(Folder:"c:\\potato", Recursive: 65535) // Recursion off FileSystem.GetFilesInFolder(Folder:"c:\\potato", Recursive: false) FileSystem.GetFilesInFolder(Folder:"c:\\potato", Recursive: 0)
- Parameter values can be supplied using @table – this is an advanced topic discussed in Passing @tables as method parameters
@tables have the following properties:
- they are used to store the output of Agent methods and SQL queries, and must be considered as SQLite tables, and as such can have multiple rows and columns, as well as empty tables with no rows.
- their names must be alphanumeric, and start with a letter, and are case-sensitive
- they can be used as parameters for Agent methods
- they can be re-created and re-used multiple times within an instruction
@device = Device.GetSummary(); @device = SELECT Manufacturer, Model FROM @device;
You can also store a numeric or string value, without the need for a full SELECT statement. The column name is Value.
@table = 99; SELECT Value FROM @table;
You can create your own @tables. One method is:
@colors = SELECT column1 AS ColorName, column2 AS ColorCode FROM ( VALUES ("Green","#008000"), ("Blue", "#0000FF"), ("Yellow", "#FFFF00"), ("Cyan", "#00FFFF"), ("Red", "#FF0000"), ("Purple", "#800080") );
@tables are deleted and memory freed when an instruction exits.
SQLite tables have a built-in autoincremented key rowid which is not included in output, unless specified in a SELECT statement. An example of where rowid is useful is when using SplitLines in methods, for example in NativeServices.RunCommand and Utilities.SplitLines.
The Tachyon built-in function IsUndefined is used to determine whether a table is real and full of values or whether it is the result of an operation that produced nothing, which is different to producing an empty table.
Having executed a method, the data can be returned directly from the instruction, or can be assigned to a @table for further processing.
In the following example, we capture all the active network connections, assign them to a @table, and then run a SQL SELECT query to count them, grouped by the process name (i.e. count number of network connections per process):
@connections = Network.GetConnections(); @connections = SELECT COUNT(1) AS ConnectionCount , ProcessName FROM @connections GROUP BY ProcessName;
Since the last statement of this instruction is our SQL query, the output of this query becomes the result of the instruction. The example shows the results of the SQL query are saved in a table called @connections which could be used by further methods or queries.
See SQL section above for useful references on the SQLite website.
The following are keywords, which are reserved, and should not be used as names of @tables or columns. Potential future keywords should also be avoided because any instructions that use these words may be invalidated, so do not use them.)
Comments can either be multi-line or single-line.
Outside of SQL statements the Agent language supports C-style comments.
/* This is a comment which spans multiple lines */ @connections = Network.GetConnections(); // This comment continues until the next line break
Within SQL statements, comments are are subject to what is supported by SQLite (see http://www.sqlite.org/lang_comment.html).
/* This is a comment which spans multiple lines */ @flag = SELECT 1 AS value; -- This comment continues until the next line break
Multi-line comments are supported in all cases, and can also be used for single-line, provided you remember to terminate the comment.
@flag = SELECT 1 AS value; /* This is a comment that has been correctly terminated */
From Agent Language to Tachyon Instruction
Writing and testing Instructions
Creating an instruction using the Agent Language is the first step in building a question or an action that can be used from Tachyon Explorer (or any other Tachyon consumer).
The easiest way to create an instruction is to refine and test it on a single device, and then incorporate it into a Product Pack which can be imported into Tachyon. 1E provides the Tachyon Instruction Management Studio (TIMS) which can be used to create and run instructions on your local machine. This tool also allows you to add your instruction into a Product Pack.
See Getting started with TIMS for how to use TIMS.
Continue reading to understand the Agent language.
Use the Functions and Methods Reference to use the power of Tachyon.
Instructions and Instruction Sets
Once the instruction has been written and tested, the instruction is then saved as an Instruction Definition, which is simply an XML file.
The definition of an instruction is explained fully in Instruction Definition Reference, but here is a summary:
- A unique name
- The type of instruction – whether it is a question or an action (which affects how it is displayed in Tachyon Explorer). Note that the Agent itself does not differentiate between questions and actions – it considers both as simply "instructions".
- A "readable payload" – this is the instruction phrased as a question or action, and is what is displayed in Tachyon Explorer
- Description - this is additional information that can also be displayed in the Tachyon Explorer; along with the Readable Payload, the Description is also parsed when searching in Tachyon Explorer
- The instruction "payload" – this is the Agent Language script which will be sent to the target devices
- Parameters for the instruction – this allows anyone running the instruction to substitute custom values within the payload
- The instruction "schema" – this describes the structure of the data which the instruction returns, and is used to create the dynamic storage for the results of the instruction on the Tachyon Server
- The instruction "aggregation schema" – this describes how data from multiple devices should be summarized, and is conceptually similar to a "GROUP BY" clause in SQL
- The time-to-live (TTL) values for the instruction – how long to gather data (gather period - InstructionTtlMinutes) and how long to keep it (keep period - ResponseTtlMinutes)
A Product Pack can also include "resources", such as script files (e.g. PowerShell, BASH, etc.). Adding resources to a Product Pack makes them available for download by the Tachyon Agents. Using scripts within the Agent Language is described in detail later.
Adding parameters to an instruction can make it flexible and enables the instruction to be reused. An instruction parameter is simply a substitution of a user-supplied value into the instruction’s payload.
In the following example, we extend the previous example by counting network connections per process where the connection is on a specific, user-supplied TCP port.
@connections = Network.GetConnections(); SELECT COUNT(1) AS ConnectionCount , ProcessName FROM @connections WHERE RemotePort = CAST("%MyPort%" AS NUMBER) GROUP BY ProcessName;
We would then define a parameter to substitute the placeholder %MyPort% with a value supplied by the user. For guidance on using TIMS to define instruction parameters, please refer to Getting started with TIMS - Instruction Parameters.
Instruction parameter considerations
Since parameter values are substituted directly into the Agent’s payload, you must take care to avoid the possibility of SQL or statement injection. Tachyon takes precautions against this (e.g. escaping double-quotes and backslashes within string parameter values), but you should consider the following when parameterizing instructions:
- Although the format %ParameterName% is the preferred convention for a parameter placeholder, you are free to use any substitution placeholder you wish
- Parameters should be defined as the most appropriate data type (string, integer, float, Boolean)
- String parameters should, where possible, be constrained using a regular expression to prevent invalid input
- Placeholders for string parameters should be placed within double-quotes within the Agent Language – the Tachyon Consumer API will automatically perform escaping of the parameter’s value
- Placeholders for non-string parameters should not be placed in double-quotes
- Within SQL statements, you may wish to consider using a CAST("%parameter%" AS NUMBER) expression for type-safety
- Although it is possible to use parameters to build "dynamic statements" within the Agent Language, it is NOT recommended unless you take extreme care (and limit the possible values which can be supplied for the parameter)
Handling multiple values in a single string instruction parameter
/* Transpose a comma-delimited string of KB numbers into a column, eliminating whitespace and blanks */ @kblist = Utilities.SplitLines(Text:"%kblist%", Delimiter:","); @kblist = SELECT DISTINCT TRIM(Output) AS kb FROM @kblist WHERE TRIM(Output) <> "" ORDER BY kb;
Output content and column names
Column naming rules
- only use basic alphanumeric characters when naming a column (A-Z, a-z, 0-9) - do not include spaces or other characters
- must start with a letter - so "MyColumn", "Value1" and "Iteration3Data" are valid column names, whereas "my_column", "my column", and "3IterationData" are not
- must be unique when considered case-insensitively - for example, you should not have instructions which returns columns named, "TEST", "test" and "Test", as this will cause conflicts.
The names of the columns within the data set returned by the Agent Language correspond to the column headings for the data when displayed in Tachyon Explorer. Tachyon Explorer performs a basic translation of the name based on changes in upper/lower-case. For example, ThisIsMyColumnName will be displayed as This Is My Column Name.
You can use column aliasing within SQL to name (or rename) a column. For example the instruction below returns two columns of data, one named ConnectionCount and the other named NameOfTheProcess. Tachyon Explorer will display these as Connection Count and Name Of The Process respectively.
@connections = Network.GetConnections(); SELECT COUNT(1) AS ConnectionCount , ProcessName AS NameOfTheProcess FROM @connections GROUP BY NameOfTheProcess;
You also have an alternative option for changing Column names displayed in Explorer by using the Render As option when defining the output schema of an instruction, as described in Getting started with TIMS: Adding a schema.
We mention this here, just so you do not confuse aggregating data using SQLite with response aggregation at the server. The latter is not part of the Agent language, but is determined by the AggregationJson specified in the instruction definition. Then the Explorer portal will display the aggregated responses with a drill down into the detailed data.
If you don't need the detailed data, then aggregating data on each device is useful to reduce the quantity of data being reported back to the server and having to be processed.
@products = Software.GetInstallations(); SELECT Publisher , Count(*) AS Products FROM @products GROUP BY Publisher;
Once you have data in a @table (e.g. by capturing the output of a method invocation), that @table is available as a pseudo-table within SQLite, which can then be queried.
A @table assignment is straight-forward:
@MyTable = Network.GetConnections(); SELECT DISTINCT ProcessName FROM @MyTable;
Once created, a @table has "scope" for the entire instruction – in other words, it will exist until the instruction completes, and can be referenced as many times as required within the remainder of the instruction.
@tables can be re-assigned if required, for example:
@MyTable = Network.GetConnections(); @MyTable = SELECT DISTINCT ProcessName FROM @MyTable;
In the example above, we assign @MyTable first to the output of querying for network connections, and to the result of SELECTing the DISTINCT (unique) process names from the previous data.
From v3.2, the maximum number of output rows from a single
SELECT_expression is limited to the value specified by the
SelectRowsLimit configuration value. The range is 1 to 1000000000 (109), with a default of 100000. This is intended to prevent runaway expressions that generate a ridiculous number of rows, consuming gigabytes of memory and wasting CPU and time. No further rows are produced if this value is exceeded, but it is not considered to be an error situation although a warning is generated in the log file.
JOINing and GROUPing
A SELECT statement can of course reference more than one @table if required. The most common reason for doing this is to join data together; for example:
@p = OperatingSystem.GetProcesses(); @c = Network.GetConnections(); SELECT COUNT(1) AS ConnectionCount , @p.AccountName , @c.RemotePort FROM @p INNER JOIN @c ON @p.ProcessId = @c.ProcessId GROUP BY @p.AccountName , @c.RemotePort;
This simple example returns a count of active TCP connections per-port, per-user, by performing an INNER JOIN on the process and connection data using the common "ProcessId" field.
SELECTing "out of thin air"
/* Note that omitting the FROM clause means we SELECT from a non-existent table which has exactly one row. This is similar to using the "dual" table in Oracle. */ @d = SELECT DATETIME() AS LocalDate;
More complex examples
SQLite provides huge flexibility and power in its SELECT syntax – the full syntax is available online.
Here are some more examples to get you started.
Get the top 5 processes with the most number of active TCP connections
@c = Network.GetConnections(); SELECT COUNT(1) AS ConnectionCount , ProcessName FROM @c GROUP BY ProcessName ORDER BY ConnectionCount DESC LIMIT 5; /* Use LIMIT n to return the first n rows */
@p = OperatingSystem.GetProcesses(); SELECT Executable || " was run by " || AccountName AS Message FROM @p;
Concatenation from multiple @tables:
@programData = NativeServices.RunCommand(CommandLine:"cmd /c echo %PROGRAMDATA%"); @tachyonFolder = SELECT "\\1E\\Tachyon" AS Path; SELECT @programData.Output || @tachyonFolder.Path AS TachyonPath FROM @programData,@tachyonFolder
Using CASE to translate values
@c = Network.GetConnections(); SELECT CASE WHEN RemotePort = 443 THEN "HTTPS" WHEN RemotePort = 80 THEN "HTTP" WHEN RemotePort = 21 THEN "FTP" WHEN RemotePort = 4000 THEN "1E Tachyon" ELSE "Port #" || CAST(RemotePort AS STRING) END AS PortName, ProcessName, RemoteAddress FROM @c;
@bit = NativeServices.RunCommand(CommandLine:"manage-bde -status", SplitLines:true); @baseLN = SELECT LineNumber FROM @bit WHERE output LIKE "Volume%"; SELECT (SELECT SUBSTR(output, 8, 2) FROM @bit WHERE @baseLN.LineNumber = @bit.LineNumber) AS DriveLetter, (SELECT SUBSTR(output, 27) FROM @bit WHERE @baseLN.LineNumber = @bit.LineNumber -3) AS DiskSpace, (SELECT SUBSTR(output, 27) FROM @bit WHERE @baseLN.LineNumber = @bit.LineNumber -4) AS BitlockerVersion, (SELECT SUBSTR(output, 27) FROM @bit WHERE @baseLN.LineNumber = @bit.LineNumber -5) AS ConversionStatus, (SELECT SUBSTR(output, 27) FROM @bit WHERE @baseLN.LineNumber = @bit.LineNumber -6) AS PercentageEncrypted, (SELECT SUBSTR(output, 27) FROM @bit WHERE @baseLN.LineNumber = @bit.LineNumber -7) AS EncryptionMethod, (SELECT SUBSTR(output, 27) FROM @bit WHERE @baseLN.LineNumber = @bit.LineNumber -8) AS ProtectionStatus FROM @baseLN;
The following is an example of how the Agent language can use a SQLite CTE. The temp table can have any number of columns so long as they are matched by the same number of columns in each of the sub-selects.
@factorials = SELECT * FROM ( WITH RECURSIVE temp(n, nfact) AS ( SELECT 0, 1 UNION ALL SELECT n+1, (n+1)*nfact FROM temp WHERE n < 9) SELECT * FROM temp );
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.
@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.
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.
@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.
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.
@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:
@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 "
- A method invocation, such as "
- 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.
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;
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.
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.
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.
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;
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".
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
Capturing the result of an IF statement
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
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
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" WHEN "Android" THEN "/my/droid/cmd -xyz" WHEN "Solaris" THEN "/my/sunny/cmd -shine" 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.
Passing @tables as method parameters
As your instructions become complex, you may wish to pass parameters to methods using @tables, rather than fixing the values. A common scenario of this is FOREACH – described later.
Consider the following example:
/* Get all the files in the C:\BigFiles folder */ @files = FileSystem.GetFilesInFolder(Folder: "C:\\BigFiles"); /* Bail out if there are no files */ EVALUATE; /* Pick the biggest one */ @biggest = SELECT FileName FROM @files ORDER BY FileSize DESC LIMIT 1; /* And then delete it */ FileSystem.DeleteFileByName(FileName: @biggest.FileName);
This example deletes the biggest file in a specific folder. We use LIMIT 1 to get the biggest file ordered by FileSize, and then pass that value to the DeleteFileByName method.
However, this syntax has a specific constraint. In our example, the use of LIMIT 1 guarantees that our "biggest" @table contains exactly one row. This is important, because parameters to methods (such as "DeleteFileByName" in this case) cannot take a set of values – they take only one.
If we had removed the LIMIT 1 clause from our query (and we had more than one file in our C:\BigFiles folder), this instruction would fail. This is because we are attempting to pass multiple "rows" of values for a single parameter.
To summarize – a field (column) from a @table can be passed as a value for parameter if:
- The @table contains exactly one row, and
- The field from the @table is of the correct datatype for the parameter
Additionally, you can omit the field name from the @table if that @table contains exactly one field:
@files = FileSystem.GetFilesInFolder(Folder: "C:\\BigFiles"); EVALUATE; @biggest = SELECT FileName FROM @files ORDER BY FileSize DESC LIMIT 1; /* Valid - @biggest contains only one field (FileName) so no need to specify it */ FileSystem.DeleteFileByName(FileName: @biggest); /* ... however... */ @smallest = SELECT FileName, FileSize FROM @files ORDER BY FileSize ASC LIMIT 1; /* NOT valid - @smallest contains two fields (FileName + FileSize) */ // FileSystem.DeleteFileByName(FileName: @smallest); /* But this is fine */ FileSystem.DeleteFileByName(FileName: @smallest.FileName);
If you cannot guarantee that your @table contains exactly one row, you will need to use the FOREACH construct to iterate over the @table.
Agent SQL functions
SQLite includes a number of built-in functions which you can use in SELECT statements:
- Core functions, such as IFNULL, INSTR, SUBSTR, REPLACE, TRIM, UPPER, LOWER, RANDOM, PRINTF etc. are documented online at: https://sqlite.org/lang_corefunc.html.
- Aggregate functions, such as COUNT, MIN, MAX, GROUP_CONCAT etc. documented online at: https://sqlite.org/lang_aggfunc.html.
- Date and time functions, documented online at: https://sqlite.org/lang_datefunc.html. See datetime handling.
The Tachyon Agent takes advantage of SQLite extensibility by providing a further "application defined functions" that are built-in to the Agent and can also be used in SELECT statements.
Agent built-in functions
The functions described above are SQL-based functions, so can be used in SELECT statements. The Agent language itself also offers some built-in functions used outside of SELECT statements – think of them like methods available in a "global" module.
Global Settings variables
Tachyon provides an ability to store variables centrally, in the master database, and use those variables in instructions, in the Readable Payload or within the instruction (Payload).
They are similar to instruction parameters but do not prompt for a value in Tachyon Explorer. They conform to following pattern: %global:variable% where variable would be the name of the variable you want to use. All variables are case-insensitive.
So if in your Global Setting table you have an entry with the name 'ExecutionPolicy' you can use this in the payload and/or readable payload: %global:ExecutionPolicy% and the Value present for that entry will be inserted into the payload(s) replacing the %global:ExecutionPolicy%.
Please note the setting you request must exist in the database in order for the instruction to be issued, as an error will be returned if you attempt to issue an instruction based on a definition that uses a Global Setting that cannot be found.
The Tachyon Settings application does not (yet) provide a means of managing Global Settings, therefore you must edit the database directly.
In TIMS, when you are testing an instruction that uses Global Settings variables, TIMS will prompt you to enter a value. This is because TIMS does not have access to a Tachyon System.
Do not use Global settings variables in Guaranteed State fragments. They are not supported. Even if they could be used, the Value would be inserted into the policy payload at the point it is deployed, and any later changes to the Value would not be used.
Instead, Guaranteed State fragments should use a normal parameter, and the value for the parameter specified when configuring rules in Guaranteed State administration UI.
They are similar to instruction parameters but conform to following pattern: %environment:variable% where variable would be the name of the variable you want to use. All variables are case-insensitive.
If you use one of the patterns below in the Readable Payload or within the instruction (Payload), it will be replaced with another value. The 'other' value depends on which pattern you specified.
These following variables are available and hard coded in the system. They are not extensible in any way and each has a fixed behaviour.
|Variable pattern||Replaced with||Introduced|
UTC ISO-8601 compatible date time stamp of when the instruction was issued.
This is the timestamp of when the instruction was originally run and sent by the Tachyon Server. It remains the same if the same instruction is re-run.
Querying historic data
The Tachyon Agent has the ability to capture security-related events from the underlying operating system and store/summarize the data in a local database.
The feature is described here: Tachyon Activity Record.
The tables used to store this data are available to be queried from within the Agent Language using a slightly modified syntax. The related tables are available using the dollar ($) prefix.
/* Sum the number of connections made per process today */ SELECT SUM(ConnectionCount) AS Connections , ProcessName FROM $TCP_Daily WHERE TS = DATETRUNC(STRFTIME("%s", "now"), "day") GROUP BY ProcessName;
The Agent returns one of these codes to the Tachyon Server when it executes an instruction.
If a statement is successful it will return an Exit Code of 0 and a response which can be seen in the Content tab of the Explorer Responses page.
If a statement has no content, or produces an error, then no response is seen in the Content tab, however the Status tab of the Explorer Responses page shows a count for each Exit Code, and the ErrorData message where applicable.
If a statement exits early or produces an error then execution of the instruction terminates immediately with an Exit Code. Any changes made by the Agent are not rolled back.
Success with content. This exit code is returned if the instruction is successful and has content.
V3.3 onwards lets you return 0 when a RETURN statement is used.
|Success - no content||1||n/a|
Success with no content. This exit code is returned if the instruction is successful but has no content.
This can occur if an EVALUATE statement is used. V3.3 onwards lets you return 1 when a RETURN, EVALUATE, or NOCONTENT statement is used.
|Error||2||Missing or invalid parameter 'xxx'.||The parameters do not match in quantity or type that the method expects. For example, a mandatory parameter name is missing, or its case-sensitive name is not correct.|
|Execution of SELECT failed.|
A SELECT statement is not constructed correctly.
The Tachyon Agent language processor does not fully parse or check the syntax of the
|Syntax error in instruction.|
A parameter value is not correct.
An escape character may be required.
A statement may not be constructed correctly. For example, a ; terminator or other part of a statement is missing; or has superfluous characters.
|<method specific errors>|
|Missing 'xxx' provider.||A supported method or function is used but is unable to find the required provider. The provider file does not exist on the device.|
|<custom error message>||V3.3 onwards lets you return custom messages as defined in an ERROR statement.|
|Not implemented||3||No built-in function named 'xxx'.||The named function does not exist. This error is usually caused by a typo outside of a SELECT statement.|
|Unsupported module 'xxx'.||The module is not supported on this platform. This error can occur if:|
|Unsupported method 'xxx'.||The method is not supported on this platform. This error can occur if:|
|Script language 'xxx' is not supported.||The language specified in Scripting.Run(Language: "xxx" ....) is not supported on this platform. This error can occur if:|
|<custom error message>||V3.3 onwards lets you return custom messages as defined in a NOTIMPLEMENTED statement.|
|Response too large||4|
The EVALUATE keyword allows an instruction to terminate early if the preceding statement did not yield any results.
From v3.3, SCALE supports the following additional termination keywords which can be used to exit an instruction, and return a corresponding Exit Code. These are most useful when used in conjunction with
IF/ELSE/ENDIF construct but can be used anywhere within an instruction.
- RETURN (0/1)
- NOCONTENT (1)
- ERROR (2)
- NOTIMPLEMENTED (3)
RETURN allows you to exit out of an instruction early with a given set of results.
@results = "some statement"; RETURN @results;
Ordinarily, the results of an instruction are based on the last statement to execute; RETURN allows you to do this at any point in an instruction. In contrast to EVALUATE (which means “break out of this instruction if the preceding statement returned no results),
RETURN breaks unconditionally. With
RETURN, you supply the @table whose value should become the results of the instruction.
The instruction exits successfully, with an exit code of 0 if the results have content, and 1 if they have no content.
Here’s an example:
@reg = NativeServices.RegistryGetValue( Hive:"HKLM", Subkey:"SOFTWARE\\MyVendor\\MyConfiguration", Name:"SecuritySetting"); // If the setting exists, return a message indicating we're secure IF (@reg) @msg = SELECT "System already secure" AS Status; RETURN @msg; ENDIF; // Otherwise, take some action... // ... and report that back. No need to use RETURN, because // this is the last statement in the instruction. SELECT "System was insecure, but is now secure" AS Status;
//some statements NOCONTENT;
NOCONTENT is an unconditional version of the
EVALUATE keyword. Each causes an instruction to exit successfully, with an exit code of 1 meaning “Success, no content”, and do not return a message.
Here’s an example:
@users = Users.GetLoggedOnUsers(); // Return "success no content" if they are any logged in users IF (@users) NOCONTENT; ENDIF; // ... otherwise do something which can only be done if no-one is logged on
//some statements ERROR "custom error message";
ERROR causes an instruction to terminate with exit code of 2 meaning “Error” and the specified custom error message. The error message must be supplied as a string literal, and cannot be a reference to a @table or any other kind of expression.
Here’s an example:
@svcs = OperatingSystem.GetServiceInfo(); @nomad = SELECT * FROM @svcs WHERE Name = "NomadBranch"; IF NOT (@nomad) ERROR "1E Nomad is not available"; ENDIF; // Otherwise do some work with 1E Nomad...
//some statements NOTIMPLEMENTED "custom error message";
NOTIMPLEMENTED causes an instruction to terminate with exit code of 3 meaning “Not implemented” and the specified custom error message. The error message must be supplied as a string literal, and cannot be a reference to a @table or any other kind of expression.
Here’s an example:
@dev = Device.GetSummary(); @win = SELECT * FROM @dev WHERE OperatingSystemType = "Windows"; IF NOT (@win) NOTIMPLEMENTED "This functionality is only available on Windows"; ENDIF; // ... otherwise do something Windows-specific
(END OF PAGE)