NixDaemon

NixOS pentest workstation as one flake — IceBreaker's successor
git clone https://git.daemon-sec.xyz/NixDaemon.git
Log | Files | Refs | README

commit 957a875a8227e402bba871eae1931886c168f112
parent cc6ab431037cba77c880c91dec92916c900b42c7
Author: DAEMON <zer0sec.xp@icloud.com>
Date:   Thu,  8 Oct 2026 18:33:11 +0100

feat(pentest): spec and implementation plan for the offensive toolkit

Diffstat:
Adocs/superpowers/plans/2026-10-08-pentest-toolkit.md | 503+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adocs/superpowers/specs/2026-10-08-pentest-toolkit-design.md | 379+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 882 insertions(+), 0 deletions(-)

diff --git a/docs/superpowers/plans/2026-10-08-pentest-toolkit.md b/docs/superpowers/plans/2026-10-08-pentest-toolkit.md @@ -0,0 +1,503 @@ +# Pentest Toolkit Implementation Plan + +> **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. + +**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. + +**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. + +**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. + +**Spec:** `docs/superpowers/specs/2026-10-08-pentest-toolkit-design.md` + +## Global Constraints + +- Every `modules/**/*.nix` is a flake-parts module; any path containing `/_` is **not** auto-imported (use for plain helper/derivation files). +- Modules reference each other **by name** through `self.nixosModules.*` / `self.homeModules.*`, never by path. +- Tool packages go to `environment.systemPackages`, never `home.packages` (spec D1 — `sudo nmap -sS` must work). +- Use native NixOS options where they exist: `programs.wireshark.enable`, `programs.proxychains.enable` (spec D5). +- 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`. +- System is `x86_64-linux`; the only host is `nixosConfigurations.nixos`. +- Every file opens with a `# modules/path/file.nix — <purpose>` comment block in the existing house style (see `modules/home/fan.nix`). +- Commit messages end with `Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>`. +- Verification command for any module task: `nix build .#checks.x86_64-linux.pentest-<cat>` then `nix flake check`. + +## Review Focus + +Five failure modes the spec implies but does not pin to a test. Each has a test added to the owning task. + +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. +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. +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`. +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`. +5. **`htbvpn up` while another profile is active** (Task 13). Must stop the running unit first; two concurrent `tun` devices must be impossible. + +--- + +### Task 1: Category harness, options tree, and `core` + +**Files:** +- Create: `modules/features/pentest/_sets.nix` (helper, not auto-imported) +- Create: `modules/features/pentest/options.nix` +- Create: `modules/features/pentest/core.nix` +- Modify: `modules/hosts/laptop/configuration.nix` (import `pentest-options` + `pentest-core`, set `daemon.pentest.enable = true`) + +**Interfaces:** +- Consumes: nothing. +- Produces: + - `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>`. + - `daemon.pentest.enable`, `daemon.pentest.<name>.enable`, `daemon.pentest.payloads.windowsArches`, `daemon.pentest.htb.vpnDir`, `daemon.pentest.htb.promptTarget`. + - Env: `$PAYLOADS`, `$WORDLISTS` (set by `core.nix`, values filled in by Tasks 2 and 12). + +- [ ] **Step 1: Write the failing check** + +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: + +```nix +# core.nix declares, via mkCategory: +# name = "core"; +# packages = pkgs: with pkgs; [ ncat socat samba krb5 openldap sshpass rlwrap ]; +# expectedBins = [ "ncat" "socat" "smbclient" "kinit" "ldapsearch" "sshpass" ]; +``` + +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. + +- [ ] **Step 2: Run the check to verify it fails** + +Run: `nix build .#checks.x86_64-linux.pentest-core -L` +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). + +- [ ] **Step 3: Implement `mkCategory` in `modules/features/pentest/_sets.nix`** + +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). + +- [ ] **Step 4: Implement `options.nix` and `core.nix`** + +`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. + +- [ ] **Step 5: Wire into the host** + +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. + +- [ ] **Step 6: Verify the check passes and the system builds** + +Run: `nix build .#checks.x86_64-linux.pentest-core -L && nix build .#nixosConfigurations.nixos.config.system.build.toplevel` +Expected: both succeed. + +- [ ] **Step 7: Commit** + +```bash +git add modules/features/pentest/ modules/hosts/laptop/configuration.nix +git commit -m "feat(pentest): category harness, option tree, and core module" +``` + +--- + +### Task 2: Wordlists + +**Files:** +- Create: `modules/features/pentest/wordlists.nix` + +**Interfaces:** +- Consumes: `mkCategory` (Task 1). +- Produces: `$WORDLISTS` pointing at a directory containing `rockyou.txt` **decompressed**; `flake.pentestPackages.wordlists`. + +- [ ] **Step 1: Write the failing check** + +The check asserts the decompressed file exists and is plain text: + +```nix +expectedPaths = [ "$WORDLISTS/rockyou.txt" "$WORDLISTS/seclists" ]; +# plus: test "$(head -c2 $WORDLISTS/rockyou.txt)" != "$(printf '\037\213')" # not gzip magic +``` + +- [ ] **Step 2: Run the check to verify it fails** + +Run: `nix build .#checks.x86_64-linux.pentest-wordlists -L` +Expected: FAIL — attribute does not exist. + +- [ ] **Step 3: Implement `wordlists.nix`** + +`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`. + +- [ ] **Step 4: Verify** + +Run: `nix build .#checks.x86_64-linux.pentest-wordlists -L` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add modules/features/pentest/wordlists.nix +git commit -m "feat(pentest): wordlists with decompressed rockyou and \$WORDLISTS" +``` + +--- + +### Task 3: Offensive Python environment and impacket aliases + +**Files:** +- Create: `modules/features/pentest/python.nix` +- Create: `modules/features/pentest/_aliases.nix` (helper) + +**Interfaces:** +- Consumes: `mkCategory` (Task 1). +- Produces: + - `pentest-python` — a `python3.withPackages` interpreter with every offensive library importable. + - `mkSuffixFreeAliases :: { package, reserved : [String], prefix : String } -> derivation` — symlinks `bin/foo.py` to `bin/foo`, **except** names in `reserved`, which become `bin/<prefix>-foo`. + - `impacket` — a discovery command: no args lists all scripts via fzf with `--help` preview; `impacket <name>` execs that script. + +- [ ] **Step 1: Write the failing collision test** + +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: + +```nix +# checks.pentest-python asserts: +# 1. `pentest-python -c "import impacket, certipy, pypykatz, bloodyad, lsassy, scapy, pwn"` exits 0 +# 2. `secretsdump --help` and `GetUserSPNs --help` exit 0 (aliased) +# 3. `command -v split` resolves into coreutils, NOT impacket (collision avoided) +# 4. `command -v ping` resolves into iputils, NOT impacket +# 5. `impacket-split --help` and `impacket-net --help` exit 0 (prefixed instead) +# 6. the alias derivation's build FAILS if a new upstream script +# collides with a reserved name that is not in the prefix list +``` + +- [ ] **Step 2: Run the check to verify it fails** + +Run: `nix build .#checks.x86_64-linux.pentest-python -L` +Expected: FAIL — attribute does not exist. + +- [ ] **Step 3: Implement `mkSuffixFreeAliases` in `_aliases.nix`** + +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. + +- [ ] **Step 4: Implement `python.nix`** + +`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. + +- [ ] **Step 5: Verify** + +Run: `nix build .#checks.x86_64-linux.pentest-python -L` +Expected: PASS — including assertions 3 and 4, which prove `split` and `ping` still resolve to coreutils and iputils. + +- [ ] **Step 6: Commit** + +```bash +git add modules/features/pentest/python.nix modules/features/pentest/_aliases.nix +git commit -m "feat(pentest): offensive python env, impacket aliases with collision guard" +``` + +--- + +### Task 4: `recon` + +**Files:** Create `modules/features/pentest/recon.nix` + +**Interfaces:** Consumes `mkCategory`. Produces `flake.pentestPackages.recon`. + +- [ ] **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" ]`. +- [ ] **Step 2: Run to verify it fails** — `nix build .#checks.x86_64-linux.pentest-recon -L`, expect attribute-missing. +- [ ] **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. +- [ ] **Step 4: Verify** — same command, expect PASS. +- [ ] **Step 5: Commit** — `feat(pentest): recon and scanning module` + +--- + +### Task 5: `ad` + +**Files:** Create `modules/features/pentest/ad.nix` + +**Interfaces:** Consumes `mkCategory`, Task 3's `pentest-python`. Produces `flake.pentestPackages.ad`. + +- [ ] **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. +- [ ] **Step 2: Run to verify it fails.** +- [ ] **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. +- [ ] **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). +- [ ] **Step 5: Commit** — `feat(pentest): active directory module` + +--- + +### Task 6: `web` + +**Files:** Create `modules/features/pentest/web.nix` + +- [ ] **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. +- [ ] **Step 2: Run to verify it fails.** +- [ ] **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. +- [ ] **Step 4: Verify** — expect PASS. +- [ ] **Step 5: Commit** — `feat(pentest): web application module` + +--- + +### Task 7: `pivot`, `crack`, `shells` + +Grouped: three identical-shape package lists over the Task 1 helper. The only non-trivial part is `programs.proxychains.enable`. + +**Files:** Create `modules/features/pentest/{pivot,crack,shells}.nix` + +- [ ] **Step 1: Write the three failing checks** + - pivot: `[ "ligolo-proxy" "ligolo-agent" "chisel" "socat" "proxychains4" "sshuttle" "gost" "frpc" "iodine" "stunnel" "wg" "openvpn" ]` + - crack: `[ "hashcat" "john" "hydra" "medusa" "crowbar" "crunch" "cewl" "hashid" "nth" ]` + - shells: `[ "msfconsole" "msfvenom" "pwsh" "pwncat" "rlwrap" "updog" "miniserve" "xfreerdp" "upx" ]` +- [ ] **Step 2: Run all three to verify they fail.** +- [ ] **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. +- [ ] **Step 4: Verify** — all three checks PASS. +- [ ] **Step 5: Commit** — `feat(pentest): pivoting, cracking, and shell modules` + +--- + +### Task 8: Optional categories (`dfir`, `reversing`, `wireless`, `cloud`, `osint`, `mobile`) + +Grouped: six package lists, all `default = false`. + +**Files:** Create `modules/features/pentest/{dfir,reversing,wireless,cloud,osint,mobile}.nix` + +- [ ] **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. +- [ ] **Step 2: Run to verify they fail.** +- [ ] **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. +- [ ] **Step 4: Verify** — `nix flake check` passes with all six disabled. +- [ ] **Step 5: Commit** — `feat(pentest): dfir, reversing, wireless, cloud, osint, mobile modules` + +--- + +### Task 9: BloodHound CE with neo4j and postgresql + +Separate task: the only category needing stateful services, so a reviewer could reject this while accepting Task 5. + +**Files:** Create `modules/features/pentest/bloodhound.nix` + +**Interfaces:** Produces `flake.nixosModules.pentest-bloodhound`; a `bloodhound-up` / `bloodhound-down` pair so the databases are not running on every boot. + +- [ ] **Step 1: Write the failing test** + +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. + +- [ ] **Step 2: Run to verify it fails** — `nix build .#checks.x86_64-linux.pentest-bloodhound-vm -L`, expect attribute-missing. +- [ ] **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`. +- [ ] **Step 4: Verify** — VM test PASSES. +- [ ] **Step 5: Commit** — `feat(pentest): bloodhound-ce with neo4j and postgresql` + +--- + +### Task 10: GUI tools and desktop entries + +**Files:** Create `modules/features/pentest/gui.nix` + +- [ ] **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. +- [ ] **Step 2: Run to verify it fails.** +- [ ] **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. +- [ ] **Step 4: Verify** — check PASSES; `nix flake check` passes with both `gui` and `wireless` enabled (proving no option conflict). +- [ ] **Step 5: Commit** — `feat(pentest): gui tools with desktop entries` + +--- + +### Task 11: Missing-tool derivations + +**Files:** +- Create: `modules/features/pentest/_pkgs/default.nix` (an attrset the categories consume) +- Create: `modules/features/pentest/_pkgs/{krbrelayx,sprayhound,velociraptor,ropper,lse,peass,sharpcollection,potato,printer}.nix` +- Modify: `ad.nix`, `dfir.nix`, `reversing.nix` to pull from `_pkgs` and extend their `expectedBins` + +**Interfaces:** +- Produces: `pentestPkgs :: attrset` of derivations, consumed by Task 12's payload tree and by the category modules. + +- [ ] **Step 1: Write the failing check** + +`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`. + +- [ ] **Step 2: Run to verify it fails.** +- [ ] **Step 3: Obtain real hashes, then implement** + +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. + +- [ ] **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`. +- [ ] **Step 5: Verify** — `nix build .#checks.x86_64-linux.pentest-pkgs -L` PASSES, and the three amended category checks still pass. +- [ ] **Step 6: Commit** — `feat(pentest): derivations for tools absent from nixpkgs` + +--- + +### Task 12: Payload tree, cross-compiled binaries, and `payload-serve` + +**Files:** +- Create: `modules/features/pentest/payloads.nix` +- Create: `modules/features/pentest/_payloads.nix` (the tree-assembly function) + +**Interfaces:** +- Consumes: `pentestPkgs` (Task 11). +- Produces: `$PAYLOADS` (overriding Task 1's placeholder with `lib.mkForce`); `payload-serve` with flags `[port]`, `--smb`, `--list`. + +- [ ] **Step 1: Write the failing checks** + +Two assertions, the second being Review Focus #2: + +```nix +# 1. structure: every path in the spec C3 tree exists, and +# file $PAYLOADS/windows/amd64/creds/mimikatz-2.2.0.exe contains "PE32+" +# file $PAYLOADS/linux/amd64/agents/ligolo-agent contains "ELF 64-bit" +# file $PAYLOADS/linux/arm64/agents/ligolo-agent contains "ARM aarch64" +# 2. payload-serve refuses to start when no tun interface exists: +# ip link show tun0 absent => exit 2, message naming htbvpn, +# and NOTHING listening on the requested port afterwards +``` + +- [ ] **Step 2: Run to verify it fails.** +- [ ] **Step 3: Implement the cross-builds** + +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. + +- [ ] **Step 4: Implement `payload-serve`** + +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`. + +- [ ] **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. +- [ ] **Step 6: Commit** — `feat(pentest): multi-arch payload tree and payload-serve` + +--- + +### Task 13: `htbvpn` + +**Files:** +- Create: `modules/features/pentest/vpn.nix` + +**Interfaces:** +- Produces: `htbvpn@.service` (systemd template); `htbvpn` with subcommands `list|up|down|status`; `htbip`. + +- [ ] **Step 1: Write the failing test** + +A `nixosTest` with a dummy OpenVPN profile, asserting the Review Focus #5 behaviour: + +``` +1. htbvpn up profileA => htbvpn@profileA active, tun0 has an address +2. htbvpn up profileB => profileA STOPPED first; exactly one tun device exists +3. htbvpn down => no htbvpn@* unit active, tun0 gone +4. htbvpn up missingProfile => exit 2, lists available profiles, no unit started +``` + +- [ ] **Step 2: Run to verify it fails.** +- [ ] **Step 3: Implement** + +`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). + +- [ ] **Step 4: Verify** — the VM test PASSES, all four assertions. +- [ ] **Step 5: Commit** — `feat(pentest): htbvpn via systemd template unit` + +--- + +### Task 14: `htbtime` + +**Files:** +- Create: `modules/features/pentest/time.nix` + +**Interfaces:** +- Produces: `htb-time` (root helper, `environment.systemPackages` + scoped NOPASSWD rule); subcommands `<target>|off|status`. + +- [ ] **Step 1: Write the failing test** + +A `nixosTest` covering the round trip and Review Focus #3: + +``` +1. record timedatectl output +2. htb-time 10.0.0.1 => NTP disabled, timesyncd inactive, state file written +3. htb-time off => timedatectl output matches step 1 exactly +4. rm the state file; htb-time off + => NTP ENABLED (the NixOS default), exit 0, warning printed + -- must never leave the clock unmanaged +5. htb-time <unreachable> + => exit nonzero, warning naming `htbtime off`, NTP still disabled + (deliberate: spec C6 says do not silently half-apply) +``` + +- [ ] **Step 2: Run to verify it fails.** +- [ ] **Step 3: Implement** + +`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. + +- [ ] **Step 4: Verify** — the VM test PASSES, all five assertions, especially 4. +- [ ] **Step 5: Commit** — `feat(pentest): htbtime, conflict-aware clock skew control` + +--- + +### Task 15: `htbtarget`, cross-terminal state, and engagement scaffolding + +**Files:** +- Create: `modules/home/htb.nix` +- Create: `modules/home/htb-shell.nix` +- Create: `modules/features/pentest/hosts.nix` (the `/etc/hosts` root helper) +- Modify: `modules/home/default.nix` (import `htb` and `htb-shell` by name) + +**Interfaces:** +- Consumes: `htbvpn`/`htbip` (Task 13), `htbtime` (Task 14). +- Produces: `htbtarget [IP [FQDN]] | clear`; exported `$TARGET`, `$RHOST`, `$IP`, `$BOX`; `htb new <box>`; `htb ls`; `htb-hosts` root helper. + +- [ ] **Step 1: Write the failing tests** + +Two parts. First the cross-terminal guarantee from spec C4: + +``` +1. shell A: htbtarget 10.10.11.5 +2. shell B (already running, new prompt): $TARGET == 10.10.11.5 +``` + +Second, Review Focus #4 — validation before the privileged helper runs: + +``` +3. htbtarget "10.10.11.5" => accepted +4. htbtarget "not an ip" => exit 2, /etc/hosts unchanged +5. htbtarget "10.10.11.5" $'x\n1.2.3.4 evil' => exit 2, /etc/hosts unchanged +6. htbtarget 10.10.11.5 dc01.vintage.htb => exactly one marked block in + /etc/hosts; running twice does + not duplicate it +7. htbtarget clear; htbtime => exit 2, message naming `htbtarget` + as the command that sets a target +``` + +- [ ] **Step 2: Run to verify they fail.** +- [ ] **Step 3: Implement the state file and shell hook** + +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`. + +- [ ] **Step 4: Implement validation and the `/etc/hosts` helper** + +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`. + +- [ ] **Step 5: Implement `htb new` and `htb ls`** + +`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. + +- [ ] **Step 6: Verify** — all six assertions pass; confirm assertion 2 by hand in two live terminals. +- [ ] **Step 7: Commit** — `feat(pentest): htbtarget cross-terminal state and engagement scaffolding` + +--- + +### Task 16: Cheat card, `pentest-update`, devshells, and docs + +**Files:** +- Create: `modules/home/cheats/pentest.md` +- Modify: `modules/home/cheats.nix` (add `"pentest"` to `cards`) +- Create: `modules/features/pentest/devshells.nix` +- Create: `modules/features/pentest/update.nix` +- Modify: `README.md` (a `Pentest` row in the "What runs" table and a tree entry) + +- [ ] **Step 1: Write the failing checks** + +`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. + +- [ ] **Step 2: Run to verify they fail.** +- [ ] **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. +- [ ] **Step 4: Implement `devshells.nix`** — `devShells.pentest` as the union of every `flake.pentestPackages.*`, plus the per-category shells already produced by `mkCategory`. +- [ ] **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. +- [ ] **Step 6: Verify** — all three checks PASS; `nix flake check` passes whole-flake. +- [ ] **Step 7: Commit** — `feat(pentest): cheat card, devshells, pentest-update, README` + +--- + +## Done when + +- `nix flake check` passes with every default-on category enabled. +- `sudo nmap -sS` works (spec D1). +- `split` and `ping` still resolve to coreutils and iputils (Review Focus #1). +- `payload-serve` refuses to bind without a tun device (Review Focus #2). +- `htbtime off` restores the clock from a missing state file (Review Focus #3). +- `htbtarget` rejects malformed input before touching `/etc/hosts` (Review Focus #4). +- `htbvpn up` never leaves two tun devices (Review Focus #5). +- `$TARGET` set in one terminal appears in another at its next prompt. diff --git a/docs/superpowers/specs/2026-10-08-pentest-toolkit-design.md b/docs/superpowers/specs/2026-10-08-pentest-toolkit-design.md @@ -0,0 +1,379 @@ +# Pentest toolkit for NixDaemon — design + +**Date:** 2026-10-08 +**Status:** awaiting review +**Author:** daemon-sec, with Claude + +## Intent + +Turn this laptop into a Kali-equivalent offensive workstation, declaratively, +for **HackTheBox CPTS prep**. Everything comes from the flake: no `curl | bash`, +no tools living outside the store except the ones that must be mutable +(VPN profiles, loot). + +Success criteria: + +1. `nh os switch` is the only install step. A fresh machine reaches the same kit. +2. Tools are grouped into independently toggleable modules (`daemon.pentest.*`), + so a category can be dropped in one line. +3. Tools are reachable with no ceremony: `nxc`, `ffuf`, `secretsdump` work in any + terminal, including under `sudo`. +4. Windows/Linux/macOS agent binaries and offensive payloads are staged in one + predictable tree, servable to a target in one command. +5. The HTB loop — connect VPN, set target, fix clock skew, enumerate — is + driven by a handful of commands that share state across terminals. + +Non-goals: C2 infrastructure beyond what ships in nixpkgs; anything targeting +hosts outside HTB/authorised labs; Kali's full 600-package catalogue (curated +instead); a second non-NixOS VM. + +## Decisions + +These were delegated to me with "whatever is best for NixOS". Recorded with +reasoning so they can be revisited. + +### D1 — Tools install to `environment.systemPackages`, not `home.packages` + +**The single most important NixOS-specific decision.** Half this toolkit needs +root: `nmap -sS` (raw sockets), `responder`, `mitm6`, `bettercap`, `tcpdump`, +`masscan`. home-manager packages are on the *user's* PATH only, so +`sudo nmap -sS` would fail with `command not found` — the classic +Kali-refugee-on-NixOS trap. System packages behave like Kali's `/usr/bin`. + +Consequence: modules live in `modules/features/pentest/` as +`flake.nixosModules.pentest-*`. Only genuinely user-scoped things (the `htb*` +commands, the cheat card, shell integration) are home modules. + +### D2 — Everything on PATH, per-category toggles, devshells as a bonus + +Friction matters more than PATH tidiness during CPTS. 1.8TB free, and the kit +comes from the binary cache rather than compiling. Toggles are the escape hatch. + +`perSystem.devShells.pentest` (plus one per category) is declared anyway — ~10 +lines, and it makes the kit portable: `nix develop github:…#pentest` on any +machine with Nix. + +### D3 — Missing tools become pinned Nix derivations; a script re-pins them + +~20 tools aren't in nixpkgs. Each gets a derivation in `_pkgs/` with a pinned +`rev`/`hash`, so the toolkit rolls back with the system generation. +`pentest-update` re-pins to latest on demand. + +Where a tool declares Windows support in nixpkgs, build it **from source** with +`pkgsCross.mingwW64` rather than downloading a release blob. Verified available +for `mimikatz`, `ligolo-ng`, `netexec`. Genuine provenance, no trusting strangers. + +Prebuilt .NET binaries (SharpCollection, Ghostpack, potato family) are the +exception: building .NET offline in Nix is brittle, so those are pinned release +assets with recorded hashes. + +### D4 — Defaults: CPTS core + heavy GUI on; the rest written but off + +On: `recon ad bloodhound web pivot crack shells wordlists payloads python gui`. +Off: `dfir reversing wireless cloud osint mobile`. +Keeps the first build finishable in one sitting; each is one line to enable. + +### D5 — Native NixOS options over bare packages, wherever they exist + +Not cosmetic — on NixOS the package alone is often *unusable*: + +| Option | Why the package alone is not enough | +|---|---| +| `programs.wireshark.enable` | Creates the `wireshark` group and the `dumpcap` capability wrapper. Without it you cannot capture without full root. | +| `programs.proxychains.enable` | Manages `/etc/proxychains.conf`. `/etc` is read-only on NixOS, so the Kali habit of editing that file by hand simply fails. This module is what makes `proxychains` work at all. | +| `security.wrappers` | Capabilities (`cap_net_raw`) instead of setuid, for raw-socket tools. | +| `services.neo4j` + `services.postgresql` | BloodHound CE needs both; there is no `services.bloodhound` in nixpkgs, so this is wired by hand. | + +`rockyou.txt` ships gzipped in the store, so `wordlists.nix` produces a +decompressed derivation and exports `$WORDLISTS`. + +## Architecture + +Dendritic, matching the existing repo: every file under `modules/` is a +flake-parts module declaring the outputs it owns, cross-referenced by **name** +through `self`, never by path. Switches live in one `options.nix`; each category +is gated with `lib.mkIf`. + +```text +modules/features/pentest/ +├── options.nix daemon.pentest.{enable,<cat>.enable} + assertions +├── core.nix always-on base + $PAYLOADS/$WORDLISTS env + /etc/hosts helper +├── python.nix the combined offensive Python env + suffix-free aliases +├── recon.nix ad.nix bloodhound.nix web.nix pivot.nix crack.nix +├── shells.nix wordlists.nix payloads.nix gui.nix +├── dfir.nix reversing.nix wireless.nix cloud.nix osint.nix mobile.nix +├── vpn.nix htbvpn@.service template + scoped sudo rule +├── time.nix htb-time privileged helper + scoped sudo rule +├── devshells.nix perSystem.devShells.pentest and per-category +└── _pkgs/ ~20 pinned derivations (paths with /_ are skipped by import-tree) + +modules/home/ +├── htb.nix htbtarget htbvpn htbtime htbip htb new — the user-facing CLI +├── htb-shell.nix zsh precmd integration: $TARGET/$BOX live in every terminal +└── cheats/pentest.md `pentest-cheat`, in the existing cheat-card style +``` + +### Option tree + +```nix +daemon.pentest = { + enable = mkEnableOption "the offensive toolkit"; # master switch, default false + <category>.enable = ... ; # per D4 defaults + payloads.windowsArches = [ "amd64" "x86" ]; # which .exe variants to build + htb.vpnDir = "~/.config/htb/vpn"; # where .ovpn profiles live + htb.promptTarget = true; # show $TARGET in the prompt +}; +``` + +Every category also implies `core`, so `$PAYLOADS`/`$WORDLISTS` always exist. +An assertion fires if a category is enabled while `daemon.pentest.enable` is false. + +## Components + +### C1 — Tool modules + +Curated from what was verified present in the pinned nixpkgs (2026-10-08). + +- **recon** — nmap masscan rustscan naabu fscan nbtscan enum4linux-ng snmpcheck + onesixtyone thc-ipv6 dnsrecon subfinder amass httpx dnsx katana gowitness + eyewitness whatweb +- **ad** — netexec impacket certipy bloodhound-py kerbrute responder mitm6 + coercer donpapi adidnsdump ldapdomaindump smbmap evil-winrm pypykatz bloodyad + lsassy dploot masky pywerview krb5 openldap samba(smbclient) sshpass + · custom: krbrelayx sprayhound +- **bloodhound** — bloodhound-ce with neo4j + postgresql wired up +- **web** — ffuf gobuster feroxbuster dirb nikto sqlmap commix wfuzz nuclei + nuclei-templates wpscan joomscan dalfox arjun jwt-cli jwt-hack mitmproxy + burpsuite zap +- **pivot** — ligolo-ng chisel socat proxychains-ng(native opt) sshuttle gost + frp iodine pingtunnel stunnel wireguard-tools openvpn +- **crack** — hashcat hashcat-utils john hydra medusa crowbar crunch cewl hashid + name-that-hash +- **shells** — metasploit msfpc powershell pwncat rlwrap updog miniserve + freerdp remmina rdesktop upx +- **wordlists** — seclists wordlists rockyou(decompressed) exploitdb +- **dfir** — volatility3 sleuthkit autopsy yara capa chainsaw hayabusa exiftool + foremost testdisk chntpw binwalk · custom: velociraptor +- **reversing** — ghidra radare2 rizin cutter gef pwntools one_gadget pwninit + checksec flare-floss retdec patchelf +- **wireless** — aircrack-ng hcxtools bettercap wireshark(native opt) termshark tcpdump +- **cloud** — awscli2 azure-cli google-cloud-sdk kubectl trivy kube-hunter pacu +- **osint** — theharvester recon-ng sherlock maigret holehe +- **mobile** — apktool jadx dex2jar frida-tools objection scrcpy + +### C2 — The Python toolkit (`python.nix`) + +Answers "a script built into the nixos config that can set up all the python tools". + +Three parts: + +1. **One combined interpreter.** `pentest-python` = `python3.withPackages` with + every offensive library: impacket certipy pywerview dploot masky bloodhound + ldapdomaindump pypykatz bloodyad lsassy minikerberos aiowinreg ldap3 dnspython + scapy pwntools pycryptodomex requests rich. Any offensive script you download + runs with `pentest-python foo.py` and its imports already resolve — no venv, + no `pip install`. + + *Risk:* a version conflict inside one env (impacket is built against + python3.14 here). Mitigation: if `withPackages` fails to resolve, fall back to + per-tool standalone packages and a slimmer shared env. Decided at build time, + not guessed. + +2. **Suffix-free aliases.** impacket installs 70 scripts as `secretsdump.py`, + `psexec.py`, `GetUserSPNs.py`. A `mkSuffixFreeAliases` helper globs a package's + `bin/*.py` and symlinks `secretsdump` → `secretsdump.py`, so both spellings + work. Applied to impacket and any other `.py`-suffixed toolkit. + +3. **A discovery command.** `impacket` with no arguments lists all 70 scripts + with fzf selection and `--help` preview; `impacket <name>` runs one. Same for + `pentest-python --list`. + +### C3 — Payload tree (`payloads.nix`) + +A derivation of symlinks at a stable path, exported as `$PAYLOADS`: + +```text +$PAYLOADS/ +├── windows/{amd64,x86}/ +│ ├── privesc/potato/ JuicyPotato JuicyPotatoNG RoguePotato SweetPotato +│ │ GodPotato PrintSpoofer SharpEfsPotato EfsPotato +│ │ LocalPotato DeadPotato +│ ├── privesc/printer/ SpoolSample SharpPrintNightmare PrintSpoofer +│ ├── privesc/ winPEASx64 winPEASx86 Seatbelt SharpUp +│ ├── ad/ Rubeus SharpHound Certify Whisker SharpView +│ │ ADCSPwn StandIn +│ ├── creds/ mimikatz-2.2.0 mimikatz-2.1.1 LaZagne SharpDPAPI +│ ├── agents/ ligolo-agent chisel nc +│ └── misc/ SharpCollection (the full upstream set) +├── linux/{amd64,arm64}/ ligolo-agent chisel linpeas pspy socat nc +├── macos/{amd64,arm64}/ ligolo-agent chisel +└── scripts/ + ├── ad/ PowerView PowerUp SharpView.ps1 Invoke-Mimikatz + │ PowerSploit nishang Invoke-Kerberoast + ├── printer/ printerbug.py CVE-2021-1675.py Invoke-Nightmare.ps1 + └── privesc/ linux-smart-enumeration linux-exploit-suggester + PowerUp.ps1 Sherlock.ps1 +``` + +Multi-arch comes from `pkgsCross.mingwW64` (Windows) and `pkgsCross.aarch64-multiplatform` +(Linux arm64) over the same source. Versioned duplicates live here as distinct +filenames (`mimikatz-2.2.0.exe`, `mimikatz-2.1.1.exe`) rather than contending +for one PATH name — the reason payloads are files, not commands. + +`payload-serve [port]` HTTP-serves the tree bound to `tun0`; +`payload-serve --smb` runs the impacket `smbserver` equivalent; +`payload-serve --list` prints the tree. + +### C4 — Shared target state (`htbtarget`) + +The requirement is a target IP visible **across terminals**. Environment +variables cannot be pushed into already-running processes, so: + +- **State** lives in `$XDG_STATE_HOME/htb/` (`target`, `name`, `vpn`), surviving + reboots so a box can be resumed next day. +- **Propagation** is a zsh `precmd` hook that re-reads the state file and exports + `$TARGET`, `$RHOST`, `$IP`, `$BOX`. Every terminal picks up a change made in + any other terminal **at its next prompt**. + +```sh +htbtarget 10.10.11.5 vintage.htb # set IP (+ optional FQDN) +htbtarget # print current +htbtarget clear +``` + +With a FQDN, it also manages a marked block in `/etc/hosts` through a privileged +helper — necessary because Kerberos in CPTS AD boxes needs working name +resolution, and `/etc` is read-only on NixOS. + +**Honest limitation, to be documented in the cheat card:** a shell that is +mid-command when the target changes keeps the old value until its next prompt. +Interactive use is unaffected. + +### C5 — VPN control (`htbvpn`) + +Implemented as a **systemd service template** `htbvpn@.service`, not a +backgrounded process — it survives closing the terminal, logs to the journal, +and `down` reliably kills it. + +```sh +htbvpn list # profiles in ~/.config/htb/vpn, active one marked +htbvpn up lab_eu_free # sudo systemctl start htbvpn@lab_eu_free +htbvpn down # stop whatever is up +htbvpn status # unit state + tun0 address +htbip # just the tun0 IP, for pasting into payloads +``` + +Profiles are **not** Nix-managed: they are per-account files that rotate, and +`~/.config/htb/vpn/` is created for them. A sops-encrypted variant is available +later if wanted. A scoped `NOPASSWD` sudo rule covers only +`systemctl {start,stop,restart} htbvpn@*`. + +### C6 — Clock skew control (`htbtime`) + +The Kerberos-skew problem: AD authentication rejects a clock more than ~5 +minutes off the DC, and on NixOS `systemd-timesyncd` is enabled by default and +will **undo** any manual adjustment. + +`htbtime $TARGET`: +1. Record prior state (is `timesyncd` active? what does `timedatectl` report for + NTP? are `chrony`/`ntpd` running?) into `/var/lib/htb-time/state`. +2. Disable the conflicts: `timedatectl set-ntp false`, stop `chrony`/`ntpd` if up. +3. Sync to the target: `ntpdate -u <target>` (from `pkgs.ntp`, confirmed to ship + `ntpdate` and `sntp`), falling back to `chronyd -q` if the DC refuses NTP. +4. Report the resulting offset. + +`htbtime off` reads the state file and restores **exactly** what was there, then +re-syncs to real time. `htbtime status` shows the current offset and mode. + +Two safeguards: + +- **A skew warning.** A large offset breaks TLS certificate validation, so `nix`, + `git` and HTTPS may fail while skewed. `htbtime` says so explicitly rather than + letting it become a confusing debugging session. +- **Self-healing.** State lives in `/var/lib`, but a reboot re-applies the NixOS + default anyway, so a forgotten `htbtime off` cannot permanently break the clock. + +Root side is `htb-time`, a fixed store script with a scoped `NOPASSWD` rule for +`wheel` — mirroring the existing `fan-ec` / `fan` split in +`modules/hosts/laptop/fan-cli.nix` and `modules/home/fan.nix`. + +### C7 — Engagement scaffolding and cheat card + +```sh +htb new escape # ~/htb/escape/{nmap,loot,creds,www,exploit}/ + notes.md template, + # sets the target if given, cds in +htb ls # boxes worked, newest first +pentest-cheat # recon | ad | pivot | transfer | crack | web | dfir | htb +``` + +`pentest-cheat` follows the existing card mechanism exactly +(`modules/home/cheats/*.md` + `cheat-render`), so it joins `nix-cheat`, +`gpg-cheat` and `niri-cheat` with no new machinery. + +### C8 — `pentest-update` + +Re-pins every `_pkgs/` derivation to its latest upstream `rev`/`hash` using +`nix-prefetch-url`/`nurl`, rewrites the hashes in place, and prints a diff for +review. Never auto-commits and never runs during a rebuild — pinning stays +deliberate. + +## Error handling + +- **Missing VPN profile** — `htbvpn up` lists what is available and exits 2. +- **No target set** — any command needing `$TARGET` says which command sets it. +- **`htbtime` without a reachable DC** — reports the failure and leaves NTP + *disabled* with a clear warning plus the `htbtime off` hint, rather than + silently half-applying. +- **Unbuildable `_pkgs` entry** (upstream retagged, asset renamed) — that one + derivation fails with the URL that broke; it must not take the rebuild down, so + payload entries are assembled so a single failure is diagnosable in isolation. +- **Python env conflict** — falls back to standalone packages (C2). + +## Testing + +No unit-test framework applies to a package set, so verification is build- and +behaviour-based, **per module as it lands**, not in one pass at the end: + +1. `nix flake check` and `nix build .#nixosConfigurations.nixos.config.system.build.toplevel` + after each module — eval errors and missing attributes surface immediately. +2. **Smoke test per category**: one `--version`/`--help` per tool, scripted, so a + package that installs but cannot run is caught. +3. **Payload tree**: assert every declared path exists and Windows artefacts + report as PE binaries (`file`), Linux ELF ones as ELF. +4. **`htbtarget` cross-terminal**: set in terminal A, confirm `$TARGET` appears in + terminal B at its next prompt. +5. **`htbvpn`**: unit starts, `tun0` gains an address, `down` removes it. +6. **`htbtime`**: capture `timedatectl` before, run `htbtime` against a test NTP + source, confirm NTP disabled and offset applied, run `htbtime off`, confirm + `timedatectl` matches the original state byte for byte. This round-trip is the + one that must not be hand-waved. +7. `sudo nmap -sS` and `sudo responder -h` — the D1 guarantee, explicitly tested. + +## Risks + +| Risk | Mitigation | +|---|---| +| First build is 40–60GB / a couple of hours | Mostly cache downloads. Phased: core + AD first, GUI last. | +| Upstream release assets move, breaking `_pkgs` | Hashes pinned; `pentest-update` re-pins; failures isolated per derivation. | +| `pkgsCross.mingwW64` fails for a given tool | Verified for mimikatz/ligolo-ng/netexec. Anything else falls back to a pinned release asset. | +| Combined Python env conflicts | C2 fallback to standalone packages. | +| Clock skew breaks TLS mid-engagement | Explicit warning + reboot self-heal (C6). | +| 200 new names shadow an existing command | Smoke test diffs `PATH` collisions against the pre-change profile and reports them. | + +## Phasing + +Each phase ends with a working, committed system. + +1. **Foundation** — `options.nix`, `core.nix`, `wordlists.nix`, `python.nix`. Proves the pattern. +2. **CPTS core** — `recon`, `ad`, `web`, `pivot`, `crack`, `shells`. +3. **HTB workflow** — `vpn.nix`, `time.nix`, `htb.nix`, `htb-shell.nix`. The commands. +4. **Payloads** — `_pkgs/` derivations, cross-builds, the staging tree, `payload-serve`. +5. **Heavy and optional** — `bloodhound`, `gui`, then `dfir`/`reversing`/`wireless`/`cloud`/`osint`/`mobile` (off by default). +6. **Polish** — `pentest-cheat`, `pentest-update`, `devshells.nix`, README. + +## Scope note + +Written for authorised lab use: HackTheBox CPTS preparation. The toolkit is +ordinary defensive-security coursework equipment; it carries no targeting +configuration and nothing here points at infrastructure the operator does not +own or have explicit permission to test.