Summary

Understanding the Tachyon client language: SCALE - Simple Cross-platform Agent Language for Extensibility.

The Tachyon Agent features a simple but powerful interpreted programming language, and it is from this language that Tachyon instructions (questions and actions) are built. The language is cross-platform, allowing the same instruction to run on different operating systems, and has support for SQL SELECT commands to allow data to be transformed, filtered and aggregated. The language is concise and easy to learn, and mastery of the language is the key to developing powerful instructions. Version information is provided to identify new features that are not available in older versions of Tachyon.

Search

Quick Start

If you’re already familiar with scripting languages and/or SQL, you’ll more than likely be able to get started just by skim-reading this document and looking at some of the examples. If that’s the case, here’s a quick summary of the language which should help: 

  • The language is interpreted, and mixes SQL SELECT statements with "method" calls on "modules" found in the Functions and Methods Reference.
  • The SQL SELECT support is provided by an enhanced version of the SQLite engine.
  • Additional custom SQL functions and Agent functions are described below.
  • The language is case sensitive (although case sensitivity is more relaxed within SQL SELECT statements) and is not whitespace sensitive.
  • Statements in the language must be terminated by a semi-colon (;)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
  • Variables within the language are written as "@myTableName" they are always tables of data that can have one or more rows and one or more columns.
  • The output of method calls and SQL SELECTs can be assigned to @tables and can be SELECTed from
  • The output of the instruction is always the last set of data produced by a statement, that is the final SELECT statement or method call.
  • If a statement produces an error, execution of the instruction terminates immediately with an Exit Code. Any changes made by the Agent are not rolled back.
  • There is some simple flow control with FOREACH looping (new in v3.0) and IF/ELSE/ENDIF conditional branching (new in v3.3).
  • You can use the "EVALUATE" statement to break out of an instruction at any point if the last statement produced an empty set of data; it is in effect "continue if data available".
  • RETURNNOCONTENTERROR, and NOTIMPLEMENTED (new in v3.3) allows Early exit termination of the instruction with different Exit Codes.
  • Use double-quotes (") to enclose string literals, and in strings use the sequences backslash-double-quote (\") and backslash-backslash (\\) to escape literal double-quotes (") and a literal backslashes (\) respectively.
  • Use C-style comments in Agent language. Both the block and single-line; e.g. /* This is a comment until the block is closed */ and // This is a comment until the end of the line.
  • Use SQL-style comments in SQL statements. Both the block and single-line; e.g. /* This is a comment until the block is closed */ and  -- This is a comment until the end of the line.

Here’s an example demonstrating some of the key features of the language – it brings back a row for each loaded DLL/EXE by any running process, the file version info data for that binary, and its MD5 hash. Since the example uses WMI, it will run on Windows only.

Example - query file version info and MD5 hash for loaded binaries
/* Get all the loaded DLLs and EXEs using a WMI query */
@binaries = NativeServices.RunWmiQuery(Namespace: "root\\cimv2", Query: "SELECT * FROM CIM_ProcessExecutable");

/* Extract the filename and count the number of times its loaded */
@binaryNames = SELECT   COUNT(1) AS LoadedCount
               ,        LOWER(REPLACE(REGEXPREPLACE("(.*)Name=\"(.*)\"", Antecedent, "$2"), "\\\\", "\\")) AS FileName
               FROM     @binaries
               GROUP BY FileName;

/* For each loaded binary, get the file version info */
@binaryInfo = FOREACH @r IN @binaryNames DO
  FileSystem.GetVersionInfo(FileName: @r.FileName);
DONE;

/* Then, for each loaded binary, get the MD5 hash */
@hashes = FOREACH @r IN @binaryNames DO
  FileSystem.GetHash(FileName: @r.FileName, Algorithm: "MD5");
DONE;

/* Then bring everything together */
SELECT     I.*
,          H.FileHash
,          N.LoadedCount
FROM       @binaryInfo AS I
INNER JOIN @hashes AS H      ON I.FileName = H.FileName
INNER JOIN @binaryNames AS N ON I.FileName = N.FileName;
On this page:

In this section...
  • Datetime handlingGuidance and examples for using date and time values in instructions.
  • Tachyon Activity RecordReference information about the Tachyon Activity Record (TAR) feature, sometimes referred to as either the inventory, or forensics feature, and previously known as Agent Historic Data Capture.
  • TagsGuidance and examples for using tags in instructions.
  • User Defined Persistent Storage TablesGuidance and examples for the User Defined Persistent Storage feature.

Key Concepts

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.

Method Parameters

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.

Method Output

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.

SQL

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:

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.

Convention

We suggest you write all SQL keywords in UPPERCASE.

Basic syntax

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:

Example - query all active network connections
Network.GetConnections();

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:

Example - query network connections, then logged on users
Network.GetConnections();
Users.GetLoggedOnUsers();

… 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.

Case sensitivity

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.

Example - String escaping
// 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.

Example - String concatenation
// 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:

Example - passing simple parameters
/* 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 true. A real will 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

@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
Example - @tables
@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.

Example - @tables
@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.

SQL queries

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):

Example - count network connections by 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.

Reserved keywords

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.)

1.0v3.0.0v3.3Potential future
  • EVALUATE
  • FALSE
  • TRUE
  • DO
  • DONE
  • FOREACH
  • IN
  • IF
  • ELSE
  • ENDIF
  • ERROR
  • NOCONTENT
  • NOTIMPLEMENTED
  • RETURN
  • ANY
  • BREAK
  • EMPTY
  • NONE
  • THEN
  • WHILE

Comments

Comments can either be multi-line or single-line.

Outside of SQL statements the Agent language supports C-style comments.

Agent language examples
/* 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).

SQL examples
/* 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.

General example
@flag = SELECT 1 AS value; /* This is a comment that has been correctly terminated */

Known issue

Comments within SQL statements should not include an unterminated quote, single or double. This is not a problem outside of a SQL statement.

Use of comments

Although comments are useful to annotate code, they do not serve any functional purpose. As such, you may wish to consider removing comments from your instructions, otherwise you will be sending comments to each Agent over the network only for them to be ignored when they are processed. At large scales, making small savings on payload sizes (e.g. by removing comments or reducing whitespace) can help to reduce network traffic.

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.

Instruction Parameters

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.

Example - parameter placeholders
@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.

Example - providing an alias for a column
@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.

Aggregating data

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.

Example - querying historic captured data
@products = Software.GetInstallations();
SELECT   Publisher
,        Count(*) AS Products 
FROM     @products
GROUP BY Publisher;

SELECTing Data

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:

Example - @table assignment
@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:

Example - @table reassignment
@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.

Limiting 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:

Example - JOINing and GROUPing
@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"

Example - SELECTing without a table
/* 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

Example - using LIMIT
@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 */

String concatenation

Example - basic string concatenation
@p = OperatingSystem.GetProcesses();

SELECT Executable || " was run by " || AccountName AS Message
FROM @p;

Concatenation from multiple @tables:

Example - string concatenation
@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

Example - SELECT CASE
@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;

Sub-SELECTs

Example - Sub-SELECT
@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;

RECURSIVE CTEs

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.

Example - CTE
@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 
  • RETURNNOCONTENTERROR, 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.

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.

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.

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".

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"
              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.

Advanced concepts

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:

Example - passing @tables as parameters
/* 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:

Example - passing non-qualified @tables as parameters
@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:

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.

  • Page:
    BASENAMEFROMPATH — Takes a path as a string and returns the basename.
  • Page:
    COMBINEPATHS — Takes two paths as strings, or a path and a basename as strings. It combines them and returns the result.
  • Page:
    COMPAREVERSIONS — Compare two version strings.
  • Page:
    DATETRUNC — Truncates a date-time value, expressed as a Unix epoch time integer, to a given resolution and returns a corresponding Unix epoch time integer.
  • Page:
    DIRECTORYFROMPATH — Takes a path as a string and returns the parent directory of the basename.
  • Page:
    EPOCHTOJSON — Takes a date-time value, expressed as a Unix epoch time in UTC, and returns a JSON-compatible (ISO-8601) string representation of that date-time, also in UTC.
  • Page:
    EXPAND — Expands environment variables in a string
  • Page:
    IPINRANGE — Determines if an IP Address is within a specified range or ranges.
  • Page:
    IPV4STRTOINT — Converts an IPv4 address into an integer.
  • Page:
    REGEXP — Determines if a string matches a regular expression.
  • Page:
    REGEXPCAPTURE — Uses a regular expression to capture matching parts from a string.
  • Page:
    REGEXPREPLACE — Uses a regular expression to replace a string.

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.

  • Page:
    HttpGetFile — A built-in function for downloading content (resources) over HTTP(S) via the Tachyon Background channel, or from an external web-server.
  • Page:
    IsUndefined — Given a table as a parameter, this built-in function will output a table that indicates whether the input 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.

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.

Environment variables

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 patternReplaced withIntroduced
%environment:now%

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.

v3.1

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.

For example:

Example - querying historic captured data
/* 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;

Instruction termination

Exit Codes

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.

StatusCodeErrorData messageMeaning
Success0n/a

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 content1n/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.

Error2Missing 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 SELECT expression, but instead just passes it on (with variable name substitutions and string literal translations) to the SQLite API. Hence, if there is a syntax error, any line or column numbers in an error message are not precise.

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>

Some examples:

MethodErrorData Notes
NativeServices.RunCommandCommand line returned exit code nnn.
Scripting.Run'Language' returned exit code nnn:'Language' is either Powershell or bash.
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 implemented3No 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:
  • the module is not enabled, for example Nomad methods are not available if Module.Nomad.Enabled=false
  • the module is not supported on this platform
  • the module library file does not exist on the device
  • the case-sensitive module name is not specified correctly
Unsupported method 'xxx'.The method is not supported on this platform. This error can occur if:
  • the method is not supported on this platform
  • the case-sensitive module name is not specified correctly
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:
  • the language is not supported on this platform (PowerShell is supported on Windows, bash is supported on Linux, MacOS and Solaris)
  • the language name is not specified correctly. Language name is not case-sensitive.
<custom error message>V3.3 onwards lets you return custom messages as defined in a NOTIMPLEMENTED statement.
Response too large4

Early exit

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

RETURN allows you to exit out of an instruction early with a given set of results.

Syntax
@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;

NOCONTENT

Syntax
//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 

ERROR

Syntax
//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...

NOTIMPLEMENTED

Syntax
//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)