NixDaemon

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

commit 4e5eb0154d6efff753a26f603f4bef60846a0495
parent a1878b43da92116c29feb24802e01397c8a07282
Author: DAEMON <zer0sec.xp@icloud.com>
Date:   Thu,  8 Oct 2026 18:45:38 +0100

feat(pentest): offensive python env and impacket aliases with collision guard

Diffstat:
Amodules/features/pentest/_aliases.nix | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmodules/features/pentest/default.nix | 1+
Amodules/features/pentest/python.nix | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 152 insertions(+), 0 deletions(-)

diff --git a/modules/features/pentest/_aliases.nix b/modules/features/pentest/_aliases.nix @@ -0,0 +1,75 @@ +# modules/features/pentest/_aliases.nix — reach a toolkit's `.py` scripts by +# bare name, without taking a name a real tool already owns. +# +# impacket installs 70 example scripts as `secretsdump.py`, `psexec.py`, +# `GetUserSPNs.py`. Muscle memory from Kali wants both `secretsdump.py` and +# `secretsdump`, so this symlinks the suffix away — except that impacket also +# ships `split.py`, `ping.py`, `net.py`, `reg.py`, `services.py`, `attrib.py` +# and `smbclient.py`. Stripping those blindly is not a style question: +# environment.systemPackages merges every package into one profile with +# buildEnv, so `impacket-aliases/bin/net` and `samba/bin/net` is a hard +# collision and the whole system stops building. Before this guard existed the +# probe in python.nix failed exactly there. +# +# So: the `<prefix>-<name>` alias is always made, and the bare one only when no +# package in `reservedFrom` (plus `extraReserved`) already owns that name. The +# reserved set is read from those packages' bin/ at build time, so it tracks +# what is really installed instead of a list that rots. +# +# secretsdump.py -> secretsdump, impacket-secretsdump (free) +# net.py -> impacket-net (samba owns `net`) +# +# $out/collisions records every name that was held back, so `pentest-cheat` and +# a future reviewer can see what moved and why. +{ lib, pkgs }: +{ + package, + prefix, + reservedFrom ? [ ], + extraReserved ? [ ], +}: +pkgs.runCommand "${prefix}-aliases" + { + meta.description = "suffix-free aliases for ${package.pname or prefix}, collision-guarded"; + } + '' + mkdir -p $out/bin + : > $out/collisions + + reserved=$( + { + for d in ${lib.escapeShellArgs (map (p: "${p}/bin") reservedFrom)}; do + [ -d "$d" ] && ls -1 "$d" + done + ${lib.optionalString (extraReserved != [ ]) ''printf '%s\n' ${lib.escapeShellArgs extraReserved}''} + } | sort -u + ) + is_reserved() { printf '%s\n' "$reserved" | grep -qxF "$1"; } + + for f in ${package}/bin/*.py; do + [ -e "$f" ] || continue + base=$(basename "$f" .py) + + # Always reachable under the prefix, whatever happens to the bare name. + ln -s "$f" "$out/bin/${prefix}-$base" + + if is_reserved "$base"; then + echo "$base (held back: a real tool owns this name) -> ${prefix}-$base" >> $out/collisions + else + ln -s "$f" "$out/bin/$base" + fi + done + + # Belt and braces: assert no bare alias is a reserved name, so a future + # change to the logic above fails here rather than at system-rebuild time. + for b in $out/bin/*; do + n=$(basename "$b") + case "$n" in ${prefix}-*) continue ;; esac + if is_reserved "$n"; then + echo "${prefix}-aliases: bare alias '$n' shadows a real tool" >&2 + exit 1 + fi + done + + echo "${prefix}-aliases: $(ls -1 $out/bin | wc -l) aliases, $(wc -l < $out/collisions) held back" + '' diff --git a/modules/features/pentest/default.nix b/modules/features/pentest/default.nix @@ -19,6 +19,7 @@ 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 + pentest-python # one offensive python env; impacket by name ]; }; } diff --git a/modules/features/pentest/python.nix b/modules/features/pentest/python.nix @@ -0,0 +1,76 @@ +# modules/features/pentest/python.nix — one interpreter with every offensive +# library importable, plus impacket's 70 example scripts reachable by name. +{ lib, ... }: +(import ./_sets.nix { inherit lib; }) { + name = "python"; + description = "offensive python: impacket, certipy, pypykatz and friends"; + + packages = pkgs: + let + aliases = (import ./_aliases.nix { inherit lib pkgs; }) { + package = pkgs.python3Packages.impacket; + prefix = "impacket"; + reservedFrom = [ pkgs.coreutils pkgs.iputils pkgs.samba pkgs.util-linux ]; + extraReserved = [ "mimikatz" ]; + }; + in + [ + (pkgs.python3.withPackages (ps: with ps; [ + impacket certipy dploot masky bloodhound ldapdomaindump + pypykatz bloodyad lsassy minikerberos aiowinreg dnspython + scapy pwntools pycryptodomex requests rich + ])) + pkgs.python3Packages.impacket + # Standalone, not in the shared env: pywerview is the one tool that pulls + # ldap3-bleeding-edge-2.10.1.1338 while every other tool here pulls + # ldap3-2.9.1, and buildEnv cannot hold both. Its own wrapper carries the + # bleeding-edge copy, so `pywerview` works; only `pentest-python -c + # "import pywerview"` does not. This is the fallback spec C2 describes. + pkgs.python3Packages.pywerview + aliases + ]; + + expectedBins = [ "secretsdump.py" "secretsdump" "GetUserSPNs" "pywerview" ]; + + # Review Focus #1. environment.systemPackages merges every package into ONE + # profile with buildEnv, so two packages owning bin/split is a collision, not + # a PATH-order question. The probe below is that same merge: it fails to build + # if an alias claims a name a real tool already owns, and once it builds we + # assert the real tool is what the name resolves to. + checkScript = { pkgs, lib }: + let + probe = pkgs.buildEnv { + name = "pentest-python-profile-probe"; + paths = [ + ((import ./_aliases.nix { inherit lib pkgs; }) { + package = pkgs.python3Packages.impacket; + prefix = "impacket"; + reservedFrom = [ pkgs.coreutils pkgs.iputils pkgs.samba pkgs.util-linux ]; + extraReserved = [ "mimikatz" ]; + }) + pkgs.coreutils + pkgs.iputils + pkgs.samba + ]; + }; + in + '' + # The bare names must still be the real tools. + for n in split ping net smbclient; do + t=$(readlink -f ${probe}/bin/$n) + case "$t" in + *impacket*) + echo "pentest-python: '$n' resolves to impacket ($t)" >&2 + echo " a bare '$n' must stay the real tool; impacket's is impacket-$n" >&2 + exit 1 ;; + esac + done + # ...and impacket's versions must be reachable under the prefix. + for n in split ping net smbclient mimikatz; do + if [ ! -e ${probe}/bin/impacket-$n ]; then + echo "pentest-python: impacket-$n is missing" >&2 + exit 1 + fi + done + ''; +}