Skip to content

Dynamic Aliases

Dynamic aliases are custom commands defined in Python scripts. They appear alongside built-in commands in the Tuoni UI and terminal, and can be sent to agents just like any native command.

Aliases can:

  • Chain multiple built-in commands into a single operation
  • Pre-process configuration before sending commands
  • Post-process and combine results from multiple commands
  • Interact with discovery (add hosts, services, credentials)
  • Modify agent metadata without sending any commands to the agent

Alias Class Contract

An alias is a Python class that implements four methods:

class MyAlias:

    def configuration_schema(self):
        """Return a JSON Schema string describing the alias configuration."""
        ...

    def can_send_to_agent(self, agent):
        """Return True if this alias can run on the given agent. Raise an exception if not."""
        ...

    def validate_config(self, config, agent):
        """Validate the configuration. Raise an exception if invalid."""
        ...

    def execute(self, ctx, config, agent):
        """Execute the alias logic. Must call ctx.finish() or ctx.fail() when done."""
        ...
Method Parameters Returns Called When
configuration_schema() str (JSON Schema) UI/API requests the schema
can_send_to_agent(agent) agent: Agent bool Before showing the command as available
validate_config(config, agent) config: Configuration, agent: Agent None Before execution, raise on invalid
execute(ctx, config, agent) ctx: AliasContext, config: Configuration, agent: Agent None When the command is executed

Optional Properties

Set these in __init__ to provide metadata:

1
2
3
4
class MyAlias:
    def __init__(self):
        self.name = "myalias"                      # Used if name not passed to register_dynamic_alias
        self.description = "Does something useful"  # Shown in UI and API responses

Registration

Register an alias instance (not the class) with tuoni.commands.register_dynamic_alias:

1
2
3
4
5
6
7
import tuoni

# Pattern 1: Explicit name
tuoni.commands.register_dynamic_alias("myalias", MyAlias())

# Pattern 2: Name from self.name attribute
tuoni.commands.register_dynamic_alias(MyAlias())

Pass an Instance

register_dynamic_alias expects an instance of your class, not the class itself. Use MyAlias() with parentheses, not MyAlias.

You can register multiple aliases from a single script file:

tuoni.commands.register_dynamic_alias("alias-a", AliasA())
tuoni.commands.register_dynamic_alias("alias-b", AliasB())

The alias is available in the UI/API as script:global:<name> (e.g. script:global:myalias).


Configuration Schema

The configuration_schema() method returns a JSON Schema string. Tuoni extends standard JSON Schema with two additional fields:

files

Declares file upload fields. Keys are field names, values are empty objects:

1
2
3
4
5
6
{
    "files": {
        "bofFile": {},
        "payloadFile": {}
    }
}

positional

Maps schema properties to positional arguments in the terminal:

1
2
3
4
5
6
{
    "positional": {
        "target": { "position": 0, "required": true },
        "port":   { "position": 1, "required": false }
    }
}

This allows terminal usage like: myalias 192.168.1.10 8080 instead of defining explicit arguments.

Full Example

def configuration_schema(self):
    return """{
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
            "target": {
                "type": "string",
                "description": "Target IP address"
            },
            "port": {
                "type": "integer",
                "description": "Target port (default: 445)"
            }
        },
        "required": ["target"],
        "files": {},
        "positional": {
            "target": { "position": 0, "required": true },
            "port":   { "position": 1, "required": false }
        }
    }"""

Agent Compatibility

The can_send_to_agent method receives an Agent object. Return True if the alias supports this agent, or raise an exception with a message:

1
2
3
4
5
6
7
def can_send_to_agent(self, agent):
    # Linux shellcode agents only
    if agent.type != "SHELLCODE_AGENT":
        return False
    if agent.get_latest_metadata().os != "LINUX":
        return False
    return True

Common agent types: SHELLCODE_AGENT, GENERIC_SHELL_AGENT, EXTERNAL_AGENT

Common OS values: LINUX, WINDOWS

Common architecture values: X64, X86


Queuing Sub-Commands

Inside execute(), use ctx.queue_command() to send commands to the agent:

1
2
3
def execute(self, ctx, config, agent):
    cmd = ctx.queue_command(command_template, config_dict)
    cmd.wait_for_finish()

Command Template IDs

You can reference built-in commands using short or fully-qualified names:

Short Form Fully Qualified Description
"sh" "shelldot.commands.native:global:sh" Shell command
"ps" "shelldot.commands.native:global:ps" Process list
"download" "shelldot.commands.native:global:download" Download file from agent
"upload" "shelldot.commands.native:global:upload" Upload file to agent
"bof" "shelldot.commands.native:global:bof" Execute BOF

The format is <plugin>:<scope>:<command>. Plugin commands use their plugin ID.

Configuration Formats

There are three ways to pass configuration to a sub-command:

Simple dict — Treated as JSON-only configuration:

ctx.queue_command("sh", {"command": "whoami"})

Structured dict with json and/or files keys:

