# vm-lab

Spin up disposable macOS, Windows, and Linux VMs and drive them over SSH for cross-OS debugging and repro. Use to run or inspect an isolated guest.

- **Kind:** skill
- **Source:** https://github.com/forefy/.context
- **Page:** https://forefy.com/skills/ed303658-a31c-4097-94cf-39bc5be8e6c7
- **API (JSON + files):** https://forefy.com/api/asr/ed303658-a31c-4097-94cf-39bc5be8e6c7

---

## .gitignore

```

```

## LEARNINGS.md

# LEARNINGS - environment quirks & recipes discovered in the field

Append here whenever you learn something that isn't obvious from the docs: a tool
that fails in a VM, an install gotcha, a provider quirk. Keep entries dated and
tagged `[os/arch/provider]` so they stay searchable. This file ships with the repo
so the knowledge compounds across sessions and users. **Do not put secrets here**
(IPs/keys/passwords live in the gitignored config.local.env).

Format:
```
## YYYY-MM-DD - short title  [os/arch/provider]
What broke / what works, with the exact command.
```

---

## 2026-08-27 - Electron IPC audit: minification-proof grep patterns  [any/electron]
Auditing an Electron app's app.asar (JS is stored plaintext, grep-able directly):
handlers minify to `<alias>.handle("chan")` so grep `.handle("` (NOT `ipcMain.handle`,
which is aliased). webPreferences flags minify booleans: `contextIsolation:!0`=true,
`:!1`=false. Custom-IPC channel names may be templated with a per-build UUID prefix
(`$eipc_message$_<uuid>_$_<ns>_$_<svc>_$_<method>`) so `.handle("literal")` finds
zero - the surface lives in the preload's exposeInMainWorld + the templated channels.
`tools/asar.py` extracts individual files with no node. `tools/electron-triage.sh`
automates the whole map; `./inspect-app.sh <tag> <app>` chains it after signing/entitlement
triage. Validated on Claude.app (Electron 42, contextIsolation+sandbox on, nodeIntegration off).

## 2026-08-27 - hardened+notarized macOS apps block the Frida path  [macos/arm64]
A Developer-ID app with Hardened Runtime (`flags=0x10000(runtime)`) and no
`disable-library-validation` / `allow-dyld-environment-variables` / `get-task-allow`
entitlement CANNOT be Frida/dylib-injected without SIP off or re-signing. inspect-app.sh
prints this verdict from codesign flags+entitlements. For such targets use static
(asar/IPC map) + observational (eslogger/fs_usage/tcpdump), not injection.

## 2026-08-27 - lib.sh must be zsh-safe (macOS default shell)  [host/any]
`source lib.sh` from an interactive shell runs under **zsh** on macOS, not bash.
Three bashisms broke it: (1) `gp()` indirect expansion `${!v}` → "bad substitution"
(fixed via `eval "printf '%s' \"\${$v-}\""`); (2) `${BASH_SOURCE[0]}` self-path
(fixed with a ZSH_VERSION branch using eval-deferred `${(%):-%x}`); (3) unquoted
`$GUESTS` word-splitting - zsh does NOT split unquoted expansions (fixed: guest_list
emits newlines via tr, guest_exists uses while-read). The bash-shebang scripts
(ctl.sh/verify.sh) were unaffected. Verified: `source lib.sh; rc mac ...; rc win ...`
now works in zsh. Caught by live-testing against the real VMs.

## 2026-08-22 - powermetrics hardware samplers fail in a VM  [macos/arm64/parallels]
`powermetrics --samplers cpu_power|gpu_power` → "cannot find the IO registry entry
for IODeviceTree:/arm-io/pmgr" (VM has no SMC/pmgr). The **tasks sampler works**:
`sudo powermetrics -n 1 --samplers tasks` (per-proc CPU ms/s, wakeups). Likely
applies to VMware Fusion too (same missing hardware).

## 2026-08-22 - wpr can't profile in a VM  [windows/arm64/parallels]
Every `wpr` profile → `0x80070032` ERROR_NOT_SUPPORTED (no PMU/kernel profiling in
the guest). Binary runs (`-status`/`-cancel` fine). Substitutes that work in-VM:
`logman create trace ... -p Microsoft-Windows-Kernel-Process -ets` (real .etl), and
Procmon (behavioral). CPU stack-sampling has no in-VM substitute - needs bare metal
or PMU passthrough. Expect the same on VirtualBox/VMware unless vPMC is enabled.

## 2026-08-22 - dtruss is SIP-blocked  [macos/arm64]
`dtruss /bin/echo` → "Operation not permitted" (SIP). Use `eslogger` + `log stream`
for behavior instead of dtruss/ktrace.

