NixDaemon

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

_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 }