NixDaemon

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

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

feat(pentest): category harness, options, core and wordlists

mkCategory derives the gated nixosModule, devShell and a binary-resolution
smoke check from one package list per category, plus a checkScript hook for
categories whose deliverable is a tree rather than a binary. Tools install to
environment.systemPackages, not home.packages, so they work under sudo.

default.nix aggregates the categories so the host imports one name.
$WORDLISTS points at nixpkgs' own wordlists tree (seclists + rockyou, already
decompressed) rather than rebuilding it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Diffstat:
Amodules/features/pentest/_sets.nix | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/features/pentest/core.nix | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/features/pentest/default.nix | 24++++++++++++++++++++++++
Amodules/features/pentest/options.nix | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amodules/features/pentest/wordlists.nix | 45+++++++++++++++++++++++++++++++++++++++++++++
Mmodules/hosts/laptop/configuration.nix | 5+++++
6 files changed, 275 insertions(+), 0 deletions(-)

diff --git a/modules/features/pentest/_sets.nix b/modules/features/pentest/_sets.nix @@ -0,0 +1,91 @@ +# modules/features/pentest/_sets.nix — the category factory. +# +# Not a flake-parts module: the path contains `/_`, so import-tree skips it +# (flake.nix). Every category file imports it and calls mkCategory once. +# +# One category is declared in one place — its package list — and this derives +# the four things that list feeds: +# +# flake.pentestPackages.<name> the list itself, as pkgs -> [package]. +# devshells.nix unions these (Task 16). +# flake.nixosModules.pentest-<name> +# environment.systemPackages when the category +# is on. System, not home: half this toolkit +# needs root (`sudo nmap -sS`), and +# home-manager packages are not on root's PATH. +# perSystem.checks.pentest-<name> +# the smoke test: every name in expectedBins +# resolves on PATH. Catches the common failure +# where an attribute exists but its binary is +# called something else (netexec -> nxc, +# snmpcheck -> snmp-check, bloodyad -> bloodyAD). +# perSystem.devShells.pentest-<name> +# the same list, portable: `nix develop`. +# +# The category declares its own `daemon.pentest.<name>.enable`; options.nix +# holds only the master switch and the options no category owns. +{ lib }: +{ + name, + description, + packages, + expectedBins ? [ ], + # Extra assertions for a category whose deliverable is not a binary (a + # wordlist tree, a payload tree). Takes { pkgs, lib }, returns shell appended + # to the check; fail with a non-zero exit and a message naming what is wrong. + checkScript ? (_: ""), + # Merged into the gated config, so a category that is off cannot turn on a + # NixOS service. Takes the module args it needs; returns a config attrset. + extraConfig ? (_: { }), + default ? true, +}: +{ + flake.pentestPackages.${name} = packages; + + flake.nixosModules."pentest-${name}" = + { config, pkgs, lib, ... }: + let + cfg = config.daemon.pentest; + on = cfg.enable && cfg.${name}.enable; + in + { + options.daemon.pentest.${name}.enable = lib.mkEnableOption description // { + inherit default; + }; + + config = lib.mkIf on ( + lib.mkMerge [ + { environment.systemPackages = packages pkgs; } + (extraConfig { inherit config pkgs lib; }) + ] + ); + }; + + perSystem = + { pkgs, ... }: + { + checks."pentest-${name}" = pkgs.runCommand "pentest-${name}-check" + { + nativeBuildInputs = packages pkgs; + passthru.expectedBins = expectedBins; + } + '' + missing="" + for b in ${lib.escapeShellArgs expectedBins}; do + command -v "$b" >/dev/null 2>&1 || missing="$missing $b" + done + if [ -n "$missing" ]; then + echo "pentest-${name}: expected binaries not on PATH:$missing" >&2 + echo " (the attribute built, but its binary is named something else)" >&2 + exit 1 + fi + ${checkScript { inherit pkgs lib; }} + echo "pentest-${name}: ${toString (builtins.length expectedBins)} binaries ok" > $out + ''; + + devShells."pentest-${name}" = pkgs.mkShell { + name = "pentest-${name}"; + packages = packages pkgs; + }; + }; +} diff --git a/modules/features/pentest/core.nix b/modules/features/pentest/core.nix @@ -0,0 +1,49 @@ +# modules/features/pentest/core.nix — what every category assumes: the +# primitives for talking to a target and the two paths the rest of the toolkit +# hangs off. +# +# On whenever any category is on, so $PAYLOADS and $WORDLISTS always resolve. +# Their values here are placeholders; wordlists.nix and payloads.nix override +# them with lib.mkForce. +# +# `ncat` is not a top-level attribute — it ships inside nmap, which is why nmap +# appears here as well as in recon.nix (Nix dedupes the closure). +{ lib, ... }: +(import ./_sets.nix { inherit lib; }) { + name = "core"; + description = "pentest primitives: ncat, socat, smbclient, kerberos, ldap"; + + packages = pkgs: [ + pkgs.nmap # ncat: the reverse-shell workhorse (--ssl, -e) + pkgs.socat + pkgs.samba # smbclient, net + pkgs.krb5 # kinit, klist — CPTS AD boxes live on these + pkgs.openldap # ldapsearch + pkgs.sshpass + pkgs.rlwrap # line editing in a dumb reverse shell + pkgs.jq + pkgs.dnsutils # dig, nslookup + pkgs.iputils + pkgs.util-linux + ]; + + expectedBins = [ + "ncat" + "socat" + "smbclient" + "kinit" + "ldapsearch" + "sshpass" + "rlwrap" + "dig" + ]; + + extraConfig = { lib, ... }: { + # Placeholders: wordlists.nix (Task 2) and payloads.nix (Task 12) take these + # over with lib.mkForce. + environment.sessionVariables = { + PAYLOADS = lib.mkDefault "/run/current-system/sw/share/pentest/payloads"; + WORDLISTS = lib.mkDefault "/run/current-system/sw/share/pentest/wordlists"; + }; + }; +} diff --git a/modules/features/pentest/default.nix b/modules/features/pentest/default.nix @@ -0,0 +1,24 @@ +# modules/features/pentest/default.nix — the toolkit as one NixOS module +# (self.nixosModules.pentest), assembled from the named modules it imports. +# +# The host imports this one name, so adding a category means editing this file +# and nothing else — the same arrangement as modules/home/default.nix. +# +# daemon.pentest.enable the master switch +# daemon.pentest.<category>.enable per category, declared by the category +# itself (_sets.nix) +# +# Default-on categories are the ones CPTS exercises; DFIR, reversing, wireless, +# cloud, OSINT and mobile ship switched off (see each file's `default`). +{ self, ... }: +{ + flake.nixosModules.pentest = + { ... }: + { + imports = with self.nixosModules; [ + pentest-options # the master switch and the options no category owns + pentest-core # ncat, socat, smbclient, kerberos, ldap; $PAYLOADS/$WORDLISTS + pentest-wordlists # seclists, rockyou, searchsploit → $WORDLISTS + ]; + }; +} diff --git a/modules/features/pentest/options.nix b/modules/features/pentest/options.nix @@ -0,0 +1,61 @@ +# modules/features/pentest/options.nix — the toolkit's master switch and the +# options no single category owns. +# +# daemon.pentest.enable the whole toolkit (default false) +# daemon.pentest.<category>.enable declared by each category itself +# (_sets.nix), so adding a category +# touches one file, not two +# daemon.pentest.payloads.windowsArches which .exe variants to cross-build +# daemon.pentest.htb.vpnDir where .ovpn profiles are looked for +# daemon.pentest.htb.promptTarget show $TARGET in the prompt +# +# Scope: authorised lab use (HackTheBox CPTS prep). Nothing here points at any +# host; the operator supplies targets at runtime. +{ ... }: +{ + flake.nixosModules.pentest-options = + { lib, config, user, ... }: + let + cfg = config.daemon.pentest; + # Every category option declared by _sets.nix: an attrset carrying a + # boolean `enable`. `htb` and `payloads.windowsArches` are not categories + # and drop out of this filter on their own. + categories = lib.filterAttrs (_: v: lib.isAttrs v && v ? enable && lib.isBool v.enable) cfg; + enabledWhileOff = lib.attrNames (lib.filterAttrs (_: v: v.enable) categories); + in + { + options.daemon.pentest = { + enable = lib.mkEnableOption "the offensive security toolkit"; + + payloads.windowsArches = lib.mkOption { + type = lib.types.listOf (lib.types.enum [ "amd64" "x86" "arm64" ]); + default = [ "amd64" "x86" ]; + description = "Windows architectures to cross-build payload binaries for."; + }; + + htb.vpnDir = lib.mkOption { + type = lib.types.str; + default = "/home/${user}/.config/htb/vpn"; + description = '' + Directory scanned for OpenVPN profiles by `htbvpn list`. Deliberately + not Nix-managed: HTB profiles are per-account and rotate. + ''; + }; + + htb.promptTarget = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Show the current $TARGET in the shell prompt."; + }; + }; + + config.assertions = [ + { + assertion = cfg.enable || enabledWhileOff == [ ]; + message = + "daemon.pentest: ${lib.concatStringsSep ", " enabledWhileOff} " + + "enabled while daemon.pentest.enable is false. Set daemon.pentest.enable = true."; + } + ]; + }; +} diff --git a/modules/features/pentest/wordlists.nix b/modules/features/pentest/wordlists.nix @@ -0,0 +1,45 @@ +# modules/features/pentest/wordlists.nix — $WORDLISTS, the tree every fuzzer +# and cracker is pointed at. +# +# $WORDLISTS/rockyou.txt 14 M passwords, plain text +# $WORDLISTS/seclists/ the SecLists checkout (1.9 G) +# $WORDLISTS/nmap.lst wfuzz/ the smaller sets +# +# nixpkgs' `wordlists` already aggregates seclists, rockyou, nmap.lst and wfuzz +# under one share/wordlists, and already ships rockyou DECOMPRESSED — so this +# module points at that tree instead of rebuilding it. The check still asserts +# rockyou is plain text: it has shipped gzipped before, and a gzip magic number +# would make every `hashcat ... rockyou.txt` silently crack nothing. +# +# `wordlists` also provides `wordlists_path`, so the shell can find the tree +# without $WORDLISTS. +{ lib, ... }: +(import ./_sets.nix { inherit lib; }) { + name = "wordlists"; + description = "seclists, rockyou and the exploit-db archive"; + + packages = pkgs: [ + pkgs.wordlists # seclists + rockyou + nmap.lst + wfuzz, and `wordlists_path` + pkgs.exploitdb # `searchsploit` + ]; + + expectedBins = [ "wordlists_path" "searchsploit" ]; + + checkScript = { pkgs, lib }: '' + dir=${pkgs.wordlists}/share/wordlists + for p in rockyou.txt seclists nmap.lst; do + if [ ! -e "$dir/$p" ]; then + echo "pentest-wordlists: $dir/$p is missing" >&2 + exit 1 + fi + done + if [ "$(head -c2 "$dir/rockyou.txt" | od -An -tx1 | tr -d ' ')" = "1f8b" ]; then + echo "pentest-wordlists: rockyou.txt is gzipped, not plain text" >&2 + exit 1 + fi + ''; + + extraConfig = { pkgs, lib, ... }: { + environment.sessionVariables.WORDLISTS = lib.mkForce "${pkgs.wordlists}/share/wordlists"; + }; +} diff --git a/modules/hosts/laptop/configuration.nix b/modules/hosts/laptop/configuration.nix @@ -27,6 +27,7 @@ desktop-hyprland # Hyprland + Caelestia (NixOS side) desktop-niri # Niri + Noctalia: the wrapped package as the login session theme # Stylix: Rosé Pine Dawn GRUB and console (tuigreet) + pentest # the offensive toolkit (modules/features/pentest/default.nix) ]; # The desktops. Both are installed and chosen at login; set one to false to @@ -36,6 +37,10 @@ niri.enable = true; }; + # The offensive toolkit (modules/features/pentest/). Each category carries + # its own daemon.pentest.<category>.enable; see pentest-cheat. + daemon.pentest.enable = true; + # GRUB (themed Rosé Pine Dawn by modules/features/theme.nix) on the EFI # partition at /boot; kernels are copied there since / is a separate btrfs. boot.loader.grub = {