-
-
Notifications
You must be signed in to change notification settings - Fork 0
Development
How to work on RE-Toolkit: setting up, adding stages and tools, testing, and the gates a change must clear.
This page is the practical companion to CONTRIBUTING.md, which holds the standards, and to Architecture and Design, which explains why the design is what it is.
- Setting up
- Running from a checkout
- Repository layout
- Adding an analysis stage
- Adding a scoring signal
- Adding a tool to the installer
- Adding a file type
- Testing
- Repository checks
- Diagnostic tools
- Definition of done
- Releasing
git clone https://github.com/Sandler73/RE-Toolkit.git
cd RE-Toolkit
# Provision the toolchain, but keep /opt/retoolkit free for your checkout
sudo ./install-retoolkit.sh --skip-source
# Development dependencies
sudo apt install shellcheck shfmt bats
python3 -m pip install --user pytest ruffWork in a disposable virtual machine. You will be running hostile files against code you are actively changing.
Point the library and stage directories at your working tree so the driver uses your code rather than an installed copy:
export RETOOLKIT_LIB_DIR=~/dev/retoolkit/lib
export RETOOLKIT_STAGES_DIR=~/dev/retoolkit/stages/static
~/dev/retoolkit/analyze-binaries.sh -t sample.exe -o ./out --log-level=debugDebug logging is worth keeping on during development, since skip reasons and tool-resolution decisions are logged at that level.
| Path | Contents |
|---|---|
install-retoolkit.sh |
Layered provisioner |
analyze-binaries.sh |
Driver: argument parsing and orchestration |
GhidraDump.py |
Ghidra postscript |
lib/ |
Shared modules sourced by the driver |
stages/static/ |
One file per stage |
tools/ |
Development and repository-check utilities |
tests/bats/ |
Shell test suite |
tests/python/ |
Python test suite |
wiki/ |
Wiki source, published to the separate wiki repository |
Module responsibilities are described in Architecture and Design. The rule that keeps them honest: a stage never decides whether it should run, only how to do its job.
Stage filenames are numbered by where their output belongs in a directory listing, not by execution order. Place a new stage where an analyst would expect to find its output.
stages/static/NN-name.sh, defining exactly one function, with a complete header
block. The required sections are Synopsis, Description, Execution Parameters,
Provides, Notes, and Version. tools/check-headers.py enforces this.
#!/usr/bin/env bash
# =============================================================================
# stages/static/NN-name.sh
# =============================================================================
#
# Synopsis:
# One sentence describing what this stage does.
#
# Description:
# What it does, why it is built this way, and anything a maintainer would
# otherwise have to rediscover by reading the code.
#
# Sourced by analyze-binaries.sh; not directly executable.
#
# Execution Parameters:
# $1 target Path to the sandboxed copy of the target binary.
# $2 outdir Path to this target's output directory.
#
# Provides:
# stage_name()
#
# Output subtrees:
# ${outdir}/NN-name/
#
# Skip controls:
# SKIP_NAME
#
# Notes:
# Release history is recorded in CHANGELOG.md.
#
# Version:
# 3.7.3 - 2026-05-03
# =============================================================================
stage_name() {
local target="$1" outdir="$2"
local out="${outdir}/NN-name"
[[ "${SKIP_NAME:-0}" == "1" ]] && { log_info "NN-name: skipped"; return 0; }
if ! command -v sometool >/dev/null 2>&1; then
log_warn "NN-name: sometool not installed, skipping"
return 0
fi
mkdir -p "$out"
run_tool "sometool" "$TOOL_TIMEOUT" sometool --analyze "$target" \
> "${out}/sometool.txt"
return 0
}The contract every stage honors:
- Take
$1target and$2outdir - Write only inside your own output subdirectory
- Invoke tools only through
run_toolorrun_shell - Honor the skip control
- Skip cleanly with a logged reason when a tool is unavailable
- Return, never
exit - Never modify the target
exit is the failure that matters most: it terminates the driver and discards
every remaining target in a batch.
Add the call in lib/dispatch.sh, in the branches for the types it applies to,
positioned correctly in the runtime order. Consult the order documented in that
file's header before inserting.
In analyze-binaries.sh argument parsing:
--no-name) SKIP_NAME=1; shift ;;A stage whose findings never reach _summary.json has not delivered anything an
analyst can use. If the stage produces findings that should influence the
verdict, parse its output in stages/static/85-summary.sh and contribute
weighted signals.
Regenerate the Stage Reference and add a changelog entry. The reference page is generated from stage headers, so an accurate header produces accurate documentation.
Signals are contributed in stages/static/85-summary.sh:
add_signal("signal_name", 20, f"evidence: {specific_observation}")Three rules:
Evidence must be specific. "Suspicious imports" is not evidence. "Imports VirtualAllocEx, WriteProcessMemory, CreateRemoteThread" is.
Do not double-count. If your signal describes the same underlying observation as an existing one, suppress the narrower signal when the composite fires. Otherwise one fact inflates a score by being counted several ways.
Update the test mirror. The scoring model is covered by a golden-sample
regression test that deliberately mirrors the implementation, because the logic
lives inside a bash heredoc and cannot be imported. Changing a weight or a band
threshold means updating tests/python/test-scoring-model.py in the same commit.
Prefer the distribution package. Use a source or vendor install only when the tool is genuinely not packaged.
- Add it to the appropriate layer
- Provide a source-build fallback where a rolling distribution may not carry it
- Make the install idempotent: detect an existing install and skip it
- Add it to the post-install verification table
- Record provenance for dual-use tooling: where it came from, and what verifies it
- Never fetch over plain HTTP
Then integrate it into a stage through run_tool, and parse its output into the
summary. Installing a tool that nothing invokes adds install time and no
capability.
- Add detection to
lib/detect-type.sh, ordered so more specific signatures win - Add a dispatch branch in
lib/dispatch.sh - Add or extend the stages that handle it
- Add parsing to the summary stage
- Document the type in the README table and the wiki
Detection ordering is the part most easily got wrong. A packed PE matches both the packer signature and the PE signature, so the packer test must come first.
# Shell tests
bats tests/bats
# Python tests
python3 -m pytest tests/python -v
# Everything CI runs, locally
shellcheck $(git ls-files '*.sh')
shfmt -d -i 4 -ci $(git ls-files '*.sh')
ruff check .
python3 tools/check-no-emdash.py
python3 tools/check-headers.py
python3 tools/check-version-consistency.pyTest against a real binary before claiming a stage works. Synthetic fixtures verify that code runs; they do not verify that a tool produces what the parser expects from a real target. Most defects found in practice were cases where a tool exited zero and produced output the parser could not use.
Three checkers enforce standards that review alone does not catch reliably.
| Check | Enforces |
|---|---|
tools/check-no-emdash.py |
No U+2014 anywhere in the repository |
tools/check-no-disclosures.py |
No personal, host, credential, or engagement detail |
tools/check-headers.py |
Every code file carries a complete header block |
tools/check-version-consistency.py |
One version across constants, headers, and changelog |
The disclosure check deserves a note, because what it catches does not look like a mistake. Development happens against real targets on a real workstation, so an operator home path in a usage example, a workstation hostname in a bug note, or the filename of an analyzed sample all read as ordinary technical detail. They pass review precisely because they are accurate. Published, they disclose who analyzed what, from which machine, and when.
The check covers four categories: identity, host, credentials, and engagement, plus authorship. The authorship category keeps build tooling out of the published repository: assistant or vendor attribution, model names, generated-by markers, build-sandbox paths, and co-author trailers.
Two deliberate non-rules are worth stating, because both look like gaps:
-
"LLM" is not flagged.
GhidraDump.pydocuments that its plain-text output is designed to be fed to a language model. That is a product design statement, not an attribution, and flagging it would be a false positive that teaches reviewers to ignore the check. - Allowlisting is scoped to the matched text, never the whole line. One benign token must not excuse a finding beside it. A machine-generated commit trailer carries a bot address that is legitimately allowlisted, and under line scope that address silently suppressed the attribution marker sitting next to it on the same line.
Use /path/to/..., sample.exe, or example.com in examples, and attribute
findings to "the operator". The check exempts only itself and its own test,
both of which must contain the patterns to work at all. There is deliberately no
inline suppression directive: one would let any line silence the check.
tools/validate-mermaid.mjs parses every mermaid diagram in the documentation.
GitHub renders diagrams server-side, so an invalid diagram does not fail a build;
it renders as an error box for a reader. The check makes that a build failure
instead.
All four run in CI. Run them locally before opening a pull request.
ShellCheck runs in two passes with different consequences.
| Pass | Severity | Effect |
|---|---|---|
| Blocking | error |
Fails the build |
| Advisory | warning |
Reported, does not fail the build |
The repository is clean at error severity. It carries a backlog of warning-severity findings, and being explicit about why is more useful than implying they were overlooked.
Most of the backlog is SC2164, a pushd or popd whose failure is not handled,
concentrated in install-retoolkit.sh. ShellCheck's suggested remedies are
|| exit or || return, and neither is correct there:
- Those call sites are at top level in the installer's main flow, not inside a
function, so
returnis invalid. -
exitcontradicts a deliberate design decision. The installer does not useset -ebecause it must survive an individual component failure, record it, and report everything unresolved in the verification table. Aborting on a failedpushdwould leave a half-provisioned system and no summary.
The correct fix is to restructure each site so the failure is handled and the install continues, which is real work rather than a flag. It is tracked separately and should not be done blind, since it changes control flow in code that can only be exercised by a full install on a real host.
One site sits inside a stage function, where return would be valid. It is
deliberately left alone as well. The stage cannot be exercised without a
provisioned host and a real sample, and changing control flow in working
analysis code to satisfy a linter is not a trade this project makes. Linting
describes the code; it does not license rewriting code that cannot be tested.
New code is expected to be warning-clean. Check before opening a pull request:
make shellcheck # blocking gate, must pass
make shellcheck-warnings # advisory, review your own additionsruff.toml holds the configuration. Repository tooling and tests are held to the
full rule set. Two files carry justified per-file ignores:
-
GhidraDump.pymust run under both Jython 2.7 and CPython 3, and executes inside a Ghidra script context where names such ascurrentProgramandgetScriptArgsare injected at runtime. They are undefined to a static reader and defined at execution. Import order is also deliberate, because a compatibility preamble must run before any Ghidra module is touched. -
tools/ghidra-diagnostic.pyimports late and conditionally on purpose: it probes the environment in stages, so an import that raises is a diagnostic result rather than a failure.
Add a suppression only when the reason is structural, and record the reason alongside it.
shfmt is available as make format but is not part of lint, check, or CI.
This codebase predates it and does not follow its default style. Adopting it
would rewrite roughly 26,000 lines of working analysis shell to satisfy a
formatter the project never used, which is churn with real regression risk and no
correctness benefit. The target exists for anyone who wants the diff; matching it
is not required.
| Tool | Purpose |
|---|---|
tools/ghidra-diagnostic.py |
Exercise the Ghidra path in isolation |
tools/stage30-repro.sh |
Reproduce a Ghidra stage run |
tools/file-stalker.sh |
Watch filesystem activity during a run |
tools/rename-trace.sh |
Trace rename operations |
tools/v13-bisect.sh |
Bisect a behavior change across stages |
file-stalker.sh is the tool to reach for when you suspect a stage is writing
outside its output directory, which is a contract violation worth catching early.
A change is not finished until all of these hold:
-
bash -npasses on every modified shell file -
shellcheckreports no new findings - Both test suites pass
-
tools/check-no-emdash.pyexits 0 -
tools/check-no-disclosures.pyexits 0 -
tools/check-headers.pyexits 0 -
tools/check-version-consistency.pyexits 0 - Header blocks are accurate and complete
- Documentation updated: wiki for behavior,
CHANGELOG.mdfor history - Any new tool invocation is bounded and recorded in the ledger
- Exercised against a real binary
State in the pull request what you verified and what you did not. A change verified in part is acceptable when the gap is declared; a change claimed as verified when it was not is not.
- Update
RETOOLKIT_VERSIONininstall-retoolkit.shandANALYZER_VERSIONinanalyze-binaries.sh - Update the
Versionline in every header block the change touched - Add a
CHANGELOG.mdentry with the date and the substantive changes - Update the version stated in
wiki/_Footer.mdandwiki/Home.md - Run every repository check and both test suites
- Tag the release as
vX.Y.Z
Tagging triggers the release workflow, which re-validates version consistency,
header compliance, and the em-dash guard, confirms the tag matches
RETOOLKIT_VERSION, confirms a changelog entry exists, runs the test suites, and
then builds and publishes the archives with checksums. A tag whose version does
not match the source fails the build rather than publishing a mislabeled release.
Versioning follows Semantic Versioning 2.0.0.
RE-Toolkit 3.7.3 -- MIT License. RE-Toolkit analyzes hostile binaries and, when
explicitly enabled, executes them: run it in a disposable virtual machine. Wiki
pages are kept at parity with the README and CHANGELOG.md in the
repository.
Getting started
Reference
Understanding it
Help
Contributing