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

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

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:

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, which uses regular expressions to safely substitute values while tolerating whitespace variations:

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

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:

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

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

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 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 and standalone runner 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →