DOCUMENTATION / PART 3 — INTERFACES3.1

Part 3 — Interfaces

Part 2 used the toolkit through its most honest surface: a shell prompt and one module at a time. That surface is the one that always works, and it is the one this part describes first and in the most detail — because the other two are views over it, and a reader who knows the command line can always reconstruct what a button did.

There are four things in this part. §3.1 is the module command line and the launcher that indexes it. §3.2 is the terminal workspace. §3.3 is the window. §3.4 is the gate suite, which is not an interface at all but is the reason the other three can be trusted to say what they say.

A note on the relationship between them. The window does not contain a second implementation of anything: every button is an argv array handed to the same shell script you would have typed. When this part says "the GUI runs --turbo-decode", it means the exact command in §3.1's sense, run as a child process, with its stdout read back into a result pane. That is a deliberate limit and it has a cost — the window cannot do anything the modules cannot do — but it is what makes the whole thing auditable: any result you see on screen can be reproduced in a terminal, and any result in a terminal can be pasted back.

3.1 The command line, and the launcher that indexes it

One script, two lives

Every module is one file under modules/, and every one of them is written to be both a program and a library:

if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
    set -o pipefail
    IMSISTING_CLI=1
    : "${TMP_DIR:=/tmp/imsisting}"
    : "${LOOT_DIR:=$IMSISTING_ROOT/loot}"
    ...
    declare -F banner >/dev/null 2>&1 || banner() { :; }
    declare -F info   >/dev/null 2>&1 || info()   { printf '[info] %s\n' "$*" >&2; }
    declare -F warn   >/dev/null 2>&1 || warn()   { printf '[warn] %s\n' "$*" >&2; }
    declare -F err    >/dev/null 2>&1 || err()    { printf '[err]  %s\n' "$*" >&2; }
    declare -F ok     >/dev/null 2>&1 || ok()     { printf '[ok]   %s\n' "$*" >&2; }
    gatk_cli_main "$@"
fi

That tail is why a gate can do this:

source modules/07_gsm_attack.sh
gatk_selftest          # no dispatch, no launcher, no window

and why the window can build a page out of a module without executing it. The fallback info/warn/err/ok stubs matter more than they look: sourced, the module has no launcher to define them, so each module defines a do-nothing version and the launcher's version, when there is one, wins.

Every function a module defines is prefixed with its own tag — gatk_ in 07_gsm_attack, uair_ in 08_umts_air, et_ in 57_evil_twin, sc_ in 58_suci_catcher. The prefixes exist so that several modules can be loaded into one shell without a collision, which is exactly what the codec gates do.

The preamble

The first twenty-five lines of a module are the same in all sixty-one of them, and each line is load-bearing.

The root comes from the script's own path, following symlinks:

if [[ -z "${IMSISTING_ROOT:-}" ]]; then
    _sl_src="${BASH_SOURCE[0]}"
    while [ -L "$_sl_src" ]; do ... done
    IMSISTING_ROOT="$(cd -P "$(dirname "$_sl_src")/.." && pwd)"
    export IMSISTING_ROOT
fi

A module launched standalone — from a pkexec prompt, from a desktop file, from a cron job — has no launcher to tell it where the tree is, and its store must still land inside that tree rather than in whatever directory happened to be current.

HOME is reset when it does not match the tree's parent, because pkexec runs with HOME=/root while writing to the invoking user's project. Anything $HOME-relative would otherwise resolve into /root and the write would either fail or succeed somewhere nobody looks.

Five directories are named and created:

Variable Default What belongs in it
IMSISTING_ROOT the checkout the tree; every other path is derived from it
TMP_DIR /tmp/imsisting scratch: caches, logs of the window, intermediate files
LOOT_DIR $IMSISTING_ROOT/loot observations that are the answer — stores you keep
CAPTURES_DIR $IMSISTING_ROOT/captures raw captures, before anything decoded them
LOGS_DIR $IMSISTING_ROOT/logs what a long run writes while it runs
STATE_DIR $IMSISTING_ROOT/state what the next launch must remember

The distinction between loot/ and captures/ is the one people get wrong first: a capture is evidence of what crossed the air, a loot store is the toolkit's reading of it. You may delete a loot store and rebuild it from the capture; you cannot rebuild the capture. That is why captures/ is the directory to back up.

All five are environment variables first and defaults second, so a survey can be relocated without touching the code:

LOOT_DIR=/mnt/field/loot CAPTURES_DIR=/mnt/field/captures \
    modules/02_cell_recon.sh --scan lte 20

Verbs, and the usage block

Dispatch is one case on "${1:-}", and the unknown-verb arm is the manual for that module:

        *)
            cat >&2 <<EOF
GSM Catcher — IMSI catcher, A5/1 downgrade, TMSI de-anonymisation, Kc recovery