## 2026-08-23 - eslogger block-buffers over SSH  [macos/arm64]
Plain-pipe `eslogger` capture flakes to 0 events (flushes at ~8 KB / clean exit) and
often ignores SIGINT (a `wait` can hang). Fix: force a pty - `sudo script -q out
eslogger exec &`, trigger activity, `pkill -INT eslogger`. Never `wait` on it.

## 2026-08-23 - Frida needs Python 3.10+, injection needs root  [macos/arm64]
Stock CLT Python 3.9 fails frida-tools import. Install python.org 3.12 headlessly,
`pip install frida-tools`. `frida.attach()` needs root (task_for_pid); SIP-on means
only non-Apple/self-signed binaries are hookable. Frida 17:
`Module.getGlobalExportByName(n)` replaced `getExportByName(null,n)`.

## 2026-08-23 - cdb symbol resolution hangs over SSH  [windows/arm64]
`lm` / any symbol lookup hangs cdb via the msdl server under SSH. Set
`$env:_NT_SYMBOL_PATH=''` (or a local cache) first, wrap in `Start-Job` +
`Wait-Job -Timeout` so a stuck session can't hang the SSH call.

## 2026-08-xx - Windows OpenSSH FoD won't fetch on fresh VMs  [windows/arm64]
`Add-WindowsCapability -Online -Name OpenSSH.Server` can fail (0x800f0950 /
0x800f0954) even with internet. Fallback: the Win32-OpenSSH GitHub release
(bootstrap/windows.ps1 auto-picks ARM64 vs Win64). Admin key auth requires
`administrators_authorized_keys` with an ACL granting only Administrators+SYSTEM.

## 2026-08-xx - GUI console mangles synthetic input without guest tools  [any/parallels]
With no guest tools installed, computer-use into the console is unreliable (every
char → `a`; paste/scroll/clipboard-sync ignored; single `key` presses work but not
`/` or uppercase). Prefer: human pastes the one bootstrap line, or use VirtualBox
`prov_type` (VBoxManage keyboardputstring), or Path B unattended install.

## SKILL.md

---
name: vm-lab
description: Spin up disposable macOS, Windows, and Linux VMs and drive them over SSH for cross-OS debugging and repro. Use to run or inspect an isolated guest.
---

# VM Lab - cross-OS debugging on any free hypervisor

Disposable local VMs - real macOS, Windows, and Linux, isolated from this host -
for cross-OS repro, live process/network/file/memory triage, code-signing checks,
and dynamic instrumentation. Works on **Parallels, VirtualBox, or VMware** with
**no paid tooling**.

## The core idea
**SSH is the universal substrate.** The hypervisor only ever does four things -
`list`, `get-ip`, `power`, `snapshot` - and each of the three has a *free* way to do
all four (`providers/*.sh`). Everything valuable (the debug toolkit) runs over SSH
and is hypervisor-agnostic; it only varies by **guest OS** and **CPU arch**. So this
skill is a thin swappable provider layer + a big OS/arch-specific debugging core.

## On every invocation - ask first, then route
This skill serves three OSes and three providers. Before doing anything, establish:
1. **Which guest** - macOS, Windows, or Linux? (drives `toolkits/<os>.md` + SSH gotchas)
2. **Which provider** - Parallels / VirtualBox / VMware? (drives `providers/<name>.sh`)
3. **Set up fresh, or use an existing guest?**
   - *Existing* → confirm it's in `config.local.env`, `./ctl.sh <tag> up`, `./verify.sh <tag>`, go.
   - *Fresh* → `bootstrap/` (Path A one-liner, or Path B unattended), then add to config, verify, **snapshot**.

If a `config.local.env` already defines the guest the task needs, skip the questions
and use it. Ask only what's genuinely unresolved.

