Recursively find every extraction archive and disk image in a directory and run a LEAPP tool (iLEAPP, ALEAPP, RLEAPP, VLEAPP or DLEAPP) on each one, producing a folder full of ready-to-review report directories plus a single master index.html that links them all.
Point it at a directory of extractions, walk away, and come back to a review-ready set of LEAPP reports.
📦 Download the latest release — prebuilt macOS & Windows apps, no Python required.
- Batch LEAPP: process all your test images at once — what it is, release regression testing, and the
--coveragemode. - Reading the coverage database in LAVA — what the reports mean and the analyses to run with them.
Prebuilt apps — no Python required:
| Platform | GUI | CLI |
|---|---|---|
| 🍎 macOS (Apple Silicon) | Batch LEAPP.app | batch-leapp |
| 🪟 Windows (x64) | Batch LEAPP.exe + CLI | — |
First launch is unsigned — see first-run notes. Prefer to run from source or build your own? See Usage and Building standalone binaries.
- Recursively finds every extraction archive (
.zip,.tar,.tar.gz/.tgz,.tar.xz, gzipped-tar.gz) and every disk image or acquisition (.E01,.s01,.Ex01,.dd,.raw,.001,.aff,.afm,.aff4,.dmg,.sparseimage,.vhd,.vhdx,.vmdk,.qcow,.qcow2,.L01,.ad1) under the input directory, and auto-selects the matching-ttype per file. See Disk images for the details. It deliberately ignores prior LEAPP output folders (*LEAPP_Output_*, the older*LEAPP_Reports_*, and any folder holding a LEAPP run's own files) and lone non-tar.gzfiles (browser-cache blobs, single logs) so it doesn't mistake report artifacts for extractions. - Runs your chosen LEAPP tool on each archive or image into its own output folder, so reports never overwrite each other.
- Leaves your LEAPP history alone. Every run gets a private settings folder, so a batch does not fill the recent paths and recent runs of the LEAPP tools.
- Hashes every input (SHA-256) and writes a manifest (
manifest.csv+manifest.json) documenting what was processed — chain-of-custody for your case file. - Pre-checks each archive and flags a corrupt/mislabeled one as
invalidinstead of letting the LEAPP run crash. - Writes a master
index.htmlat the root of the output directory with one row per extraction, linking the report folder, the LEAPPindex.html, and the_lava_data.lavafile — plus the input's SHA-256. - Adds a Source dir column showing the top-level directory each archive came from (handy for grouping by case).
- Keeps going on failure — one bad archive won't stop the batch — and prints an ok/failed/invalid/skipped summary at the end.
- Optionally runs multiple LEAPP processes in parallel, and can pass extra arguments straight through to the tool.
iLEAPP, ALEAPP, RLEAPP, VLEAPP and DLEAPP all share the same command line, python <x>leapp.py -t <type> -i <input> -o <out>, so the same script drives any of them. The tool is auto-detected from the script filename you pass to --leapp and used for labels, the per-job log filename, and the index title.
- Python 3.8+ (standard library only — nothing to
pip install). - A working LEAPP tool. You point
--leappat either:- a script:
ileapp.py,aleapp.py,rleapp.py,vleapp.py,dleapp.py, or - a LEAPP release: the macOS
.app, the Windowsileapp.exe, or the Linux AppImage. From iLEAPP v2026.4.3 and v2026.4.2 of the other four, a release is one program that opens its window when started without arguments and is the command line when given them, so batch-leapp can drive it directly. - Older releases shipped a separate window-only build (
ileappGUI), which takes no batch arguments. batch-leapp refuses a tool whose name containsgui.
- a script:
- If a
.pytool lives in its own virtual environment, point--pythonat that environment's interpreter (ignored for binaries). - The optional GUI uses Tkinter (bundled with most Python installs; on some Linux distros:
sudo apt install python3-tk).
python batch_leapp_gui.pyPick the input dir, output dir, and LEAPP tool, set parallel jobs, and click Run — a live log streams in the window, with Stop, Open report index, and Open output folder buttons. It tries to auto-detect an installed LEAPP tool to prefill the field.
Remembers your paths. Each field has a Recent ▾ button listing the input dirs, output dirs, and LEAPP tools you have used before, and the GUI prefills all three with your last-used values on launch. History is saved per-user (macOS ~/Library/Application Support/BatchLEAPP/, Windows %APPDATA%\BatchLEAPP\, Linux ~/.config/BatchLEAPP/); each menu has a Clear recent entry.
Prefer a double-clickable app with no terminal? See Building standalone binaries below.
python batch_leapp.py INPUT_DIR OUTPUT_DIR --leapp /path/to/<x>leapp.pyINPUT_DIR: directory searched recursively for extraction archives and disk images.OUTPUT_DIR: where the per-input report folders and the masterindex.htmlare written (created if it doesn't exist).
iLEAPP:
python batch_leapp.py /Volumes/Cases/ios /Volumes/Cases/ios_reports \
--leapp ~/tools/iLEAPP/ileapp.pyALEAPP:
python batch_leapp.py /Volumes/Cases/android /Volumes/Cases/android_reports \
--leapp ~/tools/ALEAPP/aleapp.pyCompiled binary / macOS .app (no Python needed for the tool itself):
python batch_leapp.py /Volumes/Cases/ios /Volumes/Cases/ios_reports \
--leapp /Applications/iLEAPP.appOpen the result:
open /Volumes/Cases/ios_reports/index.html| Option | Default | Description |
|---|---|---|
--leapp PATH |
ileapp.py |
Path to the LEAPP script (ileapp.py …) or compiled binary / macOS .app. --ileapp is accepted as an alias. |
--python PATH |
current interpreter | Python used to run a .py tool (point at the tool's venv if it has one). Ignored for binaries. |
-t, --type TYPE |
auto |
auto picks the -t value (zip/tar/gz/raw) per file from its extension. Any other value forces that type for every archive. raw runs only the disk images, and also takes .img and .bin files. |
-j, --jobs N |
1 |
Number of LEAPP runs to execute in parallel. |
--heartbeat SECONDS |
30 |
In parallel mode, print a "still running" line every N seconds so long runs don't look hung (0 disables). |
--timeout SECONDS |
none | Per-input timeout; a run exceeding it is marked failed and the batch continues. |
--no-hash |
off | Skip computing the SHA-256 of each input archive (hashing is on by default). |
--skip-existing |
off | Skip an input whose output folder already exists and is non-empty (resume a partial run). |
--dry-run |
off | Print the exact commands without running the tool. |
--coverage |
off | Developer mode. Enable the LEAPP App Inventory artifacts on each run and aggregate every report into batch_apps.sqlite (see Coverage mode). |
-- <args> |
none | Everything after a literal -- is appended verbatim to every LEAPP run (e.g. -- -m /path/to/case.ilprofile for an iLEAPP profile). |
Passing extra args: batch-leapp owns
-o(it gives each extraction its own output folder), so don't pass your own-othrough--— it would send every report to the same place. To rename the report folder use iLEAPP's--custom_output_folderinstead (e.g.-- --custom_output_folder MyReports); it renames the folder inside each per-zip output dir, so there's no collision and the index/manifest still find every report.
--coverage answers the question “which installed apps are we not parsing?”
across a whole batch of test images. It is meant for LEAPP developers, not for
casework.
python3 batch_leapp.py ~/test_images ~/reports --leapp ~/iLEAPP/ileapp.py --coverageWhat it does:
- Enables the App Inventory artifacts (
scripts/alternate_artifacts/appInventory.pyin the iLEAPP/ALEAPP source checkouts) for every run, via each tool's--custom_artifacts_pathoption. For ALEAPP checkouts that predate that option the module is staged intoscripts/artifacts/for the duration of the batch and removed afterwards. Normal (non-coverage) runs never see these artifacts. - Aggregates every report into
batch_apps.sqliteat the output root: per-extraction device identifiers (extractions), installed apps (installed_apps), every file mapped to its owning app (app_files), and which files each LEAPP module matched (artifact_files). - Computes coverage views:
v_apps_not_parsed— apps with data on disk that no app-specific module touched, per extractionv_apps_not_parsed_rollup— the same across all extractions, rankedv_unknown_containers— iOS app containers whose owner app is unknownv_module_app_spread— classifies modules as generic when they match files for 10+ different apps (e.g.userDefaults); generic matches don't make an app count as parsed
Re-aggregate an existing batch output (no re-run needed) with:
python3 batch_coverage.py ~/reportsThe aggregate also records per-artifact results (artifact_results),
installed-app versions (app_versions) and artifact run errors
(artifact_errors).
To fill an artifact's sample_data from a test image, use the LEAPP repos' own
admin/scripts/validate_sample_data.py --emit, which works from a run of the
artifact itself. The updater that used to ship here has been removed.
Notes: requires LEAPP source checkouts that contain the App Inventory module (compiled binaries don't bundle it); RLEAPP/VLEAPP batches aggregate run metadata but have no app inventory by design.
You can package batch-leapp into native binaries that don't need Python installed — a double-clickable GUI app and a single-file CLI binary — using PyInstaller. Build scripts and icons are included.
Cross-compiling isn't supported: build the macOS binaries on a Mac and the Windows binaries on Windows. Each build also targets the machine's own CPU architecture; the bundled scripts and CI target Apple Silicon on macOS.
One-time setup on each machine:
python3 -m pip install pyinstaller./build_macos.sh # both: dist/Batch LEAPP.app + dist/batch-leapp
./build_macos.sh gui # just the .app
./build_macos.sh cli # just the CLI binaryDrag Batch LEAPP.app to /Applications. To share it, zip with ditto (preserves the bundle):
ditto -c -k --sequesterRsrc --keepParent "dist/Batch LEAPP.app" Batch-LEAPP-macos.zipbuild_windows.bat :: both: dist\Batch LEAPP.exe + dist\batch-leapp.exe
build_windows.bat gui :: just the GUI .exe
build_windows.bat cli :: just the CLI .exeThe binaries are unsigned (notarizing/signing needs a paid Apple Developer ID or a Windows code-signing certificate). They run fine on the machine that built them; when shared:
- macOS — Gatekeeper says "Apple cannot check it for malicious software." Right-click → Open once, or run
xattr -dr com.apple.quarantine "Batch LEAPP.app". - Windows — SmartScreen shows "Windows protected your PC." Click More info → Run anyway.
The quarantine flag. Anything downloaded from the internet — a GitHub Actions artifact or Release asset, a browser download, email, or AirDrop — gets macOS's
com.apple.quarantineextended attribute. That attribute is what triggers the Gatekeeper prompt above; it is not set on binaries you built locally, which is why those open without complaint. To clear it for a downloaded build, right-click → Open once, or strip it from the Terminal:xattr -dr com.apple.quarantine "Batch LEAPP.app" # or: xattr -d com.apple.quarantine batch-leappWindows has an equivalent "Mark of the Web" — right-click the file → Properties → tick Unblock, or just More info → Run anyway at the SmartScreen prompt.
If you have signing certificates, add the codesign/signtool step to the build scripts and I can wire it in.
.github/workflows/build.yml builds everything in the cloud — macOS (Apple Silicon) and Windows, GUI and CLI — so you don't need a Windows machine. Two ways to run it:
- On demand: GitHub → Actions tab → Build binaries → Run workflow. The binaries appear as downloadable artifacts on the run page.
- On a release: push a version tag and the binaries are attached to a GitHub Release automatically:
git tag v1.0.0 && git push origin v1.0.0
Artifacts produced: Batch-LEAPP-macos-arm64.zip (the .app), batch-leapp-macos-arm64.tar.gz (the CLI), and Batch-LEAPP-windows-x64.zip (GUI + CLI .exe). They're still unsigned — the same first-run steps apply. Apple Silicon covers every Mac since 2020; Intel macOS builds were dropped.
A file with one of the image extensions above is handed to the tool with -t raw, which reads it in place without mounting. This needs a LEAPP release that takes -t raw (v2026.4.0 or later); an older tool rejects the argument and the run is marked failed.
- Multi-file images are run once. A segmented set is found by its first file (
.E01not.E02,.001not.002,.L01,.ad1), a split.vmdkby its descriptor, an.afmin place of its numbered data files, and an AFD folder (a folder named*.afd) by one of its.afffiles. Keep the rest of the set beside it; the tool joins it. - The SHA-256 in the manifest covers the file that was handed to the tool, so for a multi-file image it is the hash of the first file, not of the whole set.
.imgand.binare not picked up onauto, because plenty of files with those names are not disk images. Run the folder again with-t rawto process them; that run takes only the disk images..sparsebundlefolders and striped.aff4sets are not handled: a sparsebundle is a folder, so run it through the tool directly, and each.aff4file is queued on its own.- Encrypted images need their key passed through, e.g.
-- --image_password_file /path/to/passwords.txt. batch-leapp gives the tool no terminal to prompt on, so a locked image is reported by the tool rather than stopping the batch to wait for a password. - Split archives (
backup.zip.001) are not disk images and are skipped.
Always sanity-check the list of inputs and the commands before turning it loose on a case load:
python batch_leapp.py INPUT_DIR OUTPUT_DIR --leapp /path/to/<x>leapp.py --dry-runpython batch_leapp.py INPUT_DIR OUTPUT_DIR --leapp /path/to/<x>leapp.py -j 4- Output dirs are assigned before any run starts, so names never collide.
- In parallel mode each run's output is captured to
<tool>_run.log(e.g.aleapp_run.log) inside that extraction's folder, so concurrent runs don't garble the terminal; the screen shows justOK/FAILED/TIMEOUTper zip as they finish. - With
-j 1(the default), the tool's output streams live as usual.
LEAPP tools keep one shared history/settings file (e.g. macOS ~/Library/Application Support/LEAPP/history.json). When history is turned on, every command-line run adds its input path, its output path and a recent-run entry to it, in lists that hold 10 paths and 20 runs. A batch of a dozen images would push out everything you had there, and tools running at once would all be rewriting the same file.
So every run, sequential or parallel, gets a private config dir at <output>/<input>/.leapp_home/, set via HOME / APPDATA / XDG_CONFIG_HOME. Consequences:
- Your real, user-level LEAPP history is left untouched, and batch runs are not recorded in it (an empty private config dir means history recording is off for those runs).
- Concurrent runs never touch the same history file.
- A tool run from source still finds packages installed in your user site-packages: batch-leapp passes the real location along as
PYTHONUSERBASE.
Caution: LEAPP runs are CPU-, disk-, and RAM-heavy. On a typical workstation
-j 2–-j 4is a sane range. Pushing to your full core count can thrash disk I/O and run slower — and large extractions can exhaust memory. Start conservative.
Every run prints a banner (detected tool, zip count, output dir, start time) and a running [done/total] progress counter so you always know how far along the batch is.
Sequential (-j 1) — the tool's own output streams live, framed by counter lines:
[1/4] running: caseA/a.zip
... live iLEAPP/ALEAPP output ...
[1/4] OK caseA/a.zip (37s)
[2/4] running: caseB/b.zip
Parallel (-j > 1) — verbose output goes to the per-job log, so the screen instead shows a START line when each worker picks up a zip, a periodic heartbeat of what's still running (so long jobs never look hung), and a counter line as each completes (the counter climbs 1 → total in completion order):
Running 4 job(s) with 2 worker(s)...
START caseA/a.zip
START caseB/b.zip
... 2 running [0/4 done]: caseA/a.zip (30s), caseB/b.zip (30s)
[1/4] OK caseA/a.zip (37s)
START caseC/c.zip
... 2 running [1/4 done]: caseB/b.zip (60s), caseC/c.zip (23s)
[2/4] OK caseB/b.zip (66s)
...
The heartbeat interval is --heartbeat SECONDS (default 30; 0 disables it). It lists up to four in-flight zips with their elapsed times, plus +N more if more are running.
It ends with the master-index path and a summary block — counts plus start, finish, and elapsed time (and a list of any failures):
Done. 12 ok, 1 failed, 0 skipped.
Started: 2026-06-27 09:14:02
Finished: 2026-06-27 11:48:37
Elapsed: 2h 34m 35s
The same start/finish/elapsed times also appear in the master index header.
batch-leapp searches INPUT_DIR recursively, so you can arrange it however suits the case. The cleanest layout is one folder per case or device, holding just that extraction's archive:
Cases/ ← point INPUT_DIR here
├── 2024-001_iPhone15/
│ └── extraction.zip
├── 2024-002_Pixel8/
│ └── fullfs.tar.gz
└── 2024-003_iPadAir/
└── backup.zip
The top-level folder name (e.g. 2024-001_iPhone15) becomes the Source dir column in the report index and manifest, so name it however you want runs grouped and identified.
A couple of guidelines:
- Keep the output directory outside the input tree. Point
OUTPUT_DIRsomewhere separate (or a sibling) so reports never land back inside what you're scanning. - Don't mix prior LEAPP reports into the input. batch-leapp already ignores
*LEAPP_Reports_*folders, the output dir, and lone non-tar.gzfiles, but an input directory of just extractions is the safest setup and makes the--dry-runpreview easy to sanity-check. - A flat folder of archives works too — they'll all share
INPUT_DIR's name as their Source dir.
OUTPUT_DIR/
├── index.html ← master index (open this)
├── manifest.csv ← run manifest: hash, status, paths, times
├── manifest.json ← same, machine-readable, + run metadata
├── caseA_phone/ ← one folder per archive
│ ├── ileapp_run.log ← captured tool output (parallel mode)
│ ├── .leapp_home/ ← private config dir (parallel mode only)
│ └── iLEAPP_Reports_<timestamp>/
│ ├── index.html ← the LEAPP report
│ └── _lava_data.lava ← the LAVA file
├── caseB_tablet/
│ └── ...
└── ...
(The report subfolder is named by the tool — iLEAPP_Reports_*, ALEAPP_Reports_*, etc. — and the log file is named after the detected tool.)
Per-zip folders are named from each zip's path relative to INPUT_DIR (e.g. caseA/sub/phone.zip → caseA_sub_phone), so two same-named zips in different subfolders never collide. A _N suffix is appended if a name still clashes.
index.html is one table with these columns:
Every run writes two manifests at the root of OUTPUT_DIR:
manifest.csv— one row per extraction: source dir, archive path, SHA-256, status,-ttype, elapsed seconds, output folder, and the relative paths to the report and.lava.manifest.json— the same rows plus run metadata (tool, input/output dirs, start/finish, elapsed, counts, hash algorithm).
Hashing is on by default (stream-read, so it handles huge full-file-system extractions without exhausting RAM). Disable it with --no-hash if you only want the reports.
The page is styled to match leapps.org — dark gold-on-black theme, Barlow fonts, the LEAPPs logo, and a per-tool accent color (iLEAPP red, ALEAPP green, RLEAPP blue, VLEAPP purple) on the title. A summary of ok/failed/skipped counts sits in the header. The logo and favicon are embedded in the HTML (base64), so the page needs no external image files; the web fonts load from Google Fonts when online and fall back to system fonts offline.
Links open in a new tab so the index stays put for the next click.
This mirrors what the LEAPP GUIs do (leapp_functions/lava_launcher.py). When the index is generated, batch-leapp checks whether the LAVA desktop app is installed on the machine, using the same detection iLEAPP uses (open -Ra LAVA on macOS; the LAVA executable / known install paths on Windows and Linux):
- LAVA installed → the button (LAVA) opens a small in-page dialog with a one-click Open in LAVA button that fires the
lava://open?path=…handler, plus the project's full filesystem path and a Copy button as a fallback (paste into LAVA's File ▸ Open, double-click the.lavain your file manager, oropen "<path>"on macOS/Linux). - LAVA not installed → the button becomes Get LAVA; the dialog shows the project path and a link to the download page,
https://www.leapps.org/#lava.
The
lava://handler. LAVA registers alava://open?path=<url-encoded-abs-path>URL scheme, so the dialog's Open in LAVA button hands the project straight to the app — the true one-click path. It requires a LAVA build that supports the scheme; on older builds the button does nothing, which is why the dialog keeps the copyable path and manual-open steps as a fallback. The absolute path is resolved client-side fromwindow.location, so it works when the index is opened from disk (file://), the normal review workflow; served overhttpthere's no absolute path, so the dialog shows the manual steps instead. A plain<a>link couldn't do this on its own: without the scheme a browser would just download the.lava.
All links are relative, so the whole OUTPUT_DIR is portable — zip it, move it, or drop it on a share and the links still resolve. The report and LAVA files are located by scanning each output folder, so they're found wherever the tool writes them. If a tool doesn't emit a .lava file, that cell simply shows —.
- Exit code is
1if any archive failed or was invalid, otherwise0— convenient for scripting. - Stop / closing the GUI terminates running jobs. LEAPP children are launched so they can be killed — clicking Stop, or closing the window (you'll be asked to confirm), terminates the in-flight LEAPP processes instead of leaving them running as orphans.
- Ctrl-C stops launching new work and still writes the master index and manifest for whatever finished.
- Rows for failed, invalid, or skipped extractions still appear in the index and manifest; report/LAVA links show only when those files actually exist.
- Corrupt or mislabeled archives are flagged, not fatal. Each archive is integrity-checked (zip central directory / tar header) before the LEAPP run; a bad one is marked
invalidand the batch moves on. - macOS AppleDouble files are ignored. On non-HFS volumes (exFAT, NTFS, SMB shares) macOS drops a
._name.zipcompanion next to each real file. These are not archives, so they're skipped — otherwise a LEAPP tool would choke on one withBadZipFile: File is not a zip file. - Prior LEAPP reports and stray
.gzfiles don't get re-processed. The scan never descends into*LEAPP_Reports_*folders, skips the output directory, and treats a bare.gzas an extraction only when it's actually a gzipped tar — so the thousands of tiny browser-cache.gzfiles inside an existing report are not picked up as "extractions." - The script is self-contained — copy it anywhere; it doesn't depend on this repository.