1
2
3
4
5
6
7
8
9
ctx.queue_command("bof", {
    "json": {
        "method": "go",
        "inputAsBytes": b""
    },
    "files": {
        "bofFile": open("./my.bof", "rb")
    }
})

Reusing file IDs from previous results:

1
2
3
4
5
6
7
8
result_file = list(download_cmd.get_result().items())[0][1]

ctx.queue_command("upload", {
    "json": {"filepath": "/tmp/output.txt"},
    "files": {
        "file": {"file_id": result_file.file_id}
    }
})

File Passing Alternatives

There are multiple ways to pass files in configuration:

  • open("path", "rb") — File-like object (script must manage closing)
  • {"file_id": "uuid-string"} — Reference an existing file in Tuoni by ID
  • result_file — Pass a File object directly (has a .file_id property)
  • io.BytesIO(bytes_data) — In-memory bytes as a file-like object

Execution Configurations

By default, commands execute in the agent's current process context (SelfExecutionConfiguration). You can override this by passing an execution configuration as the third argument:

import tuoni

# Execute in a new process
exec_cfg = tuoni.NewExecutionConfiguration(executable="cmd.exe")
ctx.queue_command("sh", {"command": "whoami"}, exec_cfg)

# Execute in an existing process by PID
exec_cfg = tuoni.ExistingExecutionConfiguration(pid=1234)
ctx.queue_command("sh", {"command": "whoami"}, exec_cfg)

# Default: execute in current agent context (same as omitting the argument)
exec_cfg = tuoni.SelfExecutionConfiguration()
ctx.queue_command("sh", {"command": "whoami"}, exec_cfg)

tuoni.NewExecutionConfiguration

Parameter Type Description
executable str \| None Path to executable for the new process
suspended bool \| None Start the process suspended
ppid int \| None Parent process ID for spoofing
username str \| None Run as this user
password str \| None Password for the specified user

All parameters are optional keyword arguments and can also be set as properties after creation.


Result Handling

Setting Results

Use ctx.set_result(dict) to provide the command output. Keys are result names, values can be:

  • str — Text result
  • bytes — Binary result
  • File-like object — Stored as a downloadable file
  • {"file_id": "uuid"} — Reference to an existing Tuoni file
  • Object with .file_id property — Same as above
1
2
3
4
5
6
7
8
# Text result
ctx.set_result({"STDOUT": "Command output here"})

# Multiple results
ctx.set_result({
    "STDOUT": "Summary text",
    "report": open("/tmp/report.txt", "rb")
})

Intermediate Results

You can call ctx.set_result() multiple times. Each call updates the result shown to the user. This is useful for providing progress updates during long-running aliases.

Finishing

Every alias must call either ctx.finish() or ctx.fail():

1
2
3
4
5
6
# Success
ctx.set_result({"STDOUT": "Done"})
ctx.finish()

# Failure
ctx.fail("Something went wrong: " + error_message)

Always Terminate

If execute() returns without calling ctx.finish() or ctx.fail(), the alias will appear as perpetually running. Always ensure one of these is called, including in error paths.


Error Handling Pattern

This is the standard pattern used throughout Tuoni scripts for handling sub-command failures:

def execute(self, ctx, config, agent):
    cmd = ctx.queue_command("sh", {"command": "whoami"})
    cmd.wait_for_finish()

    if cmd.is_failed():
        ctx.fail(cmd.get_error_message())
        return

    ctx.set_result(cmd.get_result())
    ctx.finish()

For multiple chained commands:

def execute(self, ctx, config, agent):
    # First command
    cmd1 = ctx.queue_command("sh", {"command": "echo hello"})
    cmd1.wait_for_finish()
    if cmd1.is_failed():
        ctx.fail(cmd1.get_error_message())
        return

    # Second command
    cmd2 = ctx.queue_command("sh", {"command": "echo bye"})
    cmd2.wait_for_finish()
    if cmd2.is_failed():
        ctx.fail(cmd2.get_error_message())
        return

    # Combine results
    ctx.set_result({
        "STDOUT": cmd1.get_result()["STDOUT"] + cmd2.get_result()["STDOUT"]
    })
    ctx.finish()

Aliases Without Agent Commands

Aliases don't have to send commands to agents. You can manipulate agent metadata directly:

class TagAlias:

    def __init__(self):
        self.name = "tag"
        self.description = "Add a tag to the agent"

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

    def can_send_to_agent(self, agent):
        return True  # Works on all agent types

    def validate_config(self, config, agent):
        pass

    def execute(self, ctx, config, agent):
        tag = config.json.as_dict["tag"]
        notes = agent.metadata.custom_properties.get("notes", "")
        if f"#{tag}" not in notes.split(";"):
            agent.metadata.custom_properties["notes"] = f"{notes};#{tag}" if notes else f"#{tag}"
            ctx.set_result({"STDOUT": "Tag added"})
        else:
            ctx.set_result({"STDOUT": "Tag already exists"})
        ctx.finish()

Custom Properties

agent.metadata.custom_properties is an observable dict — changes are persisted immediately to the server. No explicit save call is needed. See AgentMetadata for all writable properties.