_sets.nix (7575B)
1 # modules/features/pentest/_sets.nix — the category factory. 2 # 3 # Not a flake-parts module: the path contains `/_`, so import-tree skips it 4 # (flake.nix). Every category file imports it and calls mkCategory once. 5 # 6 # One category is declared in one place — its package list — and this derives 7 # the four things that list feeds: 8 # 9 # flake.lib.pentestPackages.<name> 10 # the list itself, as pkgs -> [package]. 11 # devshells.nix unions these. 12 # flake.nixosModules.pentest-<name> 13 # environment.systemPackages when the category 14 # is on. System, not home: half this toolkit 15 # needs root (`sudo nmap -sS`), and 16 # home-manager packages are not on root's PATH. 17 # perSystem.checks.pentest-<name> 18 # the smoke test: every name in expectedBins 19 # resolves on PATH. Catches the common failure 20 # where an attribute exists but its binary is 21 # called something else (netexec -> nxc, 22 # snmpcheck -> snmp-check, bloodyad -> bloodyAD). 23 # perSystem.devShells.pentest-<name> 24 # the same list, portable: `nix develop`. 25 # 26 # The category declares its own `daemon.pentest.<name>.enable`; options.nix 27 # holds only the master switch and the options no category owns. 28 { lib }: 29 { 30 name, 31 description, 32 packages, 33 expectedBins ? [ ], 34 # Binaries to actually RUN (with --help), not merely resolve. `command -v` 35 # cannot see a tool that installs and then dies on a missing python module or 36 # a bad interpreter — which is how `masky` shipped broken. Spec Testing #2 37 # asks for exactly this. Non-zero exit is tolerated (plenty of tools exit 1 38 # on --help); an import or loader error is not. 39 smokeBins ? [ ], 40 # Extra assertions for a category whose deliverable is not a binary (a 41 # wordlist tree, a payload tree). Takes { pkgs, lib }, returns shell appended 42 # to the check; fail with a non-zero exit and a message naming what is wrong. 43 checkScript ? (_: ""), 44 # Merged into the gated config, so a category that is off cannot turn on a 45 # NixOS service. Called with { config, pkgs, lib, user }; take what you need 46 # and end the pattern with `...`. 47 extraConfig ? (_: { }), 48 default ? true, 49 }: 50 { 51 flake.lib.pentestPackages.${name} = packages; 52 53 flake.nixosModules."pentest-${name}" = 54 # `user` comes from the nixosSystem's specialArgs (modules/hosts/laptop/ 55 # default.nix), the same way vpn.nix and htb.nix take it. A category whose 56 # extraConfig grants a group membership needs the username, and hardcoding 57 # it in the category would make the file host-specific. 58 { config, pkgs, lib, user, ... }: 59 let 60 cfg = config.daemon.pentest; 61 on = cfg.enable && cfg.${name}.enable; 62 in 63 { 64 # The default follows the MASTER switch: with daemon.pentest.enable off, 65 # every category defaults off too. Otherwise turning the toolkit off 66 # leaves 12 categories defaulted on and the assertion in options.nix 67 # fires, which made `enable = false` an eval error rather than simply 68 # "no toolkit" — and the spec gives the master switch default false. 69 # An explicit `daemon.pentest.<cat>.enable = true` still wins, and the 70 # assertion still catches that genuine contradiction. 71 options.daemon.pentest.${name}.enable = lib.mkEnableOption description // { 72 default = cfg.enable && default; 73 }; 74 75 config = lib.mkIf on ( 76 lib.mkMerge [ 77 # Only what exists for this machine's platform: on aarch64 a few 78 # tools are x86-only (and fail evaluation outright). On x86_64 every 79 # package passes, so this filters nothing there. 80 { 81 environment.systemPackages = builtins.filter 82 (lib.meta.availableOn pkgs.stdenv.hostPlatform) 83 (packages pkgs); 84 } 85 (extraConfig { inherit config pkgs lib user; }) 86 ] 87 ); 88 }; 89 90 perSystem = 91 { pkgs, ... }: 92 { 93 checks."pentest-${name}" = pkgs.runCommand "pentest-${name}-check" 94 { 95 nativeBuildInputs = packages pkgs; 96 passthru.expectedBins = expectedBins; 97 } 98 '' 99 # The category's OWN binaries first. nativeBuildInputs also puts 100 # every propagated dependency's bin/ on PATH, which is how a 101 # python3.12 pywerview from another package's closure shadowed the 102 # python3.14 one this category actually installs — and made a working 103 # tool look broken. environment.systemPackages installs exactly this 104 # list, so this is the faithful PATH. 105 export PATH=${lib.makeBinPath (packages pkgs)}:$PATH 106 107 missing="" 108 for b in ${lib.escapeShellArgs expectedBins}; do 109 command -v "$b" >/dev/null 2>&1 || missing="$missing $b" 110 done 111 if [ -n "$missing" ]; then 112 echo "pentest-${name}: expected binaries not on PATH:$missing" >&2 113 echo " (the attribute built, but its binary is named something else)" >&2 114 exit 1 115 fi 116 # A writable HOME: several of these create a config directory on 117 # first run, and the build sandbox's HOME is /homeless-shelter. 118 # Without this, nxc fails with FileNotFoundError on ~/.nxc and the 119 # smoke test reports a defect that does not exist on a real machine. 120 export HOME=$(mktemp -d) 121 122 # NIX_PYTHONPATH as well as PYTHONPATH: nixpkgs' python setup hook 123 # uses the NIX_ one, and clearing only PYTHONPATH left the leak in 124 # place. Every python package in this check's 125 # nativeBuildInputs puts its modules on PYTHONPATH, so one tool's 126 # python3.12 impacket shadows another's python3.14 one and the tool 127 # dies on an import. That is an artifact of the check, not a defect — 128 # it produced false positives for pywerview and donpapi, and I 129 # briefly removed a working tool because of it. A user's shell has no 130 # PYTHONPATH, so clearing it is also the faithful test. 131 for b in ${lib.escapeShellArgs smokeBins}; do 132 # NOT a variable called `out`: that is the derivation's own 133 # output path, and clobbering it makes the final redirect 134 # ambiguous. (Second time I have made this mistake in this repo.) 135 smoke_out=$(env -u PYTHONPATH -u PYTHONHOME -u NIX_PYTHONPATH \ 136 -u NIX_PYTHONPREFIX -u NIX_PYTHONEXECUTABLE \ 137 "$b" --help 2>&1 </dev/null | head -40 || true) 138 case "$smoke_out" in 139 *ModuleNotFoundError*|*"ImportError"*|*"No such file or directory"*|\ 140 *"command not found"*|*"cannot open shared object"*) 141 echo "pentest-${name}: '$b' is installed but cannot run:" >&2 142 printf '%s\n' "$smoke_out" | sed 's/^/ /' >&2 143 exit 1 ;; 144 esac 145 done 146 147 ${checkScript { inherit pkgs lib; }} 148 echo "pentest-${name}: ${toString (builtins.length expectedBins)} binaries ok" > $out 149 ''; 150 151 devShells."pentest-${name}" = pkgs.mkShell { 152 name = "pentest-${name}"; 153 packages = builtins.filter (lib.meta.availableOn pkgs.stdenv.hostPlatform) (packages pkgs); 154 }; 155 }; 156 }