redteamer logo

Guides

Authoring Modules

Design and build your own redteamer modules. This guide walks through setting up a git repo, wiring up an LSP-powered editor, and authoring your first *.rt.hcl module with inputs, actions, and outputs.


Overview

redteamer modules are HCL files (*.rt.hcl) stored in git repos.

This document focuses on authoring modules. Not on building new tools from scratch. You’ll typically wrap tools you already use (nmap, gobuster, custom scripts, etc.) in a consistent, auditable interface, and then compose those modules into higher-level workflows.

How to think about modules

At a high level, a module is a small, declarative description of an operation. That operation might be:

  • a single action performed by one tool, or
  • a phase in a larger workflow composed of multiple modules.

It can be tempting to design modules as end-to-end attack chains that promise to scan the network and auto-exploit everything. In practice, these are brittle, error-prone, and never a substitute for operator judgment.

It may also be tempting to start wrapping tools 1:1 and re-exposing all CLI flags as inputs. These modules are a lot of work to maintain and offer little value over running the tool directly: they have too many knobs, weak defaults, and don’t encode any knowledge. Prefer opinionated modules with a small set of meaningful inputs that reflect how your team actually uses the tool for a particular task.

The most useful modules are boring but reliable building blocks:

  • Per-tool modules wrap a single tool doing one well-defined job.
  • Transformation modules normalize, filter, merge, or enrich data between steps.
  • Workflow (phase-level) modules orchestrate several modules for a specific phase, such as internal discovery, external web surface triage, or an AD identity overview.

Modules can let senior operators capture institutional knowledge in executable form. Their tradecraft moves from docs and one-off scripts to reusable modules that juniors can run with consistent defaults, safer guardrails, and good observability while still being free to compose them into novel operations.

Create a Git Repo

Modules live in git so they can be:

  • versioned and reviewed
  • shared between operators
  • pinned and audited

From a fresh directory:

git init evilcorp-utils
cd evilcorp-utils

A simple layout:

evilcorp-utils/
  README.md
  web/
    dirbscan.rt.hcl
  aws/
    s3-enum.rt.hcl

Grouping by domain (web/, aws/, ad/) makes discovery easier.

If this is going to be shared:

git remote add origin git@github.com:evilcorp/utils.git
git add .
git commit -m "Initial module repo"
git push -u origin main

Later, operators can point redteamer at this repo:

rt prep https://github.com/evilcorp/utils

Use an LSP-Powered Editor

redteamer ships an LSP (Language Server Protocol) endpoint so you can get:

  • syntax highlighting & completion for HCL
  • inline validation and errors
  • hover help for functions and attributes

In your editor:

  • configure a new language server named e.g. redteamer-lsp
  • point its command to rt serve lsp
  • associate it with *.rt.hcl files

Once connected, you’ll get immediate feedback while editing modules—much nicer than discovering syntax errors at runtime.


Create Your First *.rt.hcl File

Inside your repo, create a file for your module, for example:

mkdir -p modules/web
$EDITOR modules/web/whoami.rt.hcl

A minimal starting point might look like:

module {
  description = "Report the current user"
}

action "whoami" {
  command = ["whoami"]
}

output "username" {
  type        = string
  description = "Username reported by the process"
  example     = "root"
  value       = chomp(action.whoami.stdout)
}

This isn’t yet a useful module, but it shows the basic building blocks:

  • an action block that describes what to run
  • an output block that describes an output data shape
  • a data transformation function that strips the trailing newline

As you type, your LSP-powered editor should start offering completions and diagnostics.


Define Inputs

Inputs are the knobs an operator (or CI, or an API client) can turn. Conceptually:

  • each input has a name, type, optional default, and a description
  • inputs become values you can reference via input.<name>
  • they surface automatically as flags in rt run and as fields in the REST / MCP tool interfaces

A simple input set:

input "target" {
  type        = string
  description = "Target host or URL"
}

input "timeout" {
  type        = number
  default     = 30
  description = "Timeout in seconds per attempt"
}

You might then reference them:

# Interpolated into strings
url = "http://${input.target}/"

# Used in expressions
effective_timeout = input.timeout > 0 ? input.timeout : 30

Good Input Hygiene:

  • Prefer explicit types (string, number, bool) over generic any.
  • Use sensible defaults where possible so rt run <module> “just works”.
  • Validate values with functions where relevant, e.g.:
    assert {
      condition = is(var.target, "ipv4") || is(var.target, "url")
      error_msg = "Must be valid IPv4 address or URL"
    }
    
  • Keep input names short but descriptive: domain, wordlist, region, etc.

Define Actions

Actions are where the work happens. Each action block:

  • can call tools, scripts, APIs, or even other modules
  • produces structured results that you can pass into outputs or downstream modules

A simple action that calls a directory brute-forcer:

action "dir_scan" {
  # Reuse an existing tool module
  with = "core/tools/gobuster/dir"

  # Optional conditional execution
  when = input.enable_scanning

  base_url = input.target
  wordlist = input.wordlist
  threads  = 10
}

Because actions are regular HCL blocks:

  • you can loop with for_each or count
  • you can branch with when
  • you can use functions (tolist, flatten, timeadd, scan, etc.) to shape inputs/outputs

Example with for_each:

action "dir_scan" {
  for_each = toset(input.targets)

  with     = "core/tools/gobuster/dir"
  base_url = "http://${each.value}/"
  wordlist = input.wordlist
}

Later, you can aggregate results with splats:

var {
  all_results = values(action.dir_scan)[*].results
}

Define Outputs

Outputs are the published interface of a module run. They:

  • show up in rt explain <module> and rt output <run-id>
  • can be referenced by other modules
  • are included in audit logs, API responses, and MCP tool results

A simple output:

output "found_paths" {
  type        = list(string)
  description = "Discovered HTTP paths"
  example     = ["/admin", "/login"]  
  value       = action.dir_scan.results.paths
}

You can also build richer objects:

output "summary" {
  description = "Summary but this would be better as indivudual output blocks"
  type = object({
    target        = string
    total_paths   = number
    started_at    = string
    finished_at   = string
    elapsed_human = string
  })
  value = {
    target        = var.target
    total_paths   = len(action.dir_scan.results.paths)
    started_at    = action.dir_scan.started_at
    finished_at   = action.dir_scan.finished_at
    elapsed_human = fmttime(action.dir_scan.started_at)
  }
}

For multi-target actions:

output "all_results" {
  value = flatten(values(action.dir_scan)[*].results.paths)
}

Test the Module End-to-End

Once your module has inputs, actions, and outputs, test it locally:

# From inside your repo
rt run evilcorp/utils/web/whoami -i target=https://example.com

Inspect recent runs:

rt audit --limit 5
rt output <run-id> --trace
rt output <run-id> --format json

If something looks off:

  • fix the .rt.hcl file
  • lean on your LSP diagnostics
  • re-run rt run ... until the behavior and outputs match your intent

When you’re happy:

  • commit the module
  • push it to your shared git remote
  • other operators can rt prep / rt sync and use it via CLI, API, or MCP AI tools.