Skip to content

Tuoni Script Engine

The Tuoni Script Engine runs Python automation inside the Tuoni Server process using GraalPy Community. It is enabled by default in Tuoni 0.15.0.

Scripts can:

  • create dynamic command aliases
  • query agents and update their status or metadata
  • react to audit events
  • send commands and process their results
  • manage discovery data, files, listeners, and payloads

The Python standard library is available. Pure-Python packages can be installed in the configured virtual environment; native C extensions are not supported.

Module migration in 0.15.0

New scripts must use the public tuoni_script_engine module:

import tuoni_script_engine as tse

The former tuoni module remains available for existing scripts in 0.15.0 but is deprecated. Migrate both the import and the API calls; the new module uses typed enums, explicit configuration objects, read-only properties with update methods, and revised command and event interfaces.

Install tuoni-script-engine-stubs in a development environment for API documentation, type checking, and editor completion. The package is not required by scripts running in Tuoni.

Getting started

Place a .py file in the scripts directory, which defaults to /srv/tuoni/data/scripts. No Settings toggle or API request is required. Tuoni detects and executes new and modified files automatically.

1
2
3
import tuoni_script_engine as tse

print(f"Tuoni can be reached at: {', '.join(tse.ips.list())}")

Script output and errors are written to ${tuoni.scripting.logs-dir}/<script-name>.log, which defaults to /srv/tuoni/data/script_logs/<script-name>.log.

File changes

File changes are debounced for three seconds by default. Changing a running script stops its current execution and starts it again after the debounce interval.

Configuration

Set Script Engine properties in tuoni.yml or with the corresponding server command-line options.

Property Default Description
tuoni.scripting.scripts-dir ${tuoni.data-dir}/scripts Directory watched for .py scripts
tuoni.scripting.logs-dir ${tuoni.data-dir}/script_logs Directory for script output and errors
tuoni.scripting.file-change-debounce-time 3s Delay before a changed script is restarted

Permissions and sandboxing

Each script runs in an isolated GraalVM context. Configure its filesystem, network, and environment access under tuoni.scripting.permissions:

1
2
3
4
5
6
tuoni:
  scripting:
    permissions:
      fs-access: "sandbox"
      allow-net-access: false
      allow-env-access: false

Filesystem access

Mode Value Read access Write access
Sandbox "sandbox" Scripts directory, Tuoni files directory, GraalPy home, and virtual environment /tmp/tuoni-script-<name> only
Read-only "read-only" Sandbox paths and the real filesystem /tmp/tuoni-script-<name> only
Read-write "read-write" Full filesystem; scripts and Tuoni files remain read-only Full filesystem
Property Default Description
allow-net-access false Permit outbound network connections
allow-env-access false Permit reads from environment variables

Trusted scripts only

Combining fs-access: "read-write" with allow-net-access: true effectively removes the sandbox. Such a script has the same access as the Tuoni Server process.

Script lifecycle

  1. Tuoni detects a new or modified .py file.
  2. It creates an isolated GraalPy context and makes tuoni_script_engine available.
  3. The script runs from top to bottom.
  4. A script with active event subscriptions stays alive.
  5. The script stops after normal completion with no active subscriptions, when every subscription is stopped, when the file is deleted, or when the server shuts down.

Minimal dynamic alias

This alias runs whoami on Windows shellcode agents:

import tuoni_script_engine as tse


class WhoamiAlias:
    name = "script-whoami"

    def configuration_schema(self):
        return """{
            "$schema": "https://json-schema.org/draft/2020-12/schema",
            "type": "object",
            "properties": {},
            "required": []
        }"""

    def can_send_to_agent(self, agent):
        return (
            agent.type is tse.AgentType.SHELLCODE_AGENT
            and agent.metadata.operating_system is tse.OperatingSystem.WINDOWS
        )

    def validate_config(self, config, agent):
        pass

    def execute(self, ctx, config, agent):
        command = ctx.queue_command(
            "cmd",
            tse.JsonConfiguration({"command": "whoami"}),
        )
        command.wait_for_completion()

        if not command.is_success() or command.result is None:
            error = (
                command.result.error_message
                if command.result is not None
                else "The command returned no result"
            )
            ctx.fail(error or "The command failed")
            return

        ctx.set_result(command.result.entries)
        ctx.finish()


tse.commands.register_dynamic_alias(WhoamiAlias())

After the file is saved, script-whoami appears in the Tuoni UI for compatible agents.

Next steps