Add binwalk, esptool and the ESP32 flash loader for Ghidra

binwalk: new binwalk-build stage adapted from binwalk's own Dockerfile
to Ubuntu 24.04, with every source pinned: binwalk master (3.1.1),
Rust 1.99.1, dumpifs/lzfse/dmg2img commits, and the Python extractors
(jefferson, ubi-reader, uefi_firmware, vmlinux-to-elf, lz4, zstandard)
in /opt/binwalk-py. The final image adds upstream's apt extractors,
sasquatch (SHA-256 pinned), and Kaleido via KALEIDO_PATH so that
binwalk -E -p works (binwalk's build otherwise leaves it in /root of the
build stage and -p panics). BINWALK_RM_EXTRACTION_SYMLINK=1 avoids
dangling links to container paths.

esptool 5.6.0 in /opt/esptool. Entry points of both venvs are linked
into /usr/local/bin without shadowing the system python3.

ESP32 flash loader: dynacylabs/ghidra-esp32-flash-loader, built in an
esp32-loader-build stage against Ghidra 12.1.4 (no binaries are
published, and the older forks don't compile against Ghidra 12),
pinned to a master commit with its svd and ghidra_scripts submodules,
Gradle checked against gradle.org's SHA-256. Installed into
Ghidra/Extensions/esp32. Ghidra itself is now downloaded once in a
ghidra-dist stage and copied into the final image and the build stage.

Verified: binwalk extracted squashfs/gzip/cpio/7z from a test image and
rendered an entropy PNG; the loader imported a test ESP32 app image and
a flash dump with bootloader at 0x0 (partition table, bootloader and
factory app, SVD peripherals, ROM symbols). Original-ESP32 dumps with
the bootloader at 0x1000 are not supported; documented in CLAUDE.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-02 14:02:22 +01:00
co-authored by Claude Opus 5.5
parent ab6fc403e4
commit dbce497f95
2 changed files with 186 additions and 18 deletions
+4 -2
View File
@@ -13,7 +13,7 @@ The goal is that `docker compose up` gives you a working setup where Claude Desk
## Status
Early stage. What exists: a single image (`Dockerfile`, `launch-apps.sh`) with Claude Desktop, Ghidra and GhidraMCP, started by `run.sh`; Claude Desktop's MCP config is `claude-desktop/claude_desktop_config.json`; Ghidra config files are in `ghidra/`. No compose setup yet. The layout below is the **intended** structure. Update this file when the real structure differs.
Early stage. What exists: a single image (`Dockerfile`, `launch-apps.sh`) with Claude Desktop, Ghidra (plus GhidraMCP and an ESP32 loader), binwalk and esptool, started by `run.sh`; Claude Desktop's MCP config is `claude-desktop/claude_desktop_config.json`; Ghidra config files are in `ghidra/`. No compose setup yet. The layout below is the **intended** structure. Update this file when the real structure differs.
## Intended layout
@@ -37,7 +37,7 @@ Early stage. What exists: a single image (`Dockerfile`, `launch-apps.sh`) with C
## Architecture
- **Claude Desktop and Ghidra share one container** (the root `Dockerfile`, started by `run.sh`), at the user's request. Ghidra is the official GitHub release (version, build date and SHA-256 are build args), is pinned to **12.1.4** because that's what the GhidraMCP release is built for (Ghidra only loads extensions built for its exact version), runs on Ubuntu's `openjdk-21-jdk`, lives in `/opt/ghidra` and starts together with Claude Desktop. The **Developer** plugin package is enabled in both the project window and the CodeBrowser. A bare `<PACKAGE NAME="Developer" />` only adds the package's RELEASED plugins, so the STABLE ones are listed with `<INCLUDE CLASS=...>`. CodeBrowser: the build inserts `ghidra/codebrowser-developer.xml` into `defaultTools/CodeBrowser.tool` inside `Public_Release.jar`. Project window: it has no default to patch, so `ghidra/FrontEndTool.xml` is copied into the user settings, along with `ghidra/preferences` (`USER_AGREEMENT=ACCEPT`, because a pre-created settings dir otherwise makes Ghidra ask for its user agreement). To check a config really loaded, close Ghidra cleanly and see whether the plugins survive in the file it rewrites; Ghidra drops what it didn't load.
- **Claude Desktop and Ghidra share one container** (the root `Dockerfile`, started by `run.sh`), at the user's request. Ghidra is the official GitHub release (version, build date and SHA-256 are global build args, downloaded once in the `ghidra-dist` stage and copied into the final image and into build stages that compile extensions against it), is pinned to **12.1.4** because that's what the GhidraMCP release is built for (Ghidra only loads extensions built for its exact version), runs on Ubuntu's `openjdk-21-jdk`, lives in `/opt/ghidra` and starts together with Claude Desktop. The **Developer** plugin package is enabled in both the project window and the CodeBrowser. A bare `<PACKAGE NAME="Developer" />` only adds the package's RELEASED plugins, so the STABLE ones are listed with `<INCLUDE CLASS=...>`. CodeBrowser: the build inserts `ghidra/codebrowser-developer.xml` into `defaultTools/CodeBrowser.tool` inside `Public_Release.jar`. Project window: it has no default to patch, so `ghidra/FrontEndTool.xml` is copied into the user settings, along with `ghidra/preferences` (`USER_AGREEMENT=ACCEPT`, because a pre-created settings dir otherwise makes Ghidra ask for its user agreement). To check a config really loaded, close Ghidra cleanly and see whether the plugins survive in the file it rewrites; Ghidra drops what it didn't load.
- **Shared workspace volume.** Every container mounts the workspace at `/workspace`, so a file path means the same thing to Claude, Ghidra and the RE MCP. `run.sh` asks for the host folder on each start in the desktop's folder picker (`zenity`, or `kdialog` on KDE; terminal prompt if neither exists), opening at the last folder used, else `./workspace`. Skip it with `--workspace DIR` or `WORKSPACE=DIR`. It refuses `/` and `$HOME`. The image symlinks `~/workspace` to `/workspace` for Claude Desktop's file picker.
- **GUI forwarding.** Claude Desktop and Ghidra are GUI apps. Default to X11 forwarding (`/tmp/.X11-unix` + `DISPLAY`). Support Wayland through XWayland. Keep a noVNC/VNC fallback in mind for headless hosts. Never solve display access with a blanket `xhost +`. Scope it (`xhost +local:docker` or `+SI:localuser:...`).
- **GhidraMCP wiring.** LaurieWired's GhidraMCP (https://github.com/LaurieWired/GhidraMCP), release 2.0, installed from the official release zip (SHA-256 checked). It has two parts:
@@ -45,6 +45,8 @@ Early stage. What exists: a single image (`Dockerfile`, `launch-apps.sh`) with C
- the `ghidra-mcp` Python MCP server (2.0 replaced `bridge_mcp_ghidra.py`), installed from the release wheel into the venv `/opt/ghidra-mcp`. Its dependencies are pinned to GhidraMCP's `uv.lock` at the release commit via `ghidra/uvlock-to-constraints.py`. CLI: `ghidra-mcp --ghidra-server http://127.0.0.1:8080/` (stdio by default; also `--transport streamable-http`, `--headless`).
A program must be open in the CodeBrowser for the tools to return data. Claude Desktop only starts MCP servers after sign-in.
- **ESP32 Flash Loader** (https://github.com/dynacylabs/ghidra-esp32-flash-loader): a Ghidra loader ("ESP32 Flash Image" in *File → Import File*) for ESP32 app images and flash dumps. It reads the partition table at `0x8000`, maps segments to their addresses, adds peripherals from Espressif's SVD files and loads the chip's ROM ELF for symbols. A flash dump is only recognized when it **starts with the bootloader** (ESP32-S/C layouts). Original-ESP32 dumps (bootloader at `0x1000`) are rejected, and trimming the first `0x1000` bytes doesn't help: the loader then finds no partition table at `0x8000`, loads only the bootloader and throws a NullPointerException while still reporting success. Workaround: cut the app partition out of the dump (offsets from the partition table) and import it as an app image. Its tags ship no binaries and the older forks (tslater2006, Ebiroll, saibotk v1.1.0) don't compile against Ghidra 12, so the `esp32-loader-build` stage builds dynacylabs' master at a pinned commit, with its `data/svd` and `ghidra_scripts` submodules fetched over HTTPS at pinned commits and Gradle checked against gradle.org's SHA-256. It is unpacked into `/opt/ghidra/Ghidra/Extensions/esp32`; loaders need no tool config.
- **binwalk and esptool** are CLI tools in the same image (no MCP server yet). The `Dockerfile` has a `binwalk-build` stage adapted from binwalk's own Dockerfile (https://github.com/ReFirmLabs/binwalk) to Ubuntu 24.04, with every source pinned: binwalk at a master commit (3.1.1; the last tag, v3.1.0, is from 2024), Rust toolchain, `dumpifs`/`lzfse`/`dmg2img` commits, and the Python extractors (`jefferson`, `ubi-reader`, `uefi_firmware`, `vmlinux-to-elf`, ...) in `/opt/binwalk-py`. esptool is in `/opt/esptool`. The venvs' entry points are symlinked into `/usr/local/bin` (not their `python`/`pip`). The final stage adds upstream's runtime extractors from apt, sasquatch from onekey-sec's `.deb`, and Kaleido for `binwalk -E -p` (PNG entropy graphs), found via `KALEIDO_PATH`. Upstream publishes no checksums for sasquatch or Kaleido; their SHA-256s were recorded when pinned, amd64 only. `BINWALK_RM_EXTRACTION_SYMLINK=1` avoids dangling symlinks to container paths in extraction output. esptool needs the serial device passed into the container (`--device`) to talk to hardware; that isn't set up.
- **MCP transport.** Claude Desktop starts local MCP servers as stdio subprocesses. GhidraMCP runs in the same container, so `ghidra-mcp` is a plain stdio entry in `claude_desktop_config.json` that reaches Ghidra on `127.0.0.1`. For future servers in other containers, pick one approach and apply it consistently:
- *Preferred:* run them with an HTTP/SSE transport in their own containers and bridge them into Claude Desktop's stdio config with a small proxy (e.g. `mcp-remote` or `mcp-proxy`) installed in the Claude Desktop image.
- *Alternative:* install the client side inside the Claude Desktop image and point it at the other container over the network.
+182 -16
View File
@@ -1,3 +1,128 @@
# Ghidra from the official GitHub release, checked against the SHA-256 in the
# release notes: https://github.com/NationalSecurityAgency/ghidra/releases
# 12.1.4 because that is the version the GhidraMCP release below is built
# for; Ghidra only loads extensions built for its exact version.
ARG GHIDRA_VERSION=12.1.4
ARG GHIDRA_BUILD_DATE=20260921
ARG GHIDRA_SHA256=ddac49f903da9d5bac833e5cc79395098b9c33cfd3279be5f31bd00387d2d4db
# --- Stage: Ghidra distribution ----------------------------------------------
# Downloaded once, used by the final image and to build extensions against.
FROM ubuntu:24.04 AS ghidra-dist
ARG GHIDRA_VERSION
ARG GHIDRA_BUILD_DATE
ARG GHIDRA_SHA256
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl unzip \
&& rm -rf /var/lib/apt/lists/* \
&& curl -fsSLo /tmp/ghidra.zip \
"https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${GHIDRA_VERSION}_build/ghidra_${GHIDRA_VERSION}_PUBLIC_${GHIDRA_BUILD_DATE}.zip" \
&& echo "${GHIDRA_SHA256} /tmp/ghidra.zip" | sha256sum -c - \
&& unzip -q /tmp/ghidra.zip -d /opt \
&& rm /tmp/ghidra.zip
# --- Build stage: ESP32 Flash Loader for Ghidra --------------------------------
# https://github.com/dynacylabs/ghidra-esp32-flash-loader: loads ESP32 flash
# dumps / app images (partition table, segments at their addresses, chip
# ROM, peripherals from Espressif's SVD files). Its tags ship no binaries,
# so it's built against this exact Ghidra. Pinned to a master commit (Ghidra
# 12 API, newer than the v12.0 tag) and its submodules' commits. Gradle is
# downloaded directly, checked against the SHA-256 gradle.org publishes,
# because Ghidra's own wrapper doesn't verify its download.
FROM ubuntu:24.04 AS esp32-loader-build
ARG DEBIAN_FRONTEND=noninteractive
ARG GHIDRA_VERSION
ARG ESP32_LOADER_COMMIT=59d88034d07010fb3a3601053554baf02e7be2e4
ARG ESP32_SVD_COMMIT=be20aa12560889d6125d144cdb48cf615ac17628
ARG GHIDRA_SVD_LOADER_COMMIT=8fb5601c3444111159cf1e056cbe9d8457985237
ARG GRADLE_VERSION=9.7.1
ARG GRADLE_SHA256=acd53f1edaf02f1a8ff99879f8a34b302661a057d9b063ae9e35b552f804d20a
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl git unzip openjdk-21-jdk-headless \
&& rm -rf /var/lib/apt/lists/*
COPY --from=ghidra-dist /opt/ghidra_${GHIDRA_VERSION}_PUBLIC /opt/ghidra
RUN set -e \
&& fetch() { git init -q "$2"; git -C "$2" fetch -q --depth 1 "$1" "$3"; git -C "$2" checkout -q FETCH_HEAD; } \
&& fetch https://github.com/dynacylabs/ghidra-esp32-flash-loader.git /src/esp32 "${ESP32_LOADER_COMMIT}" \
&& rmdir /src/esp32/data/svd /src/esp32/ghidra_scripts \
&& fetch https://github.com/espressif/svd.git /src/esp32/data/svd "${ESP32_SVD_COMMIT}" \
&& fetch https://github.com/dynacylabs/ghidra-svd-loader.git /src/esp32/ghidra_scripts "${GHIDRA_SVD_LOADER_COMMIT}" \
&& curl -fsSLo /tmp/gradle.zip "https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip" \
&& echo "${GRADLE_SHA256} /tmp/gradle.zip" | sha256sum -c - \
&& unzip -q /tmp/gradle.zip -d /opt \
&& cd /src/esp32 \
&& GHIDRA_INSTALL_DIR=/opt/ghidra "/opt/gradle-${GRADLE_VERSION}/bin/gradle" --no-daemon -q buildExtension \
&& unzip -q dist/ghidra_${GHIDRA_VERSION}_PUBLIC_*_esp32.zip -d /out \
&& grep -qx "version=${GHIDRA_VERSION}" /out/esp32/extension.properties
# --- Build stage: binwalk and its helpers -------------------------------------
# Based on binwalk's own Dockerfile (https://github.com/ReFirmLabs/binwalk,
# Dockerfile at the pinned commit), adapted to Ubuntu 24.04 and with every
# source pinned. Only the results are copied into the final image, so
# compilers and the Rust toolchain stay out of it.
FROM ubuntu:24.04 AS binwalk-build
ARG DEBIAN_FRONTEND=noninteractive
# binwalk: last tag (v3.1.0) is from 2024; master is 3.1.1 with later fixes.
ARG BINWALK_COMMIT=26713972e3f9f52dc37d4a421c4554b0bd9e82ef
ARG RUST_VERSION=1.99.1
ARG DUMPIFS_COMMIT=b7bac90e8312eca2796f2003a52791899eb8dcd9
ARG LZFSE_COMMIT=e634ca58b4821d9f3d560cdc6df5dec02ffc93fd
ARG DMG2IMG_COMMIT=a3e413489ccdd05431401357bf21690536425012
ARG VMLINUX_TO_ELF_COMMIT=36da74ded9cd486f294c8852e0dcab9ef6a48c6b
ARG ESPTOOL_VERSION=5.6.0
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates curl git build-essential clang pkg-config \
zlib1g-dev liblzma-dev liblzo2-dev libucl-dev liblz4-dev libbz2-dev \
libssl-dev libsrecord-dev libfontconfig1-dev \
python3 python3-venv python3-dev \
&& rm -rf /var/lib/apt/lists/*
# Shallow-fetch one exact commit of a git repository.
RUN printf '%s\n' '#!/bin/sh' 'set -e' \
'git init -q "$2"' \
'git -C "$2" fetch -q --depth 1 "$1" "$3"' \
'git -C "$2" checkout -q FETCH_HEAD' > /usr/local/bin/fetch-commit \
&& chmod +x /usr/local/bin/fetch-commit
# binwalk itself, with a pinned Rust toolchain and the locked crate versions.
RUN fetch-commit https://github.com/ReFirmLabs/binwalk.git /src/binwalk "${BINWALK_COMMIT}" \
&& curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain "${RUST_VERSION}" \
&& . /root/.cargo/env \
&& cargo build --release --locked --manifest-path /src/binwalk/Cargo.toml \
&& install -D -m 755 /src/binwalk/target/release/binwalk /out/bin/binwalk
# Extractors binwalk calls that Ubuntu doesn't package (upstream src.sh).
# dumpifs ships a prebuilt binary in its repo; it's removed and rebuilt.
RUN fetch-commit https://github.com/askac/dumpifs.git /src/dumpifs "${DUMPIFS_COMMIT}" \
&& rm /src/dumpifs/dumpifs \
&& make -C /src/dumpifs dumpifs \
&& fetch-commit https://github.com/lzfse/lzfse.git /src/lzfse "${LZFSE_COMMIT}" \
&& make -C /src/lzfse install \
&& fetch-commit https://github.com/Lekensteyn/dmg2img.git /src/dmg2img "${DMG2IMG_COMMIT}" \
&& make -C /src/dmg2img dmg2img vfdecrypt HAVE_LZFSE=1 \
&& install -m 755 /src/dumpifs/dumpifs /usr/local/bin/lzfse \
/src/dmg2img/dmg2img /src/dmg2img/vfdecrypt /out/bin/
# Python extractors binwalk calls (upstream requirements.txt, versions
# pinned), and esptool. Each gets its own venv at its final path; the final
# image uses the same base and Python, so the venvs can be copied as-is.
RUN python3 -m venv /opt/binwalk-py \
&& CC=clang /opt/binwalk-py/bin/pip install --no-cache-dir --disable-pip-version-check \
uefi_firmware==1.16 jefferson==0.4.8 ubi-reader==0.8.16 \
lz4==4.4.5 zstandard==0.25.0 \
"vmlinux-to-elf @ git+https://github.com/marin-m/vmlinux-to-elf@${VMLINUX_TO_ELF_COMMIT}" \
&& python3 -m venv /opt/esptool \
&& /opt/esptool/bin/pip install --no-cache-dir --disable-pip-version-check \
"esptool==${ESPTOOL_VERSION}"
# --- Final image -------------------------------------------------------------
FROM ubuntu:24.04
ARG DEBIAN_FRONTEND=noninteractive
@@ -33,24 +158,15 @@ RUN apt-get update \
fonts-dejavu-core \
&& rm -rf /var/lib/apt/lists/*
# Ghidra from the official GitHub release, checked against the SHA-256 in the
# release notes: https://github.com/NationalSecurityAgency/ghidra/releases
# 12.1.4 because that is the version the GhidraMCP release below is built
# for; Ghidra only loads extensions built for its exact version.
# Ghidra 12.1.4 needs JDK 21 (application.java.min=21). The full JDK, not the
# headless one, because Ghidra's GUI needs the AWT/X11 libraries.
ARG GHIDRA_VERSION=12.1.4
ARG GHIDRA_BUILD_DATE=20260921
ARG GHIDRA_SHA256=ddac49f903da9d5bac833e5cc79395098b9c33cfd3279be5f31bd00387d2d4db
# Ghidra (downloaded and verified in the ghidra-dist stage). Ghidra 12.1.4
# needs JDK 21 (application.java.min=21). The full JDK, not the headless one,
# because Ghidra's GUI needs the AWT/X11 libraries.
ARG GHIDRA_VERSION
RUN apt-get update \
&& apt-get install -y --no-install-recommends openjdk-21-jdk unzip \
&& rm -rf /var/lib/apt/lists/* \
&& curl -fsSLo /tmp/ghidra.zip \
"https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${GHIDRA_VERSION}_build/ghidra_${GHIDRA_VERSION}_PUBLIC_${GHIDRA_BUILD_DATE}.zip" \
&& echo "${GHIDRA_SHA256} /tmp/ghidra.zip" | sha256sum -c - \
&& unzip -q /tmp/ghidra.zip -d /opt \
&& rm /tmp/ghidra.zip \
&& ln -s "/opt/ghidra_${GHIDRA_VERSION}_PUBLIC" /opt/ghidra \
&& rm -rf /var/lib/apt/lists/*
COPY --from=ghidra-dist /opt/ghidra_${GHIDRA_VERSION}_PUBLIC /opt/ghidra_${GHIDRA_VERSION}_PUBLIC
RUN ln -s "/opt/ghidra_${GHIDRA_VERSION}_PUBLIC" /opt/ghidra \
&& ln -s /opt/ghidra/ghidraRun /usr/local/bin/ghidra
ENV GHIDRA_INSTALL_DIR=/opt/ghidra
@@ -72,6 +188,11 @@ RUN set -e; cd /tmp \
&& jar uf "$jar_file" defaultTools/CodeBrowser.tool \
&& rm -r /tmp/defaultTools /tmp/codebrowser-developer.xml
# ESP32 Flash Loader (built above), installed like GhidraMCP into Ghidra's
# own Extensions directory. It's a loader, not a plugin, so no tool config is
# needed: it shows up as a format in File > Import File.
COPY --from=esp32-loader-build /out/esp32 /opt/ghidra/Ghidra/Extensions/esp32
# GhidraMCP (https://github.com/LaurieWired/GhidraMCP), official release zip
# checked against the SHA-256 GitHub lists for it. It holds:
# - the Ghidra extension, unpacked into Ghidra's own Extensions directory so
@@ -109,6 +230,51 @@ RUN set -e \
&& /opt/ghidra-mcp/bin/ghidra-mcp --help >/dev/null \
&& rm -rf /tmp/ghidramcp /tmp/ghidramcp.zip /tmp/uv.lock /tmp/constraints.txt /tmp/uvlock-to-constraints.py
# binwalk (built above) with the extraction tools its Dockerfile installs.
# sasquatch (SquashFS variants) comes as a .deb from onekey-sec; the release
# publishes no checksums, so the SHA-256 was taken when it was pinned.
# The Python tools' entry points are linked into /usr/local/bin; the venvs'
# python/pip are not, so the system python3 isn't shadowed.
ARG SASQUATCH_RELEASE=sasquatch-v4.5.1-5
ARG SASQUATCH_SHA256_AMD64=23191cb530bed01e9f00b9a28a74d03db057fb111e13ce5fb383356c1fdf7e6c
# binwalk -E -p (entropy graph as PNG) renders through Kaleido. binwalk's
# build downloads it into the build stage's /root, which the image doesn't
# have, so -p would crash; it's fetched here instead (same release the crate
# uses, no upstream checksum, SHA-256 taken when pinned) and found through
# KALEIDO_PATH at runtime.
ARG KALEIDO_VERSION=0.2.1
ARG KALEIDO_SHA256_AMD64=3eaa3efd41a00db05dd71add7f54ad4e9d4259552e3f7470ac4c1a8594cea214
COPY --from=binwalk-build /out/bin/ /usr/local/bin/
COPY --from=binwalk-build /opt/binwalk-py /opt/binwalk-py
COPY --from=binwalk-build /opt/esptool /opt/esptool
RUN set -e \
&& arch="$(dpkg --print-architecture)" \
&& [ "$arch" = amd64 ] || { echo "sasquatch checksum only pinned for amd64" >&2; exit 1; } \
&& curl -fsSLo /tmp/sasquatch.deb \
"https://github.com/onekey-sec/sasquatch/releases/download/${SASQUATCH_RELEASE}/sasquatch_1.0_${arch}.deb" \
&& echo "${SASQUATCH_SHA256_AMD64} /tmp/sasquatch.deb" | sha256sum -c - \
&& curl -fsSLo /tmp/kaleido.zip \
"https://github.com/plotly/Kaleido/releases/download/v${KALEIDO_VERSION}/kaleido_linux_x64.zip" \
&& echo "${KALEIDO_SHA256_AMD64} /tmp/kaleido.zip" | sha256sum -c - \
&& unzip -q /tmp/kaleido.zip -d /opt/kaleido \
&& rm /tmp/kaleido.zip \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
/tmp/sasquatch.deb \
7zip 7zip-standalone zstd srecord tar unzip sleuthkit cabextract \
lz4 lzop unrar unyaffs cpio device-tree-compiler \
liblzo2-2 libucl1 libfontconfig1 \
&& rm -rf /var/lib/apt/lists/* /tmp/sasquatch.deb \
&& for f in /opt/binwalk-py/bin/* /opt/esptool/bin/*; do \
case "$(basename "$f")" in python*|pip*|activate*|Activate*) continue ;; esac; \
ln -s "$f" /usr/local/bin/; \
done \
&& binwalk --version && esptool version
# Binwalk leaves a symlink to the input file in its extraction directory. The
# target is a container path, which is dangling when viewed from the host.
ENV BINWALK_RM_EXTRACTION_SYMLINK=1 \
KALEIDO_PATH=/opt/kaleido
# Ubuntu 24.04 ships a default "ubuntu" user on UID 1000; replace it.
RUN if id ubuntu >/dev/null 2>&1; then userdel -r ubuntu; fi \
&& groupadd -g "${USER_GID}" "${USER_NAME}" \