# How the LazyOwn .tool File Trigger System Automates Security Tool Execution

> Automate security tool execution using LazyOwn's .tool file trigger system. Match services to triggers and dynamically generate commands for seamless security workflows.

- Repository: [Grisuno/lazyown](https://github.com/grisuno/lazyown)
- Tags: how-to-guide
- Published: 2026-03-02

---

**The LazyOwn .tool file trigger system automatically executes security tools by matching discovered network services against JSON-defined trigger lists, dynamically generating executable console commands through placeholder substitution.**

LazyOwn is an open-source penetration testing framework that streamlines reconnaissance through a declarative automation engine. The `.tool` file trigger system treats every JSON file in the `tools/` directory as a configurable automation rule, activating specific security tools whenever Nmap scans detect matching services on target hosts.

## Loading JSON Tool Definitions from the tools/ Directory

At startup and after each network scan, [`lazyown.py`](https://github.com/grisuno/lazyown/blob/main/lazyown.py) scans the `tools/` directory using `glob` to discover all files ending with the `.tool` extension. The framework loads each file as a JSON object and validates that the tool is **active** and contains both a `toolname` and `command` field:

```python
tool_dir = "tools"
for tool_file in glob.glob(os.path.join(tool_dir, "*.tool")):
    with open(tool_file, "r") as f:
        tool_data = json.load(f)

    tool_name = tool_data.get("toolname")
    command_template = tool_data.get("command")
    triggers = tool_data.get("trigger", [])
    active = tool_data.get("active", False)

    if not active or not tool_name or not command_template:
        continue

```

Only tools passing this validation proceed to the trigger matching phase. This filter ensures that incomplete or deactivated tool definitions do not clutter the execution environment.

## Matching Scan Results Against Trigger Lists

After completing an Nmap scan, LazyOwn parses the resulting XML report (`sessions/scan_<rhost>.nmap.xml`) and iterates through every discovered host and service. For each service, the framework checks whether the service name appears in the tool's `trigger` array or if the special value `"all"` is present:

```python
for host in report.hosts:
    for service in host.services:
        if service.service in triggers or "all" in triggers:
            cmd_params = {
                "ip": host.address,
                "port": str(service.port),
                "domain": domain,
                "dnswordlist": dnswordlist,
                "service": service.service,
                "proto": service.protocol,
                "username": start_user,
                "password": start_pass,
                "outputdir": os.path.join(
                    f"sessions/{rhost}/{tool_name}/{tool_name}.txt",
                    host.address,
                    str(service.port),
                    tool_name
                ),
                "tunnel": "s" if service.tunnel == "ssl" else "",
            }

```

When a match occurs, LazyOwn builds a **parameter dictionary** containing network context, credentials, and output paths. This dictionary serves as the data source for command template substitution.

## Placeholder Substitution with replace_command_placeholders

The `command` field in a `.tool` file contains a template string with placeholders wrapped in curly braces, such as `{ip}`, `{port}`, and `{outputdir}`. LazyOwn passes these templates to the `replace_command_placeholders` helper defined in [`utils.py`](https://github.com/grisuno/lazyown/blob/main/utils.py), which uses regular expressions to safely substitute values while tolerating whitespace variations:

```python
final_command = replace_command_placeholders(command_template, cmd_params)

```

This transformation converts abstract templates into concrete shell commands ready for execution. For example, a template like `nmap -p {port} {ip}` becomes `nmap -p 22 192.168.1.42` when matched against an SSH service.

## Dynamic Method Registration (do_<toolname>)

Once the final command string is prepared, LazyOwn dynamically attaches a new method to the interactive command interpreter class. The framework creates a wrapper function that calls `self.cmd(final_cmd)` and assigns it the name `do_<tool_name>` using `setattr`:

```python
def tool_wrapper(*args, final_cmd=final_command):
    self.cmd(final_cmd)

setattr(self.__class__, f"do_{tool_name}", tool_wrapper)

```

This registration pattern allows operators to execute the tool immediately by typing its name in the console. The dynamically generated method inherits the fully expanded command, ensuring all network-specific parameters are baked into the callable at creation time.

## Web-Based Tool Management via lazyc2.py

The Flask-based C2 portal ([`lazyc2.py`](https://github.com/grisuno/lazyown/blob/main/lazyc2.py)) provides comprehensive CRUD operations for `.tool` files through HTTP routes. Administrators can list existing tools, create new definitions, or deactivate outdated ones without editing files manually. When creating a tool via the web interface, the route constructs a JSON structure matching the loader's schema:

```python
tool_data = {
    "toolname": securetoolname,
    "command": command,
    "trigger": trigger,
    "active": active
}
with open(tool_path, "w") as file:
    json.dump(tool_data, file, indent=4)

```

Changes made through the web UI take effect immediately upon the next scan cycle, enabling real-time customization of the automation pipeline during active engagements.

## Standalone Execution with pwntomate.py

The [`pwntomate.py`](https://github.com/grisuno/lazyown/blob/main/pwntomate.py) script demonstrates that the trigger system operates independently of the interactive console. This lightweight runner iterates over `.tool` files, applies the same `active` and `trigger` filtering logic, and performs placeholder replacement to build a shell script for batch execution:

```python
for filename in glob.glob(args.tooldir+"/*.tool"):
    tool = json.load(open(filename, 'r'))
    if tool["active"] and (service.service in tool["trigger"] or 'all' in tool["trigger"]):
        cmd = tool["command"]
        # simple placeholder replacement (see pwntomate.py)

```

This modular design proves that the `.tool` trigger architecture is reusable across different execution contexts, from interactive shells to automated batch processing.

## Summary

- **LazyOwn** treats every `.tool` file as a JSON-encoded automation rule containing `toolname`, `command`, `trigger`, and `active` fields.
- The **trigger matching engine** compares discovered Nmap services against trigger lists, supporting specific service names or the universal `"all"` keyword.
- **Parameter dictionaries** capture scan data including IP addresses, ports, protocols, and SSL tunnel status for dynamic substitution.
- The **`replace_command_placeholders`** function in [`utils.py`](https://github.com/grisuno/lazyown/blob/main/utils.py) safely expands curly-brace templates into executable commands.
- **Dynamic method registration** creates `do_<toolname>` methods on the interpreter class, making tools instantly accessible via console commands.
- The **web UI** in [`lazyc2.py`](https://github.com/grisuno/lazyown/blob/main/lazyc2.py) and **standalone runner** [`pwntomate.py`](https://github.com/grisuno/lazyown/blob/main/pwntomate.py) demonstrate the system's flexibility across interactive and automated workflows.

## Frequently Asked Questions

### What is the JSON structure of a .tool file?

A `.tool` file contains a JSON object with four required fields: `toolname` (string identifier), `command` (template string with placeholders), `trigger` (array of service names or `["all"]`), and `active` (boolean flag). The `_example.tool` template in the `tools/` directory demonstrates this schema with a sample command that writes protocol information to an output file.

### How does the "all" trigger value work?

When a `.tool` file includes `"all"` in its `trigger` array, LazyOwn executes that tool for **every service** discovered during the Nmap scan, regardless of service type. This is useful for universal reconnaissance tools like screenshots or banner grabbers that should run against all open ports.

### Can I manually execute a tool without running an Nmap scan?

Yes. Once LazyOwn loads a `.tool` file and registers it via `setattr`, you can invoke the tool directly through its dynamic method name. Type `do_<toolname>` in the console to execute the pre-populated command. However, the command will use parameters from the last scan cycle; for fresh targets, a new scan is required to update the parameter dictionary.

### Where does LazyOwn store tool execution output?

The framework constructs output paths using the `outputdir` parameter, which defaults to `sessions/<rhost>/<tool_name>/<tool_name>.txt/<ip>/<port>/<tool_name>`. This nested structure organizes results by target host and port, preventing collisions when the same tool runs against multiple services during a single scan.