## Config & connect
All mutable details live in **`config.local.env`** (copy from `config.example.env`;
it's gitignored so your IPs/keys never get shared). Then:
```bash
source lib.sh                 # loads config + helpers (rc, rc_sudo, ssh_cmd, prov, ctl)
rc mac 'uname -a'             # run on the macOS guest
rc win 'whoami'              # Windows: auto-wrapped as base64 PowerShell
rc lin 'uname -a'            # Linux
rc_sudo mac 'fs_usage -w'    # sudo on mac/linux (echoes the guest pw via -S)
./verify.sh                   # every guest: tools present + callable, with hints
./ctl.sh doctor               # host preflight: which providers/tools are present here
./ctl.sh win up               # boot + WAIT for sshd; then down|ip|ssh|snaps|vms
push mac ./tool /tmp/tool     # copy to guest (scp); pull mac /tmp/x.pcap ./  to fetch
tun  mac -L 8080:127.0.0.1:8080   # port-forward (MITM/reach a guest service)
./ctl.sh mac reset            # restore the clean snapshot (RESET_SNAPSHOT) - the disposable loop
```
Guests are tags (`mac`/`win`/`lin`, your choice) with `{PROVIDER, OS, ARCH, VMNAME,
IP, USER, AUTH, KEY/PW}`. Leave `IP` blank to auto-discover (provider → mDNS → ARP).

**Quoting caveat:** `rc <tag> 'cmd'` single-quotes the command, so an embedded `'`
breaks it. For anything non-trivial (or with quotes) use **`rcs <tag> <script> [args]`**
- it pushes a local script, runs it (bash for mac/linux, `powershell -File` for
windows), and cleans up. Or the heredoc form `rc mac 'bash -s' <<'EOF' … EOF`.
`run_win` (base64) is quote-safe for Windows one-liners.

## Inspecting an app (installed or running)
`./inspect-app.sh <tag> <app>` - one-command triage of any app on a guest:
identity, code signing / notarization, **hardened-runtime + injectability verdict**
(tells you whether the Frida path will even work), entitlements, URL schemes,
process tree, loaded modules, bundled runtimes. It **auto-detects Electron** and
chains `tools/electron-triage.sh`, which maps the renderer↔main attack surface -
`exposeInMainWorld` bridges, `.handle("…")` IPC channels, `webPreferences` security
flags (contextIsolation/sandbox/nodeIntegration), and privileged custom protocols -
all without `node` (a pure-python `tools/asar.py` extracts the asar on the guest).
```bash
./inspect-app.sh mac Claude        # macOS: codesign/entitlements/schemes + electron map
./inspect-app.sh lin firefox       # linux: dpkg/rpm/flatpak + systemd + maps
./inspect-app.sh win Claude        # windows: uninstall-registry + signature + modules
```
Per-OS logic is in `tools/inspect-<os>.{sh,ps1}`; `asar.py` is a reusable
standalone Electron extractor (`python3 tools/asar.py app.asar --list`).

## Fresh guest → SSH (bootstrap)
A pristine guest has no sshd/key, and only VirtualBox can type into the console
headlessly. Two paths (Path B detailed in `bootstrap/unattended.md`):
- **Path A - one-line paste:** paste `bootstrap/<os>.{sh,ps1}` into the guest
  console once (a human is most reliable here; some GUI consoles mangle synthetic
  input), then everything is over SSH.
- **Path B - unattended/seed** (best for VirtualBox/VMware): bake sshd+key in at
  install via cloud-init / autounattend.xml (`bootstrap/unattended.md`).
After it connects: pin/blank the IP in config, `./verify.sh <tag>`, then **snapshot**
so you never bootstrap again. You supply ISOs; `bootstrap/unattended.md` lists
official sources and can seed them.

## Run commands reliably over SSH - per OS
- **macOS** (`toolkits/macos.md`): no `timeout`; bound live captures with
  background+`sleep`+`kill -INT` (never `-9`); `eslogger` needs a pty (`script`) or
  it shows 0 events.
- **Windows** (`toolkits/windows.md`): `run_win` sends PowerShell as UTF-16LE
  base64 and strips CLIXML noise; structured output → write-to-file-then-read;
  `cmd` one-liners are reliable.
- **Linux** (`toolkits/linux.md`): the easy one - one shell, real `timeout`, real
  signals; `rc_sudo` for root.

## Toolkits (task → tool matrix + install recipes)
Each `toolkits/<os>.md` has the full matrix and arch-forked install steps:
- **macOS/arm64** - built-ins first (`ps`, `eslogger`, `lsof`, `nettop`, `tcpdump`,
  `vmmap`, `codesign`, `sample`, `log`); CLT for `otool`/`nm`; optional Frida.
  VM caveat: `powermetrics` needs `--samplers tasks`; `dtruss` SIP-blocked.
- **Windows/arm64|x64** - native cmdlets first; add only `autorunsc`/`handle`/
  `sigcheck`/`procdump`/`Procmon`; optional `cdb`. VM caveat: `wpr` fails
  (0x80070032) → use `logman`/`Procmon`. **Pick ARM64 vs x64 downloads by `ARCH`.**
- **Linux/x64|arm64** - `apt`/`dnf`/`pacman` install everything: `strace`,
  `bpftrace`, `perf`, `gdb`, `ss`, `lsof`, `tcpdump`, `auditd`. VM caveat: `perf`
  hardware counters usually unavailable → software events only.

## Maintainable & self-remembering
When you learn a new environment quirk, install recipe, or gotcha, **append it to
`LEARNINGS.md`** (dated, tagged by OS/arch/provider) - that file travels with the
repo, so the knowledge compounds across sessions and users instead of living in one
machine's memory. Re-run `./verify.sh` anytime to re-prove every tool is callable.

## Notes
- **Safety** - the default `SSH_OPTS` disable host-key checking (right for
  disposable, re-snapshotted VMs; removes MITM protection). Never point this at a
  machine you care about; override `SSH_OPTS` in config for anything non-throwaway.
  `config.local.env` is sourced as shell - only use configs you trust.
- **Disposable** - installing tools, killing processes, editing config, rebooting
  are all fair game; a snapshot restore recovers anything. Take a clean snapshot
  right after bootstrap.
- **No paid CLIs**: Parallels needs only free `prlctl list`; VirtualBox's
  `VBoxManage` is fully free; VMware Fusion/Workstation are free for personal use.
  The provider layer degrades to SSH-based fallbacks when a native verb is gated.

## bootstrap

```

```

## bootstrap/linux.sh

```bash

```

## bootstrap/macos.sh

```bash

```

## bootstrap/unattended.md

# Unattended / seed installs (Path B - no console typing)

The robust way to get a fresh guest to SSH without fighting the GUI console: bake
sshd + your public key into the OS install itself. You supply the ISO; these seed
files do the rest.

## Linux - cloud-init (best)
Most distros ship a cloud image. Provide a `user-data` file on a seed ISO (label
`cidata`) alongside an empty `meta-data`:
```yaml
#cloud-config
users:
  - name: lab
    sudo: ALL=(ALL) NOPASSWD:ALL
    shell: /bin/bash
    ssh_authorized_keys:
      - ssh-ed25519 AAAA...REPLACE_ME... vmlab
ssh_pwauth: false
package_update: true
packages: [openssh-server, strace, gdb, lsof, tcpdump]
```
Build the seed ISO:
```bash
cloud-localds seed.iso user-data meta-data     # or: mkisofs -o seed.iso -V cidata -J -r user-data meta-data
```
- **VirtualBox** has native unattended install that generates this for you:
  `VBoxManage unattended install <vm> --iso=<distro.iso> --user=lab \
    --ssh-key=~/.ssh/vmlab_ed25519.pub --install-additions`.
- **VMware / Parallels**: attach `seed.iso` as a second CD and boot the cloud image.

## Windows - autounattend.xml
Put an `autounattend.xml` on a FAT32 USB/ISO; Setup auto-detects it. Include a
`<FirstLogonCommands>` that runs the same steps as `windows.ps1`:
```xml
<FirstLogonCommands>
  <SynchronousCommand><Order>1</Order>
    <CommandLine>powershell -ExecutionPolicy Bypass -Command "Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0; Set-Service sshd -StartupType Automatic; Start-Service sshd"</CommandLine>
  </SynchronousCommand>
  <SynchronousCommand><Order>2</Order>
    <CommandLine>powershell -ExecutionPolicy Bypass -Command "Set-Content $env:ProgramData\ssh\administrators_authorized_keys 'ssh-ed25519 AAAA...REPLACE_ME... vmlab' -Encoding ascii; icacls $env:ProgramData\ssh\administrators_authorized_keys /inheritance:r /grant Administrators:F /grant SYSTEM:F"</CommandLine>
  </SynchronousCommand>
</FirstLogonCommands>
```
Generate the full file with the Windows SIM, or start from the many public
autounattend generators. For Windows 11 ARM64 use the ARM64 ISO.

## macOS
No supported unattended install for guests. Do Path A once (`macos.sh` + Remote
Login toggle), enable auto-login (see toolkits/macos.md), then **snapshot** - that
snapshot is your reusable "already bootstrapped" baseline.

## Where to get ISOs (you supply these)
- **Windows 11** (x64 & ARM64): Microsoft's official ISO / Download Windows 11 page.
- **Linux**: the distro's cloud image (Ubuntu cloud-images, Fedora Cloud, Debian genericcloud).
- **macOS**: `mist list` / `mist download` or `softwareupdate --fetch-full-installer`
  on a Mac, then build the `.ipsw`/installer for Parallels/Fusion.

## bootstrap/windows.ps1

```

```

## config.example.env

```

```

## ctl.sh

```bash

```

## inspect-app.sh

```bash

```

## lib.sh

```bash

```

## providers

```

```

## providers/common.sh

```bash

```

## providers/parallels.sh

```bash

```

## providers/virtualbox.sh

```bash

```

## providers/vmware.sh

```bash

```

## toolkits

```

```

## toolkits/linux.md

# Linux guest toolkit (x64 on AMD/Intel, or arm64 on Apple Silicon)

Linux is the easiest guest: SSH is one `apt`/`dnf` away, package managers install
everything, and (unlike macOS SIP / Windows PMU limits) most tracing works in a VM
- with two caveats noted below. Commands assume `rc lin '<cmd>'` and `rc_sudo lin '<cmd>'`.

## Run-commands-over-SSH gotchas
- Dead simple vs. the other two: one login shell, real `timeout`, real signals.
- Non-interactive sudo: `rc_sudo lin '<cmd>'` (echoes `$PW | sudo -S`), or set up
  `NOPASSWD` for the lab user. Passwordless key auth is the norm here.
- Bound a live capture with `timeout 5 <cmd>` (present on all distros) - no macOS-style dance.

## One-time setup (per distro family)
```bash
# Debian/Ubuntu:
sudo apt-get update && sudo apt-get install -y \
  strace ltrace gdb lsof tcpdump linux-perf bpfcc-tools bpftrace \
  auditd sysstat htop
# Fedora/RHEL:
sudo dnf install -y strace ltrace gdb lsof tcpdump perf bcc-tools bpftrace audit sysstat htop
# Arch:
sudo pacman -S --noconfirm strace ltrace gdb lsof tcpdump perf bcc bpftrace audit sysstat htop
```
`perf` package name is `linux-perf` (Debian) / `perf` (Fedora) and must match the
running kernel version; in a minimal cloud image you may need `linux-tools-$(uname -r)`.

## Task → tool matrix
| Task | Tool |
|------|------|
| Process tree + cmdline | `ps -ef --forest` · `pstree -ap` · `cat /proc/<pid>/cmdline` |
| Live exec/fork events | `bpftrace -e 'tracepoint:sched:sched_process_exec{...}'` · `execsnoop-bpfcc` · auditd `execve` |
| File activity | `opensnoop-bpfcc` · `strace -f -e trace=file -p <pid>` · `fatrace` |
| Socket ↔ process | `ss -tanp` (state+pid) · `lsof -nP -iTCP -sTCP:ESTABLISHED` |
| Byte throughput | `nethogs` (per-proc) · `ss -i` · `/proc/net/dev` |
| Packet capture | `sudo tcpdump -i any -w x.pcap` |
| Loaded libs (live proc) | `cat /proc/<pid>/maps` · `ldd <bin>` · `lsof -p <pid>` |
| Symbols / imports | `nm -D` · `readelf -d` · `objdump -T` |
| Signature / integrity | `debsums` / `rpm -V` · `sha256sum` (no code-signing like mac/win) |
| Persistence / autostart | `systemctl list-unit-files --state=enabled` · `systemd-analyze` · crontab · `~/.config/autostart` |
| File locks / open handles | `lsof <path>` · `fuser -v <path>` |
| RAM/CPU per process | `top`/`htop` · `/proc/<pid>/status` · `pidstat 1` |
| CPU stacks / profiling | `perf record -g -p <pid>` → `perf report` (see VM note) · `pstack`/`gstack` |
| Syscall trace | `strace -f -p <pid>` · `ltrace` (library calls) |
| Kernel/event tracing | `bpftrace` · `ftrace` (`/sys/kernel/tracing`) · `auditctl` |
| Memory dump | `gcore <pid>` · `/proc/<pid>/mem` via gdb |
| Dynamic instrumentation | `gdb -p <pid>` · Frida (`pip install frida-tools`; no SIP, so system bins hookable as root) |

## VM-specific limitations
- **`perf` hardware counters** (cycles, cache-misses, PMU events) are usually not
  virtualized - `perf stat` reports `<not supported>` for hardware events. Software
  events (`task-clock`, `context-switches`, `page-faults`) and `perf record`
  call-graph sampling via software clock (`-e cpu-clock`) still work. Enable guest
  PMU passthrough if your hypervisor supports it (VMware `vpmc`, KVM `-cpu host`).
- **eBPF / bpftrace** need a kernel with BTF (`CONFIG_DEBUG_INFO_BTF`) and, on some
  distros, `sudo`. Ubuntu 20.04+ / Fedora ship it. If `bpftrace` errors on BTF, use
  `strace`/`auditd`/`ftrace` as the portable fallback.
- Running as **root inside the guest** removes the usual ptrace-scope limits; on a
  non-root setup you may need `sudo sysctl kernel.yama.ptrace_scope=0` to attach.

## toolkits/macos.md

# macOS guest toolkit (Apple Silicon, arm64)

macOS guests are only legal/practical on Apple hardware - Parallels or VMware
Fusion on an Apple Silicon Mac. Built-ins cover almost everything; only ESF
reliability, Command Line Tools, and (optional) Frida need setup.

## Run-commands-over-SSH gotchas
- **Multi-line:** `rc mac 'bash -s' <<'EOF' … EOF` (or `eval "$(ssh_cmd mac) 'bash -s'"`).
- **sudo non-interactively:** `rc_sudo mac '<cmd>'` (does `echo $PW | sudo -S`).
- **No `timeout`** on macOS. Bound a live capture by backgrounding it, `sleep`,
  then `kill -INT` + `wait` - **never `kill -9`** (loses buffered output; for ESF
  it also leaks the client slot).

## Task → tool matrix (all base-system unless noted)
| Task | Tool |
|------|------|
| Process tree + cmdline | `ps -axo pid,ppid,user,command` |
| Live exec/fork/file events | `eslogger exec fork open …` (root; see ESF note) |
| File activity | `sudo fs_usage -w -f filesys` |
| Socket ↔ process | `sudo lsof -nP -iTCP -sTCP:ESTABLISHED` |
| Per-flow byte throughput | `nettop -d -x -P -s1` |
| Packet capture | `sudo tcpdump -i en0 -w x.pcap` |
| Loaded images (live proc) | `vmmap <pid>` · `otool -L <bin>` † |
| Symbols / imports | `nm` · `otool -tV` † · `dyld_info` † |
| Code signing / trust | `codesign -dv --verbose=4` · `spctl -a -vv` |
| Persistence / autostart | launchd (`~/Library/LaunchAgents`, `/Library/Launch*`), `sfltool dumpbtm`, login items |
| File locks / open handles | `sudo lsof <path>` |
| RAM/CPU per process | `top -l1 -stats pid,command,cpu,mem` · `footprint <pid>` · `vm_stat` |
| CPU stacks / profiling | `sample <pid> 5` · `spindump <pid>` |
| Syscall trace | `sudo dtruss`/`ktrace` - **SIP-limited**, prefer `eslogger` |
| Unified log | `log stream --style compact --predicate '…'` |
| Memory dump | `lldb -p N` → `process save-core` · `sample` |
| Dynamic instrumentation | Frida (see below) |

† CLT stubs on a clean VM - `xcode-select --install` (GUI) or run one once to
auto-prompt. `vmmap`+`codesign` cover a lot without CLT.

## ESF (`eslogger`) - reliable capture over SSH
`eslogger` is Apple's built-in ESF CLI (macOS 13+): same exec/fork/file events as
the old Objective-See ProcessMonitor/FileMonitor, as JSON with signing + ancestry.
**No download, no Full Disk Access** - but it **block-buffers** stdout (flushes at
~8 KB / clean exit) and often ignores SIGINT, so naive short captures show 0
events and a `wait` can hang. Force a pty with `script`:
```bash
rc_sudo mac 'script -q /tmp/es.log eslogger exec &'   # pty => line-buffered
# …trigger activity in another rc call…
rc_sudo mac 'pkill -INT eslogger; sleep 0.5; pkill -9 eslogger'
rc mac 'grep -c event_type /tmp/es.log'               # consistent, non-zero
```
Never `wait` on `eslogger`; the pty (not the signal) is what makes it reliable.

## VM-specific limitation (folded in from field notes)
- **`powermetrics`** hardware samplers (`cpu_power`,`gpu_power`) fail in a VM -
  `IODeviceTree:/arm-io/pmgr` doesn't exist (no SMC/pmgr). The **`tasks` sampler
  works**: `sudo powermetrics -n 1 --samplers tasks` (per-proc CPU ms/s, wakeups).
- `dtruss` is SIP-blocked (`Operation not permitted`) - use `eslogger` + `log stream`.
- Objective-See DNSMonitor needs a GUI-approved Network Extension; `log stream`
  on mDNSResponder masks qnames. For a process's domains use live TCP endpoints
  (`lsof`/`nettop`) + reverse-DNS.

## Optional: Frida (dynamic instrumentation)
Stock CLT Python is 3.9; `frida-tools` needs 3.10+. Install python.org 3.12
headlessly, then Frida:
```bash
# on the guest:
curl -LO https://www.python.org/ftp/python/3.12.8/python-3.12.8-macos11.pkg
echo "$PW" | sudo -S installer -pkg python-3.12.8-macos11.pkg -target /
/usr/local/bin/python3.12 -m pip install frida-tools
# CLIs land in /Library/Frameworks/Python.framework/Versions/3.12/bin (add to PATH)
```
- **Injection needs root** (`frida.attach()` as user fails on `task_for_pid`).
- **SIP on** ⇒ can only instrument non-Apple/self-signed binaries; system binaries
  need SIP off. `frida-ps` works without root.
- Frida 17 API: `Module.getExportByName(null,n)` → `Module.getGlobalExportByName(n)`.

## GUI/computer-use on the console (optional)
SSH needs no GUI session, but WindowServer/GUI apps do. Enable headless auto-login
(survives reboots, FileVault must be Off): set `/etc/kcpassword` (pw XOR Apple's
cipher `7D 89 52 23 D2 BC DD EA A3 B9 1F`, padded to ×12) + `defaults write
/Library/Preferences/com.apple.loginwindow autoLoginUser <user>`. Verify with
`stat -f %Su /dev/console`. NOTE: the kcpassword write may trip an agent's
credential classifier - the human may have to run that one line.

## toolkits/windows.md

# Windows guest toolkit (arm64 on Apple Silicon, or x64 on AMD/Intel)

Native tooling covers process/network/signing/logging; add only the Sysinternals
tools native can't do, plus optional cdb for debugging. **Arch matters** for every
download - pick the ARM64 or x64 asset to match `GUEST_win_ARCH`.

## Run-commands-over-SSH gotchas (each one cost real time to learn)
1. **Send PowerShell as UTF-16LE base64** to dodge quoting hell - `run_win`
   (in lib.sh) does this for you: `run_win win 'Get-Process | ...'`.
2. **Non-string output gets CLIXML-mangled** over SSH (blank/garbled). For
   structured data, **write to a file and read it back**:
   `... | Out-File C:\Users\<u>\o.txt` then `rc win 'cmd /c type C:\Users\<u>\o.txt'`.
   `run_win` already strips the `#< CLIXML` / `<Objs …>` noise lines.
3. Always `$ProgressPreference='SilentlyContinue'` (progress leaks as CLIXML) -
   `run_win` prepends this.
4. **`cmd` one-liners are reliable**: `netstat -ano`, `tasklist`, `type`.
   `timeout` errors under redirected SSH stdin - use `ping -n N 127.0.0.1 >nul`
   as a sleep, or `Start-Sleep` in PowerShell.
5. Long/interactive captures: `run_in_background`, or write-to-file-then-read
   (a foreground call can truncate).

## Task → tool matrix
| Task | Native | Sysinternals (add) |
|------|--------|--------------------|
| Process tree + cmdline | `Get-CimInstance Win32_Process` (ParentProcessId, CommandLine) | - |
| Live exec/file/registry events | ETW `logman` (see limits) | `Procmon /BackingFile` |
| Socket ↔ process | `Get-NetTCPConnection -State Established -OwningProcess` · `netstat -ano` | - |
| Byte throughput | `Get-NetAdapterStatistics` | - |
| Packet capture | `pktmon start --capture --pkt-size 0 -f x.etl` → `pktmon etl2pcap x.etl` | - |
| Loaded modules (live proc) | `(Get-Process -Id N).Modules` | `listdlls` · `handle64` |
| Symbols / imports | `dumpbin` (VS) | `sigcheck64 -a` |
| Code signing / trust | `Get-AuthenticodeSignature` | `sigcheck64 -a -h` |
| Persistence / autostart | `Get-ScheduledTask` · `Get-CimInstance Win32_Service` | `autorunsc64 -a * -c` |
| File locks / open handles | - | `handle64 <path>` |
| RAM/CPU per process | `Get-Process` · `Get-Counter '\Process(*)\% Processor Time'` · `tasklist` | - |
| CPU stacks / profiling | `wpr` (see limits) | `procdump64` |
| Syscall/low-level trace | ETW `logman`/`wpr` | `Procmon` |
| Event log | `Get-WinEvent` · `wevtutil` | - |
| Memory dump | - | `procdump64 -ma <pid>` |
| Debugging | `cdb` (see below) | - |

## Sysinternals - install only what native can't do
```powershell
$d='C:\Tools\Sysinternals'; mkdir $d -Force | Out-Null
# x64 host/guest - CLI user-mode tools:
'autorunsc64.exe','handle64.exe','sigcheck64.exe','procdump64.exe' | % {
  iwr "https://live.sysinternals.com/$_" -OutFile "$d\$_" -UseBasicParsing }
# ARM64 guest note: live.sysinternals.com serves x86/x64 only; those x64 CLIs run
# fine under emulation. Only driver-based Procmon needs the NATIVE ARM64 suite:
#   iwr https://download.sysinternals.com/files/SysinternalsSuite-ARM64.zip -OutFile $d\s.zip
#   Expand-Archive $d\s.zip $d -Force
```
Pass `-accepteula` on first run. Skip pslist/pskill/tcpvcon - native beats them.

## Optional: Frida (dynamic instrumentation)
`pip install frida-tools` (needs Python 3.10+; install from python.org if the store
build is older). Match the Frida wheel to the guest arch (arm64 vs x64). Injecting
into another user's / a protected process needs an elevated session - the SSH
session already runs elevated if you seeded `administrators_authorized_keys`. No SIP
equivalent on Windows, so system binaries are hookable (mind PPL/anti-tamper on some).

## VM-specific limitation (folded in from field notes)
- **`wpr`** fails every profile with `0x80070032` ERROR_NOT_SUPPORTED - the guest
  exposes no PMU/kernel-profiling. Substitutes that DO work in-VM:
  - ETW software providers via **`logman`**:
    `logman create trace t -p Microsoft-Windows-Kernel-Process -ets` → real .etl.
  - **Procmon** for behavioral (proc/file/registry) capture.
  - CPU **stack-sampling** has no in-VM substitute (needs PMU) - sample on bare metal.

## Optional: cdb (headless debugger over SSH)
Install the SDK Debuggers feature only (bootstrapper pulls the MSIs; use the
current link from learn.microsoft.com/windows/apps/windows-sdk/downloads):
```powershell
winsdksetup.exe /features OptionId.WindowsDesktopDebuggers /quiet /norestart /ceip off
# -> C:\Program Files (x86)\Windows Kits\10\Debuggers\arm64\cdb.exe  (or \x64\)
```
- cdb is the headless/SSH-friendly one; classic `windbg.exe` ships alongside it;
  modern WinDbgX (`winget install Microsoft.WinDbg`, TTD) is MSIX/GUI - bad headless.
- **Gotcha:** `lm` / symbol resolution hangs cdb over SSH via the msdl server. Set
  `$env:_NT_SYMBOL_PATH=''` (or a local cache) first, and wrap calls in a
  `Start-Job` + `Wait-Job -Timeout` so a stuck session can't hang the SSH call.

## tools

```

```

## tools/asar.py

```python
#!/usr/bin/env python3
# Minimal Electron .asar reader - no node/npm needed (runs on any guest with
# python3). List files, or extract one by exact-or-substring path to stdout.
#   asar.py <app.asar> --list                 # size + UNP flag + path, one per line
#   asar.py <app.asar> /package.json          # exact path preferred over substring
#   asar.py <app.asar> build/index.pre.js     # substring match
# asar stores JS as plaintext (no compression/encryption), so grep also works
# directly on the .asar; this tool is for pulling individual files out cleanly.
import sys, struct, json

def load(path):
    with open(path, 'rb') as f:
        struct.unpack('<I', f.read(4))[0]                 # pickle1 size (=4)
        header_size = struct.unpack('<I', f.read(4))[0]   # header pickle total size
        struct.unpack('<I', f.read(4))[0]                 # json pickle payload size
        json_len = struct.unpack('<I', f.read(4))[0]      # json string length
        header = json.loads(f.read(json_len).decode('utf-8', 'replace'))
    return header, 8 + header_size                        # (header, base offset)

def walk(node, prefix=''):
    for name, meta in node.get('files', {}).items():
        p = prefix + '/' + name
        if 'files' in meta:
            yield from walk(meta, p)
        else:
            yield p, meta

def main():
    if len(sys.argv) < 3:
        sys.exit("usage: asar.py <app.asar> (--list | <path-substring>)")
    path, sel = sys.argv[1], sys.argv[2]
    header, base = load(path)
    files = list(walk(header))
    if sel == '--list':
        for p, m in files:
            print(f"{m.get('size','?'):>10} {'UNP' if m.get('unpacked') else '   '} {p}")
        return
    cands = [(p, m) for p, m in files if sel in p and not m.get('unpacked')]
    pick = [c for c in cands if c[0] == sel] or cands
    if not pick:
        sys.exit(f"no packed file matching: {sel}")
    p, m = pick[0]
    sys.stderr.write(f"[extracted {p} ({m['size']}b)]\n")
    with open(path, 'rb') as f:
        f.seek(base + int(m['offset']))
        sys.stdout.buffer.write(f.read(m['size']))

main()
```

## tools/electron-triage.sh

```bash

```

## tools/inspect-linux.sh

```bash

```

## tools/inspect-macos.sh

```bash

```

## tools/inspect-windows.ps1

```

```

## verify.sh

```bash

```