Usage: $0 COMMAND
  --status-tsv                 radio and plan state as TSV
  --plan <name>                write a handoff plan file and report the hardware
  ...
  --selftest                   verify sequence, classmark, messages and ciphers
  --menu                       interactive menu
EOF
            return 2 ;;

Two consequences follow. Asking a module for help is an error, so modules/07_gsm_attack.sh with no arguments prints usage to stderr and exits 2 — it is not a failure, it is a refusal to guess. And the usage block is the only place a verb is described, which is what lets tools/check-verbs.sh (§3.4) compare the set of documented verbs against the set of things the window offers.

The usage text follows a shape across all sixty-one modules: a title line naming the module and what it is for, Usage:, then groups separated by a blank line and introduced by a short heading, because a flat list of ninety verbs cannot be read. An argument in <angle> is required, one in [brackets] is optional, and an [a|b|c] is a closed set with the default first.

What a verb prints

Machine-readable output goes to stdout, as TSV, always with a header row:

ts  ts_epoch    run step    channel direction   protocol    message_type    message source

That convention is not cosmetic. It is what lets awk -F'\t' '$2 ~ /^GSM Um/' work, what lets the window fill a list without an API, what makes two runs diffable, and what keeps a column from being renamed silently — because a retired column name breaks a gate before it breaks a caller.

Human-facing lines go to stderr, and their first field is the kind:

First field Meaning Exit code that usually follows
ERROR the request cannot be honoured; the rest of the line says why 2
WHY a capability is absent and here is the reason, with the alternative 2
[info] progress on a run that is still going carries on
[warn] something was skipped or assumed carries on
[ok] the step the operator was waiting for finished carries on

The WHY line deserves its own sentence. When a module cannot do something — no radio, no tshark, a driver that will not open — it does not print a generic failure. It prints a WHY line naming the missing piece and the verb that would work instead. A refusal you cannot act on is worse than no tool at all, and the WHY line is the toolkit's answer to that.

Exit codes are the same three throughout, and §1.2 gave them in principle: 0 measured and printed, 1 ran and the answer is negative or nothing was found, 2 refused because the request or the bench cannot be honoured. A pipeline of modules is therefore readable with &&:

modules/02_cell_recon.sh --scan lte 20 \
  && modules/04_tower_map.sh --survey lte 20

The four flags to try first on any module

modules/26_ss7_map.sh                 # no verb: usage on stderr, exit 2
modules/26_ss7_map.sh --selftest      # prove this module's own arithmetic
modules/26_ss7_map.sh --status-tsv    # what it can see right now, as TSV
modules/26_ss7_map.sh --menu          # the same verbs as a numbered menu

--selftest is the one to reach for when a result looks wrong. It asserts the module against published values — 357 rows in 07_gsm_attack, a few dozen in most others — and it runs without a radio, without a capture and without root. If --selftest passes and your result still looks wrong, the module is doing what it claims and the claim is what you disagree with, which is a different conversation; if it fails, stop and read the failing row.

--status-tsv is the honest one. It reports what the module can see now — a radio that answers, a tool that is present, a store with rows in it — and it reports absence as absence rather than as zero. That distinction is why the window's header strip can say SDR none instead of SDR ready.

The launcher

imsisting.sh is an index, not an engine. Its job is to hold one table and to turn a number into a function call:

CATEGORY|KEY|LABEL|MENU_FUNCTION|MODULE_ID

KEY is a sequential position — 01 through 61 — and the table's own header comment says so, which is why the keys are unique and gapless. MENU_FUNCTION names a shell function in the launcher; MODULE_ID names the script it ends up running. Two rows may share a category and no two rows may share a key, because fn_for_key() returns the first match and a duplicate key silently makes the second row unreachable — a bug this tree has had once, and now has a gate against (§3.4).

bash imsisting.sh --list            # every module, its key and its availability
bash imsisting.sh --index           # the module index as TSV
bash imsisting.sh --tui             # the terminal workspace, §3.2
bash imsisting.sh --gui             # the window, §3.3
bash imsisting.sh --version         # imsisting v1.0.86 — 61 modules, 2G/3G/4G/5G
bash imsisting.sh --help            # the above

The interactive browser needs root because the modules it launches may; the read-only verbs do not. That asymmetry is deliberate: looking at a catalogue should never require privilege, and transmitting always should.

Declared settings

The launcher also owns the small set of settings that outlive a session:

bash imsisting.sh --settings list        # declared settings as TSV
bash imsisting.sh --settings set K V     # declare one
bash imsisting.sh --settings unset K     # back to the default
bash imsisting.sh --settings reset       # back to every default

A setting declared here is written to a file the modules read, and a module reports the value it used, so "why did it use that path" has an answer. The window's Settings page (§3.3) edits the same values; the two cannot drift apart because there is only one store.