2026-10-08-pentest-toolkit.md (30567B)
1 # Pentest Toolkit Implementation Plan 2 3 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. 4 5 **Goal:** Build a Kali-equivalent, modular, toggleable offensive toolkit into the NixDaemon flake, with an HTB workflow layer (`htbtarget` / `htbvpn` / `htbtime`) and a multi-arch payload staging tree. 6 7 **Architecture:** Dendritic flake-parts. Each tool category declares one `flake.pentestPackages.<cat>` function (`pkgs -> [package]`); a `mkCategory` helper in `modules/features/pentest/_sets.nix` derives the gated `flake.nixosModules.pentest-<cat>`, a `perSystem.devShells.pentest-<cat>`, and a `perSystem.checks.pentest-<cat>` smoke test from that single list. Tools install to `environment.systemPackages` so they work under `sudo`. Privileged user commands follow the repo's existing `fan-ec` / `fan` split: a fixed store script plus a scoped NOPASSWD sudo rule. 8 9 **Tech Stack:** Nix (flakes, flake-parts, import-tree), NixOS 26.11, home-manager, `pkgsCross.mingwW64` for Windows binaries, zsh `precmd` for cross-terminal state, systemd template units for VPN. 10 11 **Spec:** `docs/superpowers/specs/2026-10-08-pentest-toolkit-design.md` 12 13 ## Global Constraints 14 15 - Every `modules/**/*.nix` is a flake-parts module; any path containing `/_` is **not** auto-imported (use for plain helper/derivation files). 16 - Modules reference each other **by name** through `self.nixosModules.*` / `self.homeModules.*`, never by path. 17 - Tool packages go to `environment.systemPackages`, never `home.packages` (spec D1 — `sudo nmap -sS` must work). 18 - Use native NixOS options where they exist: `programs.wireshark.enable`, `programs.proxychains.enable` (spec D5). 19 - Master switch `daemon.pentest.enable`, default `false`. Default-on categories: `recon ad bloodhound web pivot crack shells wordlists payloads python gui`. Default-off: `dfir reversing wireless cloud osint mobile`. 20 - System is `x86_64-linux`; the only host is `nixosConfigurations.nixos`. 21 - Every file opens with a `# modules/path/file.nix — <purpose>` comment block in the existing house style (see `modules/home/fan.nix`). 22 - Commit messages end with `Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>`. 23 - Verification command for any module task: `nix build .#checks.x86_64-linux.pentest-<cat>` then `nix flake check`. 24 25 ## Review Focus 26 27 Five failure modes the spec implies but does not pin to a test. Each has a test added to the owning task. 28 29 1. **impacket suffix-free aliases shadowing real commands** (Task 3). `impacket` ships `split.py`, `ping.py`, `net.py`, `reg.py`, `services.py`, `attrib.py`, `smbclient.py`. Stripping `.py` would make `split -l 1000 hosts` invoke an SMB tool, and `net`/`smbclient` collide with samba once Task 5 lands. Collisions must be detected against the assembled closure at build time and exposed as `impacket-<name>` instead. 30 2. **`payload-serve` binding every interface when `tun0` is absent** (Task 12). Must fail closed rather than silently publishing the payload tree to the local network. 31 3. **`htbtime off` with a missing or corrupt state file** (Task 14). Must restore the NixOS default (NTP enabled) rather than leaving the clock unmanaged — including when `off` is run without a preceding `htbtime`. 32 4. **`htbtarget` writing unvalidated input to `/etc/hosts`** (Task 15). Anything that is not an IPv4/IPv6 address or a DNS label must be rejected before the privileged helper runs, so no arbitrary line can land in `/etc/hosts`. 33 5. **`htbvpn up` while another profile is active** (Task 13). Must stop the running unit first; two concurrent `tun` devices must be impossible. 34 35 --- 36 37 ### Task 1: Category harness, options tree, and `core` 38 39 **Files:** 40 - Create: `modules/features/pentest/_sets.nix` (helper, not auto-imported) 41 - Create: `modules/features/pentest/options.nix` 42 - Create: `modules/features/pentest/core.nix` 43 - Modify: `modules/hosts/laptop/configuration.nix` (import `pentest-options` + `pentest-core`, set `daemon.pentest.enable = true`) 44 45 **Interfaces:** 46 - Consumes: nothing. 47 - Produces: 48 - `mkCategory :: { name : String, description : String, packages : pkgs -> [package], expectedBins : [String], extraModule ? : module, default ? : Bool } -> flakePartsModule` — returns a module declaring `flake.pentestPackages.<name>`, `flake.nixosModules.pentest-<name>`, `perSystem.checks.pentest-<name>`, `perSystem.devShells.pentest-<name>`. 49 - `daemon.pentest.enable`, `daemon.pentest.<name>.enable`, `daemon.pentest.payloads.windowsArches`, `daemon.pentest.htb.vpnDir`, `daemon.pentest.htb.promptTarget`. 50 - Env: `$PAYLOADS`, `$WORDLISTS` (set by `core.nix`, values filled in by Tasks 2 and 12). 51 52 - [ ] **Step 1: Write the failing check** 53 54 In `_sets.nix`, `mkCategory` must produce a check derivation that fails when any expected binary is absent. Write `core.nix` declaring the category with its binaries, so the check exists before the module works: 55 56 ```nix 57 # core.nix declares, via mkCategory: 58 # name = "core"; 59 # packages = pkgs: with pkgs; [ ncat socat samba krb5 openldap sshpass rlwrap ]; 60 # expectedBins = [ "ncat" "socat" "smbclient" "kinit" "ldapsearch" "sshpass" ]; 61 ``` 62 63 The check runs, for each name in `expectedBins`, `command -v <name>` inside a derivation whose `buildInputs` are the category's packages, and fails naming every binary it could not find. 64 65 - [ ] **Step 2: Run the check to verify it fails** 66 67 Run: `nix build .#checks.x86_64-linux.pentest-core -L` 68 Expected: FAIL — `error: flake output attribute 'checks.x86_64-linux.pentest-core' does not exist` (before `_sets.nix` is written), then a missing-binary failure naming `smbclient` (samba's binary is not in `pkgs.samba`'s default output path until the correct attribute is used). 69 70 - [ ] **Step 3: Implement `mkCategory` in `modules/features/pentest/_sets.nix`** 71 72 A function taking the attrset above and returning a flake-parts module. The gated nixosModule body is `config = lib.mkIf (cfg.enable && cfg.${name}.enable) { environment.systemPackages = packages pkgs; }` merged with `extraModule`. Resolve `smbclient` from `pkgs.samba` (it is not a top-level attribute — confirmed absent). 73 74 - [ ] **Step 4: Implement `options.nix` and `core.nix`** 75 76 `options.nix` declares `flake.nixosModules.pentest-options` with the option tree from Global Constraints, plus an assertion that no `daemon.pentest.<cat>.enable` is true while `daemon.pentest.enable` is false. `core.nix` uses `mkCategory` and additionally sets `environment.sessionVariables.PAYLOADS` and `WORDLISTS` to `lib.mkDefault` placeholders that Tasks 2 and 12 override. 77 78 - [ ] **Step 5: Wire into the host** 79 80 Add `pentest-options` and `pentest-core` to the `imports` list in `modules/hosts/laptop/configuration.nix`, and set `daemon.pentest.enable = true;` beside the existing `daemon.desktop` block. 81 82 - [ ] **Step 6: Verify the check passes and the system builds** 83 84 Run: `nix build .#checks.x86_64-linux.pentest-core -L && nix build .#nixosConfigurations.nixos.config.system.build.toplevel` 85 Expected: both succeed. 86 87 - [ ] **Step 7: Commit** 88 89 ```bash 90 git add modules/features/pentest/ modules/hosts/laptop/configuration.nix 91 git commit -m "feat(pentest): category harness, option tree, and core module" 92 ``` 93 94 --- 95 96 ### Task 2: Wordlists 97 98 **Files:** 99 - Create: `modules/features/pentest/wordlists.nix` 100 101 **Interfaces:** 102 - Consumes: `mkCategory` (Task 1). 103 - Produces: `$WORDLISTS` pointing at a directory containing `rockyou.txt` **decompressed**; `flake.pentestPackages.wordlists`. 104 105 - [ ] **Step 1: Write the failing check** 106 107 The check asserts the decompressed file exists and is plain text: 108 109 ```nix 110 expectedPaths = [ "$WORDLISTS/rockyou.txt" "$WORDLISTS/seclists" ]; 111 # plus: test "$(head -c2 $WORDLISTS/rockyou.txt)" != "$(printf '\037\213')" # not gzip magic 112 ``` 113 114 - [ ] **Step 2: Run the check to verify it fails** 115 116 Run: `nix build .#checks.x86_64-linux.pentest-wordlists -L` 117 Expected: FAIL — attribute does not exist. 118 119 - [ ] **Step 3: Implement `wordlists.nix`** 120 121 `packages = pkgs: [ seclists wordlists exploitdb ]`. Add a derivation that `gunzip`s `${pkgs.rockyou}`'s gzipped payload into `$out/rockyou.txt` and symlinks `${pkgs.seclists}/share/seclists` to `$out/seclists`. Set `environment.sessionVariables.WORDLISTS` to that derivation with `lib.mkForce`. 122 123 - [ ] **Step 4: Verify** 124 125 Run: `nix build .#checks.x86_64-linux.pentest-wordlists -L` 126 Expected: PASS. 127 128 - [ ] **Step 5: Commit** 129 130 ```bash 131 git add modules/features/pentest/wordlists.nix 132 git commit -m "feat(pentest): wordlists with decompressed rockyou and \$WORDLISTS" 133 ``` 134 135 --- 136 137 ### Task 3: Offensive Python environment and impacket aliases 138 139 **Files:** 140 - Create: `modules/features/pentest/python.nix` 141 - Create: `modules/features/pentest/_aliases.nix` (helper) 142 143 **Interfaces:** 144 - Consumes: `mkCategory` (Task 1). 145 - Produces: 146 - `pentest-python` — a `python3.withPackages` interpreter with every offensive library importable. 147 - `mkSuffixFreeAliases :: { package, reserved : [String], prefix : String } -> derivation` — symlinks `bin/foo.py` to `bin/foo`, **except** names in `reserved`, which become `bin/<prefix>-foo`. 148 - `impacket` — a discovery command: no args lists all scripts via fzf with `--help` preview; `impacket <name>` execs that script. 149 150 - [ ] **Step 1: Write the failing collision test** 151 152 This is Review Focus #1. The check must prove that no alias shadows a command provided by any other enabled category, computed from the closure rather than hardcoded: 153 154 ```nix 155 # checks.pentest-python asserts: 156 # 1. `pentest-python -c "import impacket, certipy, pypykatz, bloodyad, lsassy, scapy, pwn"` exits 0 157 # 2. `secretsdump --help` and `GetUserSPNs --help` exit 0 (aliased) 158 # 3. `command -v split` resolves into coreutils, NOT impacket (collision avoided) 159 # 4. `command -v ping` resolves into iputils, NOT impacket 160 # 5. `impacket-split --help` and `impacket-net --help` exit 0 (prefixed instead) 161 # 6. the alias derivation's build FAILS if a new upstream script 162 # collides with a reserved name that is not in the prefix list 163 ``` 164 165 - [ ] **Step 2: Run the check to verify it fails** 166 167 Run: `nix build .#checks.x86_64-linux.pentest-python -L` 168 Expected: FAIL — attribute does not exist. 169 170 - [ ] **Step 3: Implement `mkSuffixFreeAliases` in `_aliases.nix`** 171 172 Glob `${package}/bin/*.py`. For each, strip `.py`; if the stripped name appears in `reserved`, emit `${prefix}-${name}` instead, otherwise emit the bare name. Build `reserved` at eval time from the binaries of `coreutils`, `iputils`, `samba`, `util-linux` and `systemd` so it tracks the real closure instead of a hand-maintained list. Fail the build listing any name that is reserved **and** absent from the emitted prefixed set. 173 174 - [ ] **Step 4: Implement `python.nix`** 175 176 `pentestPython = pkgs.python3.withPackages (ps: with ps; [ impacket certipy pywerview dploot masky bloodhound ldapdomaindump pypykatz bloodyad lsassy minikerberos aiowinreg ldap3 dnspython scapy pwntools pycryptodomex requests rich ])`, exposed as `pentest-python`. Per spec C2, if `withPackages` fails to resolve, fall back to listing the conflicting tools as standalone packages and keep the shared env for the libraries only — record which path was taken in the file's header comment. 177 178 - [ ] **Step 5: Verify** 179 180 Run: `nix build .#checks.x86_64-linux.pentest-python -L` 181 Expected: PASS — including assertions 3 and 4, which prove `split` and `ping` still resolve to coreutils and iputils. 182 183 - [ ] **Step 6: Commit** 184 185 ```bash 186 git add modules/features/pentest/python.nix modules/features/pentest/_aliases.nix 187 git commit -m "feat(pentest): offensive python env, impacket aliases with collision guard" 188 ``` 189 190 --- 191 192 ### Task 4: `recon` 193 194 **Files:** Create `modules/features/pentest/recon.nix` 195 196 **Interfaces:** Consumes `mkCategory`. Produces `flake.pentestPackages.recon`. 197 198 - [ ] **Step 1: Write the failing check** — `expectedBins = [ "nmap" "masscan" "rustscan" "naabu" "fscan" "nbtscan" "enum4linux-ng" "snmp-check" "onesixtyone" "dnsrecon" "subfinder" "amass" "httpx" "dnsx" "katana" "gowitness" "whatweb" ]`. 199 - [ ] **Step 2: Run to verify it fails** — `nix build .#checks.x86_64-linux.pentest-recon -L`, expect attribute-missing. 200 - [ ] **Step 3: Implement** — `packages` per spec C1 recon list, plus `thc-ipv6` and `eyewitness`. Note `snmpcheck`'s binary is `snmp-check`; resolve the real binary names rather than assuming they match attribute names. 201 - [ ] **Step 4: Verify** — same command, expect PASS. 202 - [ ] **Step 5: Commit** — `feat(pentest): recon and scanning module` 203 204 --- 205 206 ### Task 5: `ad` 207 208 **Files:** Create `modules/features/pentest/ad.nix` 209 210 **Interfaces:** Consumes `mkCategory`, Task 3's `pentest-python`. Produces `flake.pentestPackages.ad`. 211 212 - [ ] **Step 1: Write the failing check** — `expectedBins = [ "nxc" "certipy" "bloodhound-python" "kerbrute" "responder" "mitm6" "coercer" "donpapi" "adidnsdump" "ldapdomaindump" "smbmap" "evil-winrm" "pypykatz" "bloodyAD" "lsassy" "dploot" "masky" "pywerview" "secretsdump" "ntlmrelayx" "GetUserSPNs" ]`. The last three come from Task 3's aliases and prove the two tasks compose. 213 - [ ] **Step 2: Run to verify it fails.** 214 - [ ] **Step 3: Implement** — spec C1 `ad` list. `netexec`'s binary is `nxc`; `bloodyad`'s is `bloodyAD`. Custom `krbrelayx` and `sprayhound` are deferred to Task 11 and must **not** appear in `expectedBins` yet. 215 - [ ] **Step 4: Verify** — expect PASS, and additionally run `sudo -n true 2>/dev/null; sudo nmap -sS -p22 127.0.0.1` on the built system to prove spec D1 (root PATH). 216 - [ ] **Step 5: Commit** — `feat(pentest): active directory module` 217 218 --- 219 220 ### Task 6: `web` 221 222 **Files:** Create `modules/features/pentest/web.nix` 223 224 - [ ] **Step 1: Write the failing check** — `expectedBins = [ "ffuf" "gobuster" "feroxbuster" "dirb" "nikto" "sqlmap" "commix" "wfuzz" "nuclei" "wpscan" "joomscan" "dalfox" "arjun" "jwt-cli" "jwt-hack" "mitmproxy" ]`. GUI tools (`burpsuite`, `zap`) belong to Task 10, not here. 225 - [ ] **Step 2: Run to verify it fails.** 226 - [ ] **Step 3: Implement** — spec C1 `web` list minus the GUI entries. Set `NUCLEI_TEMPLATES_DIR` to `${pkgs.nuclei-templates}` so `nuclei` does not try to write to `$HOME` on first run. 227 - [ ] **Step 4: Verify** — expect PASS. 228 - [ ] **Step 5: Commit** — `feat(pentest): web application module` 229 230 --- 231 232 ### Task 7: `pivot`, `crack`, `shells` 233 234 Grouped: three identical-shape package lists over the Task 1 helper. The only non-trivial part is `programs.proxychains.enable`. 235 236 **Files:** Create `modules/features/pentest/{pivot,crack,shells}.nix` 237 238 - [ ] **Step 1: Write the three failing checks** 239 - pivot: `[ "ligolo-proxy" "ligolo-agent" "chisel" "socat" "proxychains4" "sshuttle" "gost" "frpc" "iodine" "stunnel" "wg" "openvpn" ]` 240 - crack: `[ "hashcat" "john" "hydra" "medusa" "crowbar" "crunch" "cewl" "hashid" "nth" ]` 241 - shells: `[ "msfconsole" "msfvenom" "pwsh" "pwncat" "rlwrap" "updog" "miniserve" "xfreerdp" "upx" ]` 242 - [ ] **Step 2: Run all three to verify they fail.** 243 - [ ] **Step 3: Implement the three modules** — spec C1 lists. In `pivot.nix`'s `extraModule`, set `programs.proxychains.enable = true` with `proxyDNS = true` and a `socks5 127.0.0.1 1080` default, because `/etc/proxychains.conf` is unwritable on NixOS (spec D5). Verify `ligolo-ng`'s actual binary names and `name-that-hash`'s (`nth`) before asserting them. 244 - [ ] **Step 4: Verify** — all three checks PASS. 245 - [ ] **Step 5: Commit** — `feat(pentest): pivoting, cracking, and shell modules` 246 247 --- 248 249 ### Task 8: Optional categories (`dfir`, `reversing`, `wireless`, `cloud`, `osint`, `mobile`) 250 251 Grouped: six package lists, all `default = false`. 252 253 **Files:** Create `modules/features/pentest/{dfir,reversing,wireless,cloud,osint,mobile}.nix` 254 255 - [ ] **Step 1: Write the six failing checks** — binaries per spec C1. Checks must build even when the category is disabled, since they test the package list, not the system closure. 256 - [ ] **Step 2: Run to verify they fail.** 257 - [ ] **Step 3: Implement the six modules** — `default = false` for each. `wireless.nix`'s `extraModule` sets `programs.wireshark.enable = true` with `package = pkgs.wireshark` and adds `daemonsec` to the `wireshark` group, because the package alone cannot capture without root (spec D5). `velociraptor` is deferred to Task 11. 258 - [ ] **Step 4: Verify** — `nix flake check` passes with all six disabled. 259 - [ ] **Step 5: Commit** — `feat(pentest): dfir, reversing, wireless, cloud, osint, mobile modules` 260 261 --- 262 263 ### Task 9: BloodHound CE with neo4j and postgresql 264 265 Separate task: the only category needing stateful services, so a reviewer could reject this while accepting Task 5. 266 267 **Files:** Create `modules/features/pentest/bloodhound.nix` 268 269 **Interfaces:** Produces `flake.nixosModules.pentest-bloodhound`; a `bloodhound-up` / `bloodhound-down` pair so the databases are not running on every boot. 270 271 - [ ] **Step 1: Write the failing test** 272 273 A NixOS VM test (`pkgs.nixosTest`) asserting that with `daemon.pentest.bloodhound.enable = true` the `neo4j` and `postgresql` units reach active, and port 7474 answers. A VM test rather than a binary check, because the deliverable is running services. 274 275 - [ ] **Step 2: Run to verify it fails** — `nix build .#checks.x86_64-linux.pentest-bloodhound-vm -L`, expect attribute-missing. 276 - [ ] **Step 3: Implement** — `services.neo4j.enable` and `services.postgresql.enable` (with a `bloodhound` database and user) gated on the category, plus `bloodhound-ce`. Both services set `wantedBy = []` so they start only via `bloodhound-up`, which is a `writeShellScriptBin` running `systemctl start` through a scoped NOPASSWD rule as in `fan-cli.nix`. 277 - [ ] **Step 4: Verify** — VM test PASSES. 278 - [ ] **Step 5: Commit** — `feat(pentest): bloodhound-ce with neo4j and postgresql` 279 280 --- 281 282 ### Task 10: GUI tools and desktop entries 283 284 **Files:** Create `modules/features/pentest/gui.nix` 285 286 - [ ] **Step 1: Write the failing check** — `expectedBins = [ "burpsuite" "zap" "ghidra" "wireshark" "autopsy" "cutter" "sqlitebrowser" ]`, plus an assertion that each ships a `share/applications/*.desktop` file so they appear in the launcher like Kali's menu. 287 - [ ] **Step 2: Run to verify it fails.** 288 - [ ] **Step 3: Implement** — the GUI set; `allowUnfree` already holds for `burpsuite`. Gate on `daemon.pentest.gui.enable` (default true per D4). Do not duplicate `wireshark`'s native option here — depend on `wireless.nix`'s and assert in the check that enabling `gui` without `wireless` still yields a usable `wireshark` by setting `programs.wireshark.enable` in whichever module is enabled, using `lib.mkDefault` to avoid a conflict. 289 - [ ] **Step 4: Verify** — check PASSES; `nix flake check` passes with both `gui` and `wireless` enabled (proving no option conflict). 290 - [ ] **Step 5: Commit** — `feat(pentest): gui tools with desktop entries` 291 292 --- 293 294 ### Task 11: Missing-tool derivations 295 296 **Files:** 297 - Create: `modules/features/pentest/_pkgs/default.nix` (an attrset the categories consume) 298 - Create: `modules/features/pentest/_pkgs/{krbrelayx,sprayhound,velociraptor,ropper,lse,peass,sharpcollection,potato,printer}.nix` 299 - Modify: `ad.nix`, `dfir.nix`, `reversing.nix` to pull from `_pkgs` and extend their `expectedBins` 300 301 **Interfaces:** 302 - Produces: `pentestPkgs :: attrset` of derivations, consumed by Task 12's payload tree and by the category modules. 303 304 - [ ] **Step 1: Write the failing check** 305 306 `checks.pentest-pkgs` asserts each derivation builds and, for script collections, that a named file exists in the output (e.g. `${peass}/linpeas.sh`, `${sharpcollection}/NetFramework_4.7_Any/Rubeus.exe`). Prebuilt Windows artefacts must additionally report as PE: `file` output contains `PE32`. 307 308 - [ ] **Step 2: Run to verify it fails.** 309 - [ ] **Step 3: Obtain real hashes, then implement** 310 311 Hashes cannot be guessed. For each source run `nix-prefetch-url --unpack <url>` (or `nurl <repo>`) and paste the result. Linux/Python tools (`krbrelayx`, `sprayhound`, `ropper`, `lse`, `velociraptor`) are `fetchFromGitHub` + `buildPythonApplication` or `writeShellApplication` wrappers. Prebuilt sets (`peass`, `sharpcollection`, the potato family, the printer family) are `fetchFromGitHub` / `fetchurl` of release assets installed verbatim — per spec D3, building .NET offline is out of scope. 312 313 - [ ] **Step 4: Extend the consuming categories** — add `krbrelayx`/`sprayhound` to `ad.nix`, `velociraptor` to `dfir.nix`, `ropper` to `reversing.nix`, with their binaries appended to those `expectedBins`. 314 - [ ] **Step 5: Verify** — `nix build .#checks.x86_64-linux.pentest-pkgs -L` PASSES, and the three amended category checks still pass. 315 - [ ] **Step 6: Commit** — `feat(pentest): derivations for tools absent from nixpkgs` 316 317 --- 318 319 ### Task 12: Payload tree, cross-compiled binaries, and `payload-serve` 320 321 **Files:** 322 - Create: `modules/features/pentest/payloads.nix` 323 - Create: `modules/features/pentest/_payloads.nix` (the tree-assembly function) 324 325 **Interfaces:** 326 - Consumes: `pentestPkgs` (Task 11). 327 - Produces: `$PAYLOADS` (overriding Task 1's placeholder with `lib.mkForce`); `payload-serve` with flags `[port]`, `--smb`, `--list`. 328 329 - [ ] **Step 1: Write the failing checks** 330 331 Two assertions, the second being Review Focus #2: 332 333 ```nix 334 # 1. structure: every path in the spec C3 tree exists, and 335 # file $PAYLOADS/windows/amd64/creds/mimikatz-2.2.0.exe contains "PE32+" 336 # file $PAYLOADS/linux/amd64/agents/ligolo-agent contains "ELF 64-bit" 337 # file $PAYLOADS/linux/arm64/agents/ligolo-agent contains "ARM aarch64" 338 # 2. payload-serve refuses to start when no tun interface exists: 339 # ip link show tun0 absent => exit 2, message naming htbvpn, 340 # and NOTHING listening on the requested port afterwards 341 ``` 342 343 - [ ] **Step 2: Run to verify it fails.** 344 - [ ] **Step 3: Implement the cross-builds** 345 346 In `_payloads.nix`, build Windows artefacts as `pkgs.pkgsCross.mingwW64.<tool>` (verified available for `mimikatz`, `ligolo-ng`, `netexec`) and Linux arm64 as `pkgs.pkgsCross.aarch64-multiplatform.<tool>`. Assemble the spec C3 tree with `runCommand` + `lib.fileset`, naming versioned duplicates by version (`mimikatz-2.2.0.exe`) rather than letting them contend for one name. 347 348 - [ ] **Step 4: Implement `payload-serve`** 349 350 Resolve the serve address from the first `tun*` interface. If none exists, exit 2 naming `htbvpn up` — never fall back to `0.0.0.0`, which would publish the tree to the LAN. `--smb` runs impacket's `smbserver` bound to the same address; `--list` prints `tree $PAYLOADS`. 351 352 - [ ] **Step 5: Verify** — both checks PASS. Confirm assertion 2 by running `payload-serve 8000` with no VPN up and checking `ss -tlnp` shows nothing new. 353 - [ ] **Step 6: Commit** — `feat(pentest): multi-arch payload tree and payload-serve` 354 355 --- 356 357 ### Task 13: `htbvpn` 358 359 **Files:** 360 - Create: `modules/features/pentest/vpn.nix` 361 362 **Interfaces:** 363 - Produces: `htbvpn@.service` (systemd template); `htbvpn` with subcommands `list|up|down|status`; `htbip`. 364 365 - [ ] **Step 1: Write the failing test** 366 367 A `nixosTest` with a dummy OpenVPN profile, asserting the Review Focus #5 behaviour: 368 369 ``` 370 1. htbvpn up profileA => htbvpn@profileA active, tun0 has an address 371 2. htbvpn up profileB => profileA STOPPED first; exactly one tun device exists 372 3. htbvpn down => no htbvpn@* unit active, tun0 gone 373 4. htbvpn up missingProfile => exit 2, lists available profiles, no unit started 374 ``` 375 376 - [ ] **Step 2: Run to verify it fails.** 377 - [ ] **Step 3: Implement** 378 379 `systemd.services."htbvpn@"` runs `openvpn --config ${vpnDir}/%i.ovpn` with `Restart=no`. `htbvpn up` resolves the profile, stops any active `htbvpn@*` instance, then starts the new one through a NOPASSWD sudo rule scoped to `systemctl {start,stop,restart} htbvpn@*` only — mirroring `fan-cli.nix`. Create `${vpnDir}` via `systemd.tmpfiles` and leave profiles un-managed (spec C5). 380 381 - [ ] **Step 4: Verify** — the VM test PASSES, all four assertions. 382 - [ ] **Step 5: Commit** — `feat(pentest): htbvpn via systemd template unit` 383 384 --- 385 386 ### Task 14: `htbtime` 387 388 **Files:** 389 - Create: `modules/features/pentest/time.nix` 390 391 **Interfaces:** 392 - Produces: `htb-time` (root helper, `environment.systemPackages` + scoped NOPASSWD rule); subcommands `<target>|off|status`. 393 394 - [ ] **Step 1: Write the failing test** 395 396 A `nixosTest` covering the round trip and Review Focus #3: 397 398 ``` 399 1. record timedatectl output 400 2. htb-time 10.0.0.1 => NTP disabled, timesyncd inactive, state file written 401 3. htb-time off => timedatectl output matches step 1 exactly 402 4. rm the state file; htb-time off 403 => NTP ENABLED (the NixOS default), exit 0, warning printed 404 -- must never leave the clock unmanaged 405 5. htb-time <unreachable> 406 => exit nonzero, warning naming `htbtime off`, NTP still disabled 407 (deliberate: spec C6 says do not silently half-apply) 408 ``` 409 410 - [ ] **Step 2: Run to verify it fails.** 411 - [ ] **Step 3: Implement** 412 413 `ntpdate` comes from `${pkgs.ntp}/bin/ntpdate` — it is **not** a top-level package (confirmed). Record prior state to `/var/lib/htb-time/state`: whether `systemd-timesyncd` was active, `timedatectl show -p NTP`, and whether `chrony`/`ntpd` were running. Sync with `ntpdate -u <target>`, falling back to `${pkgs.chrony}/bin/chronyd -q` when the DC refuses. On `off` with no or unparseable state, default to `timedatectl set-ntp true` and say so. Print the resulting offset and warn that a large skew breaks TLS, so `nix` and `git` may fail. 414 415 - [ ] **Step 4: Verify** — the VM test PASSES, all five assertions, especially 4. 416 - [ ] **Step 5: Commit** — `feat(pentest): htbtime, conflict-aware clock skew control` 417 418 --- 419 420 ### Task 15: `htbtarget`, cross-terminal state, and engagement scaffolding 421 422 **Files:** 423 - Create: `modules/home/htb.nix` 424 - Create: `modules/home/htb-shell.nix` 425 - Create: `modules/features/pentest/hosts.nix` (the `/etc/hosts` root helper) 426 - Modify: `modules/home/default.nix` (import `htb` and `htb-shell` by name) 427 428 **Interfaces:** 429 - Consumes: `htbvpn`/`htbip` (Task 13), `htbtime` (Task 14). 430 - Produces: `htbtarget [IP [FQDN]] | clear`; exported `$TARGET`, `$RHOST`, `$IP`, `$BOX`; `htb new <box>`; `htb ls`; `htb-hosts` root helper. 431 432 - [ ] **Step 1: Write the failing tests** 433 434 Two parts. First the cross-terminal guarantee from spec C4: 435 436 ``` 437 1. shell A: htbtarget 10.10.11.5 438 2. shell B (already running, new prompt): $TARGET == 10.10.11.5 439 ``` 440 441 Second, Review Focus #4 — validation before the privileged helper runs: 442 443 ``` 444 3. htbtarget "10.10.11.5" => accepted 445 4. htbtarget "not an ip" => exit 2, /etc/hosts unchanged 446 5. htbtarget "10.10.11.5" $'x\n1.2.3.4 evil' => exit 2, /etc/hosts unchanged 447 6. htbtarget 10.10.11.5 dc01.vintage.htb => exactly one marked block in 448 /etc/hosts; running twice does 449 not duplicate it 450 7. htbtarget clear; htbtime => exit 2, message naming `htbtarget` 451 as the command that sets a target 452 ``` 453 454 - [ ] **Step 2: Run to verify they fail.** 455 - [ ] **Step 3: Implement the state file and shell hook** 456 457 State in `$XDG_STATE_HOME/htb/{target,name,vpn}`. In `htb-shell.nix`, add a zsh `precmd` function that re-reads those files and exports `$TARGET`, `$RHOST`, `$IP`, `$BOX`. Wire it through `programs.zsh.initContent` so it coexists with the dotfiles' ZDOTDIR tree (`modules/home/shell.nix`) rather than overwriting it. When `daemon.pentest.htb.promptTarget`, add a starship custom module showing `$TARGET`. 458 459 - [ ] **Step 4: Implement validation and the `/etc/hosts` helper** 460 461 Validate the IP with a strict IPv4/IPv6 match and the FQDN against `^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$` **before** invoking `htb-hosts`. `htb-hosts` rewrites only the region between `# BEGIN htb` and `# END htb` markers, so repeated calls replace rather than append. Scoped NOPASSWD rule as in `fan-cli.nix`. 462 463 - [ ] **Step 5: Implement `htb new` and `htb ls`** 464 465 `htb new <box>` creates `~/htb/<box>/{nmap,loot,creds,www,exploit}` plus `notes.md` from a template, sets the target when given one, and prints the path. `htb ls` lists `~/htb/*` newest first. 466 467 - [ ] **Step 6: Verify** — all six assertions pass; confirm assertion 2 by hand in two live terminals. 468 - [ ] **Step 7: Commit** — `feat(pentest): htbtarget cross-terminal state and engagement scaffolding` 469 470 --- 471 472 ### Task 16: Cheat card, `pentest-update`, devshells, and docs 473 474 **Files:** 475 - Create: `modules/home/cheats/pentest.md` 476 - Modify: `modules/home/cheats.nix` (add `"pentest"` to `cards`) 477 - Create: `modules/features/pentest/devshells.nix` 478 - Create: `modules/features/pentest/update.nix` 479 - Modify: `README.md` (a `Pentest` row in the "What runs" table and a tree entry) 480 481 - [ ] **Step 1: Write the failing checks** 482 483 `pentest-cheat --list` prints exactly `recon ad pivot transfer crack web dfir htb`; `nix develop .#pentest --command nxc --version` exits 0; `pentest-update --dry-run` exits 0 and changes no file. 484 485 - [ ] **Step 2: Run to verify they fail.** 486 - [ ] **Step 3: Implement the cheat card** — sections per the check, in the existing `modules/home/cheats/*.md` format. Document the two spec limitations explicitly: `$TARGET` refreshes at the next prompt, and `htbtime` skew breaks TLS. 487 - [ ] **Step 4: Implement `devshells.nix`** — `devShells.pentest` as the union of every `flake.pentestPackages.*`, plus the per-category shells already produced by `mkCategory`. 488 - [ ] **Step 5: Implement `pentest-update`** — re-pins every `_pkgs/` derivation via `nix-prefetch-url`/`nurl`, rewrites hashes in place, prints a diff, never commits. `--dry-run` only reports. 489 - [ ] **Step 6: Verify** — all three checks PASS; `nix flake check` passes whole-flake. 490 - [ ] **Step 7: Commit** — `feat(pentest): cheat card, devshells, pentest-update, README` 491 492 --- 493 494 ## Done when 495 496 - `nix flake check` passes with every default-on category enabled. 497 - `sudo nmap -sS` works (spec D1). 498 - `split` and `ping` still resolve to coreutils and iputils (Review Focus #1). 499 - `payload-serve` refuses to bind without a tun device (Review Focus #2). 500 - `htbtime off` restores the clock from a missing state file (Review Focus #3). 501 - `htbtarget` rejects malformed input before touching `/etc/hosts` (Review Focus #4). 502 - `htbvpn up` never leaves two tun devices (Review Focus #5). 503 - `$TARGET` set in one terminal appears in another at its next prompt.