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:
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
+ '';
+}