diff --git a/nixos/doc/manual/release-notes/rl-2411.section.md b/nixos/doc/manual/release-notes/rl-2411.section.md index 7b2d266601f6..b973311df5af 100644 --- a/nixos/doc/manual/release-notes/rl-2411.section.md +++ b/nixos/doc/manual/release-notes/rl-2411.section.md @@ -132,6 +132,8 @@ - [Gotenberg](https://gotenberg.dev), an API server for converting files to PDFs that can be used alongside Paperless-ngx. Available as [services.gotenberg](options.html#opt-services.gotenberg). +- [Suricata](https://suricata.io/), a free and open source, mature, fast and robust network threat detection engine. Available as [services.suricata](options.html#opt-services.suricata). + - [Playerctld](https://github.com/altdesktop/playerctl), a daemon to track media player activity. Available as [services.playerctld](option.html#opt-services.playerctld). - [MenhirLib](https://gitlab.inria.fr/fpottier/menhir/-/tree/master/coq-menhirlib) A support library for verified Coq parsers produced by Menhir. diff --git a/nixos/modules/services/networking/suricata/default.nix b/nixos/modules/services/networking/suricata/default.nix new file mode 100644 index 000000000000..5473fc913ebc --- /dev/null +++ b/nixos/modules/services/networking/suricata/default.nix @@ -0,0 +1,282 @@ +{ + config, + pkgs, + lib, + ... +}: +let + cfg = config.services.suricata; + pkg = cfg.package; + yaml = pkgs.formats.yaml { }; + inherit (lib) + mkEnableOption + mkPackageOption + mkOption + types + literalExpression + filterAttrsRecursive + concatStringsSep + strings + lists + mkIf + ; +in +{ + meta.maintainers = with lib.maintainers; [ felbinger ]; + + options.services.suricata = { + enable = mkEnableOption "Suricata"; + + package = mkPackageOption pkgs "suricata" { }; + + configFile = mkOption { + type = types.path; + visible = false; + default = pkgs.writeTextFile { + name = "suricata.yaml"; + text = '' + %YAML 1.1 + --- + ${builtins.readFile ( + yaml.generate "suricata-settings-raw.yaml" ( + filterAttrsRecursive (name: value: value != null) cfg.settings + ) + )} + ''; + }; + description = '' + Configuration file for suricata. + + It is not usual to override the default values; it is recommended to use `settings`. + If you want to include extra configuration to the file, use the `settings.includes`. + ''; + }; + + settings = mkOption { + type = types.submodule (import ./settings.nix { inherit config lib yaml; }); + example = literalExpression '' + vars.address-groups.HOME_NET = "192.168.178.0/24"; + outputs = [ + { + fast = { + enabled = true; + filename = "fast.log"; + append = "yes"; + }; + } + { + eve-log = { + enabled = true; + filetype = "regular"; + filename = "eve.json"; + community-id = true; + types = [ + { + alert.tagged-packets = "yes"; + } + ]; + }; + } + ]; + af-packet = [ + { + interface = "eth0"; + cluster-id = "99"; + cluster-type = "cluster_flow"; + defrag = "yes"; + } + { + interface = "default"; + } + ]; + af-xdp = [ + { + interface = "eth1"; + } + ]; + dpdk.interfaces = [ + { + interface = "eth2"; + } + ]; + pcap = [ + { + interface = "eth3"; + } + ]; + app-layer.protocols = { + telnet.enabled = "yes"; + dnp3.enabled = "yes"; + modbus.enabled = "yes"; + }; + ''; + description = "Suricata settings"; + }; + + enabledSources = mkOption { + type = types.listOf types.str; + # see: nix-shell -p suricata python3Packages.pyyaml --command 'suricata-update list-sources' + default = [ + "et/open" + "etnetera/aggressive" + "stamus/lateral" + "oisf/trafficid" + "tgreen/hunting" + "sslbl/ja3-fingerprints" + "sslbl/ssl-fp-blacklist" + "malsilo/win-malware" + "pawpatrules" + ]; + description = '' + List of sources that should be enabled. + Currently sources which require a secret-code are not supported. + ''; + }; + + disabledRules = mkOption { + type = types.listOf types.str; + # protocol dnp3 seams to be disabled, which causes the signature evaluation to fail, so we disable the + # dnp3 rules, see https://github.com/OISF/suricata/blob/master/rules/dnp3-events.rules for more details + default = [ + "2270000" + "2270001" + "2270002" + "2270003" + "2270004" + ]; + description = '' + List of rules that should be disabled. + ''; + }; + }; + + config = + let + captureInterfaces = + let + inherit (lists) unique optionals; + in + unique ( + map (e: e.interface) ( + (optionals (cfg.settings.af-packet != null) cfg.settings.af-packet) + ++ (optionals (cfg.settings.af-xdp != null) cfg.settings.af-xdp) + ++ (optionals ( + cfg.settings.dpdk != null && cfg.settings.dpdk.interfaces != null + ) cfg.settings.dpdk.interfaces) + ++ (optionals (cfg.settings.pcap != null) cfg.settings.pcap) + ) + ); + in + mkIf cfg.enable { + assertions = [ + { + assertion = (builtins.length captureInterfaces) > 0; + message = '' + At least one capture interface must be configured: + - `services.suricata.settings.af-packet` + - `services.suricata.settings.af-xdp` + - `services.suricata.settings.dpdk.interfaces` + - `services.suricata.settings.pcap` + ''; + } + ]; + + boot.kernelModules = mkIf (cfg.settings.af-packet != null) [ "af_packet" ]; + + users = { + groups.${cfg.settings.run-as.group} = { }; + users.${cfg.settings.run-as.user} = { + group = cfg.settings.run-as.group; + isSystemUser = true; + }; + }; + + systemd.tmpfiles.rules = [ + "d ${cfg.settings."default-log-dir"} 755 ${cfg.settings.run-as.user} ${cfg.settings.run-as.group}" + "d /var/lib/suricata 755 ${cfg.settings.run-as.user} ${cfg.settings.run-as.group}" + "d ${cfg.settings."default-rule-path"} 755 ${cfg.settings.run-as.user} ${cfg.settings.run-as.group}" + ]; + + systemd.services = { + suricata-update = { + description = "Update Suricata Rules"; + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + + script = + let + python = pkgs.python3.withPackages (ps: with ps; [ pyyaml ]); + enabledSourcesCmds = map ( + src: "${python.interpreter} ${pkg}/bin/suricata-update enable-source ${src}" + ) cfg.enabledSources; + in + '' + ${concatStringsSep "\n" enabledSourcesCmds} + ${python.interpreter} ${pkg}/bin/suricata-update update-sources + ${python.interpreter} ${pkg}/bin/suricata-update update --suricata-conf ${cfg.configFile} --no-test \ + --disable-conf ${pkgs.writeText "suricata-disable-conf" "${concatStringsSep "\n" cfg.disabledRules}"} + ''; + serviceConfig = { + Type = "oneshot"; + + PrivateTmp = true; + PrivateDevices = true; + PrivateIPC = true; + + DynamicUser = true; + User = cfg.settings.run-as.user; + Group = cfg.settings.run-as.group; + + ReadOnlyPaths = cfg.configFile; + ReadWritePaths = [ + "/var/lib/suricata" + cfg.settings."default-rule-path" + ]; + }; + }; + suricata = { + description = "Suricata"; + wantedBy = [ "multi-user.target" ]; + after = [ "suricata-update.service" ]; + serviceConfig = + let + interfaceOptions = strings.concatMapStrings (interface: " -i ${interface}") captureInterfaces; + in + { + ExecStartPre = "!${pkg}/bin/suricata -c ${cfg.configFile} -T"; + ExecStart = "!${pkg}/bin/suricata -c ${cfg.configFile}${interfaceOptions}"; + Restart = "on-failure"; + + User = cfg.settings.run-as.user; + Group = cfg.settings.run-as.group; + + NoNewPrivileges = true; + PrivateTmp = true; + PrivateDevices = true; + PrivateIPC = true; + ProtectSystem = "strict"; + DevicePolicy = "closed"; + LockPersonality = true; + MemoryDenyWriteExecute = true; + ProtectHostname = true; + ProtectProc = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectControlGroups = true; + ProcSubset = "pid"; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + RemoveIPC = true; + + ReadOnlyPaths = cfg.configFile; + ReadWritePaths = cfg.settings."default-log-dir"; + RuntimeDirectory = "suricata"; + }; + }; + }; + }; +} diff --git a/nixos/modules/services/networking/suricata/settings.nix b/nixos/modules/services/networking/suricata/settings.nix new file mode 100644 index 000000000000..f96d78ca66d5 --- /dev/null +++ b/nixos/modules/services/networking/suricata/settings.nix @@ -0,0 +1,625 @@ +{ + lib, + config, + yaml, + ... +}: +let + cfg = config.services.suricata; + inherit (lib) + mkEnableOption + mkOption + types + literalExpression + ; + mkDisableOption = + name: + mkEnableOption name + // { + default = true; + example = false; + }; +in +{ + freeformType = yaml.type; + options = { + vars = mkOption { + type = types.nullOr ( + types.submodule { + options = { + address-groups = mkOption { + type = ( + types.submodule { + options = { + HOME_NET = mkOption { default = "[192.168.0.0/16,10.0.0.0/8,172.16.0.0/12]"; }; + EXTERNAL_NET = mkOption { default = "!$HOME_NET"; }; + HTTP_SERVERS = mkOption { default = "$HOME_NET"; }; + SMTP_SERVERS = mkOption { default = "$HOME_NET"; }; + SQL_SERVERS = mkOption { default = "$HOME_NET"; }; + DNS_SERVERS = mkOption { default = "$HOME_NET"; }; + TELNET_SERVERS = mkOption { default = "$HOME_NET"; }; + AIM_SERVERS = mkOption { default = "$EXTERNAL_NET"; }; + DC_SERVERS = mkOption { default = "$HOME_NET"; }; + DNP3_SERVER = mkOption { default = "$HOME_NET"; }; + DNP3_CLIENT = mkOption { default = "$HOME_NET"; }; + MODBUS_CLIENT = mkOption { default = "$HOME_NET"; }; + MODBUS_SERVER = mkOption { default = "$HOME_NET"; }; + ENIP_CLIENT = mkOption { default = "$HOME_NET"; }; + ENIP_SERVER = mkOption { default = "$HOME_NET"; }; + }; + } + ); + default = { }; + example = { + HOME_NET = "[192.168.0.0/16,10.0.0.0/8,172.16.0.0/12]"; + EXTERNAL_NET = "!$HOME_NET"; + HTTP_SERVERS = "$HOME_NET"; + SMTP_SERVERS = "$HOME_NET"; + SQL_SERVERS = "$HOME_NET"; + DNS_SERVERS = "$HOME_NET"; + TELNET_SERVERS = "$HOME_NET"; + AIM_SERVERS = "$EXTERNAL_NET"; + DC_SERVERS = "$HOME_NET"; + DNP3_SERVER = "$HOME_NET"; + DNP3_CLIENT = "$HOME_NET"; + MODBUS_CLIENT = "$HOME_NET"; + MODBUS_SERVER = "$HOME_NET"; + ENIP_CLIENT = "$HOME_NET"; + ENIP_SERVER = "$HOME_NET"; + }; + description = '' + The address group variables for suricata, if not defined the + default value of suricata (see example) will be used. + Your settings will extend the predefined values in example. + ''; + }; + + port-groups = mkOption { + type = with types; nullOr (attrsOf str); + default = { + HTTP_PORTS = "80"; + SHELLCODE_PORTS = "!80"; + ORACLE_PORTS = "1521"; + SSH_PORTS = "22"; + DNP3_PORTS = "20000"; + MODBUS_PORTS = "502"; + FILE_DATA_PORTS = "[$HTTP_PORTS,110,143]"; + FTP_PORTS = "21"; + GENEVE_PORTS = "6081"; + VXLAN_PORTS = "4789"; + TEREDO_PORTS = "3544"; + }; + description = '' + The port group variables for suricata. + ''; + }; + }; + } + ); + default = { }; # add default values to config + }; + + stats = mkOption { + type = + with types; + nullOr (submodule { + options = { + enable = mkEnableOption "suricata global stats"; + + interval = mkOption { + type = types.str; + default = "8"; + description = '' + The interval field (in seconds) controls the interval at + which stats are updated in the log. + ''; + }; + + decoder-events = mkOption { + type = types.bool; + default = true; + description = '' + Add decode events to stats + ''; + }; + + decoder-events-prefix = mkOption { + type = types.str; + default = "decoder.event"; + description = '' + Decoder event prefix in stats. Has been 'decoder' before, but that leads + to missing events in the eve.stats records. + ''; + }; + + stream-events = mkOption { + type = types.bool; + default = false; + description = '' + Add stream events as stats. + ''; + }; + }; + }); + default = null; # do not add to config unless specified + }; + + plugins = mkOption { + type = with types; nullOr (listOf path); + default = null; + description = '' + Plugins -- Experimental -- specify the filename for each plugin shared object + ''; + }; + + outputs = mkOption { + type = + with types; + nullOr ( + listOf ( + attrsOf (submodule { + freeformType = yaml.type; + options = { + enabled = mkEnableOption ""; + }; + }) + ) + ); + default = null; + example = literalExpression '' + [ + { + fast = { + enabled = "yes"; + filename = "fast.log"; + append = "yes"; + }; + } + { + eve-log = { + enabled = "yes"; + filetype = "regular"; + filename = "eve.json"; + community-id = true; + types = [ + { + alert.tagged-packets = "yes"; + } + ]; + }; + } + ]; + ''; + description = '' + Configure the type of alert (and other) logging you would like. + + Valid values for are e. g. `fast`, `eve-log`, `syslog`, `file-store`, ... + - `fast`: a line based alerts log similar to Snort's fast.log + - `eve-log`: Extensible Event Format (nicknamed EVE) event log in JSON format + + For more details regarding the configuration, checkout the shipped suricata.yaml + ```shell + nix-shell -p suricata yq coreutils-full --command 'yq < $(dirname $(which suricata))/../etc/suricata/suricata.yaml' + ``` + and the [suricata documentation](https://docs.suricata.io/en/latest/output/index.html). + ''; + }; + + "default-log-dir" = mkOption { + type = types.str; + default = "/var/log/suricata"; + description = '' + The default logging directory. Any log or output file will be placed here if it's + not specified with a full path name. This can be overridden with the -l command + line parameter. + ''; + }; + + logging = { + "default-log-level" = mkOption { + type = types.enum [ + "error" + "warning" + "notice" + "info" + "perf" + "config" + "debug" + ]; + default = "notice"; + description = '' + The default log level: can be overridden in an output section. + Note that debug level logging will only be emitted if Suricata was + compiled with the --enable-debug configure option. + ''; + }; + + "default-log-format" = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The default output format. Optional parameter, should default to + something reasonable if not provided. Can be overridden in an + output section. You can leave this out to get the default. + ''; + }; + + "default-output-filter" = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + A regex to filter output. Can be overridden in an output section. + Defaults to empty (no filter). + ''; + }; + + "stacktrace-on-signal" = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Requires libunwind to be available when Suricata is configured and built. + If a signal unexpectedly terminates Suricata, displays a brief diagnostic + message with the offending stacktrace if enabled. + ''; + }; + + outputs = { + console = { + enable = mkDisableOption "logging to console"; + }; + file = { + enable = mkDisableOption "logging to file"; + + level = mkOption { + type = types.enum [ + "error" + "warning" + "notice" + "info" + "perf" + "config" + "debug" + ]; + default = "info"; + description = '' + Loglevel for logs written to the logfile + ''; + }; + + filename = mkOption { + type = types.str; + default = "suricata.log"; + description = '' + Filename of the logfile + ''; + }; + + format = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Logformat for logs written to the logfile + ''; + }; + + type = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Type of logfile + ''; + }; + }; + syslog = { + enable = mkEnableOption "logging to syslog"; + + facility = mkOption { + type = types.str; + default = "local5"; + description = '' + Facility to log to + ''; + }; + + format = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Logformat for logs send to syslog + ''; + }; + + type = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Type of logs send to syslog + ''; + }; + }; + }; + }; + + "af-packet" = mkOption { + type = + with types; + nullOr ( + listOf (submodule { + freeformType = yaml.type; + options = { + interface = mkOption { + type = types.str; + default = null; + }; + }; + }) + ); + default = null; + description = '' + Linux high speed capture support + ''; + }; + + "af-xdp" = mkOption { + type = + with types; + nullOr ( + listOf (submodule { + freeformType = yaml.type; + options = { + interface = mkOption { + type = types.str; + default = null; + }; + }; + }) + ); + default = null; + description = '' + Linux high speed af-xdp capture support, see + [docs/capture-hardware/af-xdp](https://docs.suricata.io/en/suricata-7.0.3/capture-hardware/af-xdp.html) + ''; + }; + + "dpdk" = mkOption { + type = + with types; + nullOr (submodule { + options = { + eal-params.proc-type = mkOption { + type = with types; nullOr str; + default = null; + }; + interfaces = mkOption { + type = + with types; + nullOr ( + listOf (submodule { + freeformType = yaml.type; + options = { + interface = mkOption { + type = types.str; + default = null; + }; + }; + }) + ); + default = null; + }; + }; + }); + default = null; + description = '' + DPDK capture support, see + [docs/capture-hardware/dpdk](https://docs.suricata.io/en/suricata-7.0.3/capture-hardware/dpdk.html) + ''; + }; + + "pcap" = mkOption { + type = + with types; + nullOr ( + listOf (submodule { + freeformType = yaml.type; + options = { + interface = mkOption { + type = types.str; + default = null; + }; + }; + }) + ); + default = null; + description = '' + Cross platform libpcap capture support + ''; + }; + + "pcap-file".checksum-checks = mkOption { + type = types.enum [ + "yes" + "no" + "auto" + ]; + default = "auto"; + description = '' + Possible values are: + - yes: checksum validation is forced + - no: checksum validation is disabled + - auto: Suricata uses a statistical approach to detect when + checksum off-loading is used. (default) + Warning: 'checksum-validation' must be set to yes to have checksum tested + ''; + }; + + "app-layer" = mkOption { + type = + with types; + nullOr (submodule { + options = { + "error-policy" = mkOption { + type = types.enum [ + "drop-flow" + "pass-flow" + "bypass" + "drop-packet" + "pass-packet" + "reject" + "ignore" + ]; + default = "ignore"; + description = '' + The error-policy setting applies to all app-layer parsers. Values can be + "drop-flow", "pass-flow", "bypass", "drop-packet", "pass-packet", "reject" or + "ignore" (the default). + ''; + }; + protocols = mkOption { + type = + with types; + nullOr ( + attrsOf (submodule { + freeformType = yaml.type; + options = { + enabled = mkOption { + type = types.enum [ + "yes" + "no" + "detection-only" + ]; + default = "no"; + description = '' + The option "enabled" takes 3 values - "yes", "no", "detection-only". + "yes" enables both detection and the parser, "no" disables both, and + "detection-only" enables protocol detection only (parser disabled). + ''; + }; + }; + }) + ); + default = null; + }; + }; + }); + default = null; # do not add to config unless specified + }; + + "run-as" = { + user = mkOption { + type = types.str; + default = "suricata"; + description = "Run Suricata with a specific user-id"; + }; + group = mkOption { + type = types.str; + default = "suricata"; + description = "Run Suricata with a specific group-id"; + }; + }; + + "host-mode" = mkOption { + type = types.enum [ + "router" + "sniffer-only" + "auto" + ]; + default = "auto"; + description = '' + If the Suricata box is a router for the sniffed networks, set it to 'router'. If + it is a pure sniffing setup, set it to 'sniffer-only'. If set to auto, the variable + is internally switched to 'router' in IPS mode and 'sniffer-only' in IDS mode. + This feature is currently only used by the reject* keywords. + ''; + }; + + "unix-command" = mkOption { + type = + with types; + nullOr (submodule { + options = { + enabled = mkOption { + type = types.either types.bool (types.enum [ "auto" ]); + default = "auto"; + }; + filename = mkOption { + type = types.path; + default = "/run/suricata/suricata-command.socket"; + }; + }; + }); + default = { }; + description = '' + Unix command socket that can be used to pass commands to Suricata. + An external tool can then connect to get information from Suricata + or trigger some modifications of the engine. Set enabled to yes + to activate the feature. In auto mode, the feature will only be + activated in live capture mode. You can use the filename variable to set + the file name of the socket. + ''; + }; + + "exception-policy" = mkOption { + type = types.enum [ + "auto" + "drop-packet" + "drop-flow" + "reject" + "bypass" + "pass-packet" + "pass-flow" + "ignore" + ]; + default = "auto"; + description = '' + Define a common behavior for all exception policies. + In IPS mode, the default is drop-flow. For cases when that's not possible, the + engine will fall to drop-packet. To fallback to old behavior (setting each of + them individually, or ignoring all), set this to ignore. + All values available for exception policies can be used, and there is one + extra option: auto - which means drop-flow or drop-packet (as explained above) + in IPS mode, and ignore in IDS mode. Exception policy values are: drop-packet, + drop-flow, reject, bypass, pass-packet, pass-flow, ignore (disable). + ''; + }; + + "default-rule-path" = mkOption { + type = types.path; + default = "/var/lib/suricata/rules"; + description = "Path in which suricata-update managed rules are stored by default"; + }; + + "rule-files" = mkOption { + type = types.listOf types.str; + default = [ "suricata.rules" ]; + description = "Files to load suricata-update managed rules, relative to 'default-rule-path'"; + }; + + "classification-file" = mkOption { + type = types.str; + default = "/var/lib/suricata/rules/classification.config"; + description = "Suricata classification configuration file"; + }; + + "reference-config-file" = mkOption { + type = types.str; + default = "${cfg.package}/etc/suricata/reference.config"; + description = "Suricata reference configuration file"; + }; + + "threshold-file" = mkOption { + type = types.str; + default = "${cfg.package}/etc/suricata/threshold.config"; + description = "Suricata threshold configuration file"; + }; + + includes = mkOption { + type = with types; nullOr (listOf path); + default = null; + description = '' + Files to include in the suricata configuration. See + [docs/configuration/suricata-yaml](https://docs.suricata.io/en/suricata-7.0.3/configuration/suricata-yaml.html) + for available options. + ''; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index af9d79f649bd..4574088575fd 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -942,6 +942,7 @@ in { sudo = handleTest ./sudo.nix {}; sudo-rs = handleTest ./sudo-rs.nix {}; sunshine = handleTest ./sunshine.nix {}; + suricata = handleTest ./suricata.nix {}; suwayomi-server = handleTest ./suwayomi-server.nix {}; swap-file-btrfs = handleTest ./swap-file-btrfs.nix {}; swap-partition = handleTest ./swap-partition.nix {}; diff --git a/nixos/tests/suricata.nix b/nixos/tests/suricata.nix new file mode 100644 index 000000000000..e1cdd91aaaa2 --- /dev/null +++ b/nixos/tests/suricata.nix @@ -0,0 +1,86 @@ +import ./make-test-python.nix ( + { lib, pkgs, ... }: + { + name = "suricata"; + meta.maintainers = with lib.maintainers; [ felbinger ]; + + nodes = { + ids = { + imports = [ + ../modules/profiles/minimal.nix + ../modules/services/networking/suricata/default.nix + ]; + + networking.interfaces.eth1 = { + useDHCP = false; + ipv4.addresses = [ + { + address = "192.168.1.2"; + prefixLength = 24; + } + ]; + }; + + # disable suricata-update because this requires an Internet connection + systemd.services.suricata-update.enable = false; + + # install suricata package to make suricatasc program available + environment.systemPackages = with pkgs; [ suricata ]; + + services.suricata = { + enable = true; + settings = { + vars.address-groups.HOME_NET = "192.168.1.0/24"; + unix-command.enabled = true; + outputs = [ { fast.enabled = true; } ]; + af-packet = [ { interface = "eth1"; } ]; + classification-file = "${pkgs.suricata}/etc/suricata/classification.config"; + }; + }; + + # create suricata.rules with the rule to detect the output of the id command + systemd.tmpfiles.rules = [ + ''f /var/lib/suricata/rules/suricata.rules 644 suricata suricata 0 alert ip any any -> any any (msg:"GPL ATTACK_RESPONSE id check returned root"; content:"uid=0|28|root|29|"; classtype:bad-unknown; sid:2100498; rev:7; metadata:created_at 2010_09_23, updated_at 2019_07_26;)'' + ]; + }; + helper = { + imports = [ ../modules/profiles/minimal.nix ]; + + networking.interfaces.eth1 = { + useDHCP = false; + ipv4.addresses = [ + { + address = "192.168.1.1"; + prefixLength = 24; + } + ]; + }; + + services.nginx = { + enable = true; + virtualHosts."localhost".locations = { + "/id/".return = "200 'uid=0(root) gid=0(root) groups=0(root)'"; + }; + }; + networking.firewall.allowedTCPPorts = [ 80 ]; + }; + }; + + testScript = '' + start_all() + + # check that configuration has been applied correctly with suricatasc + with subtest("suricata configuration test"): + ids.wait_for_unit("suricata.service") + assert '1' in ids.succeed("suricatasc -c 'iface-list' | ${pkgs.jq}/bin/jq .message.count") + + # test detection of events based on a static ruleset (output of id command) + with subtest("suricata rule test"): + helper.wait_for_unit("nginx.service") + ids.wait_for_unit("suricata.service") + + ids.succeed("curl http://192.168.1.1/id/") + assert "id check returned root [**] [Classification: Potentially Bad Traffic]" in ids.succeed("tail -n 1 /var/log/suricata/fast.log"), "Suricata didn't detect the output of id comment" + ''; + } +)