# services/crowdsec.nix — Vendors the crowdsec module rewrite from
# https://github.com/NixOS/nixpkgs/pull/446307 (TornaxO7's branch) until
# it lands upstream. The upstream module in nixpkgs at the pinned revision
# is broken for first-time bootstrap (no auto cscli machines add, DynamicUser
# state ownership wedges).
#
# When PR #446307 merges to nixpkgs unstable:
#   1. Bump flake.lock past the merge commit
#   2. Delete ../modules/crowdsec/ and the disabledModules + imports lines below
#   3. The settings/option API is the same as the PR's, so config below is forward-compatible
#
# Before first deploy, create /var/secrets/ntfy-url with your topic URL:
#   echo 'https://ntfy.sh/nordhammer-<random>' | sudo tee /var/secrets/ntfy-url
#   sudo chmod 640 /var/secrets/ntfy-url
{ config, lib, pkgs, ... }:
let
  ntfyUrlFile = "/var/secrets/ntfy-url";
  ntfyUrl =
    if builtins.pathExists ntfyUrlFile
    then lib.removeSuffix "\n" (builtins.readFile ntfyUrlFile)
    else "https://ntfy.sh/CHANGE-ME-CREATE-VAR-SECRETS-NTFY-URL";

  # nixpkgs only builds the agent + cscli; the new module also expects
  # notification plugins at $out/libexec/crowdsec/plugins/. Compile them
  # from the same source tree (cmd/notification-*) and move them there.
  pluginNames = [ "dummy" "email" "file" "http" "sentinel" "slack" "splunk" ];
  crowdsecWithPlugins = pkgs.crowdsec.overrideAttrs (old: {
    subPackages = (old.subPackages or [ ]) ++ map (p: "cmd/notification-${p}") pluginNames;
    postInstall = (old.postInstall or "") + ''
      mkdir -p $out/libexec/crowdsec/plugins
      for p in ${lib.concatStringsSep " " pluginNames}; do
        if [ -f $out/bin/notification-$p ]; then
          mv $out/bin/notification-$p $out/libexec/crowdsec/plugins/notification-$p
        fi
      done
    '';
  });
in
{
  disabledModules = [
    "services/security/crowdsec.nix"
    "services/security/crowdsec-firewall-bouncer.nix"
  ];

  imports = [
    ../modules/crowdsec/crowdsec.nix
    ../modules/crowdsec/crowdsec-firewall-bouncer.nix
  ];

  config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") {

    # Static user/group for crowdsec. The vendored module relies on
    # DynamicUser=true plus a chown hack in crowdsec-setup's ExecStartPre,
    # which broke on stable's systemd because the dynamic user isn't
    # visible to NSS at chown time. Declaring the user statically makes
    # systemd use it (DynamicUser becomes a no-op) and chown succeeds.
    users.users.crowdsec = {
      isSystemUser = true;
      group = "crowdsec";
      home = "/var/lib/crowdsec";
      description = "CrowdSec security agent";
    };
    users.groups.crowdsec = { };

    services.crowdsec = {
      enable = true;
      name = "fredos-mediaserver";
      package = crowdsecWithPlugins;

      hub.collections = [
        "crowdsecurity/linux"                 # sshd + linux LPE
        "crowdsecurity/nginx"                 # nginx parser
        "crowdsecurity/base-http-scenarios"   # generic HTTP attacks
        "crowdsecurity/http-cve"              # known-CVE fingerprints
        "crowdsecurity/whitelist-good-actors" # don't ban legit crawlers
      ];

      # Allow the agent (DynamicUser) to read its acquisition sources:
      #  - nginx group → /var/log/nginx/access.log (nginx:nginx 640)
      #  - systemd-journal → journald entries from sshd + authelia
      #    (without it, journalctl returns "insufficient permissions" and
      #    the entire ssh-bf / authelia-bf detection chain runs blind)
      readOnlyPaths = [ "/var/log/nginx" ];
      extraGroups = [ "nginx" "systemd-journal" ];

      settings = {
        # config.yaml — main agent + LAPI configuration
        config.api.server.listen_uri = "127.0.0.1:8081";  # 8080 is qBit

        # Drop alerts originating from LAN clients before they're scored.
        # Without this, repeated Authelia 401s from inside the house (e.g.
        # a stale browser session on the gaming desktop) trip ssh-bf /
        # http-bf scenarios and the firewall bouncer self-bans 10.0.0.x.
        parsers.s02Enrich = [
          {
            name = "nordhammer/lan-whitelist";
            description = "Whitelist LAN + loopback to prevent self-bans";
            whitelist = {
              reason = "Local LAN";
              ip = [ "127.0.0.1" "::1" ];
              cidr = [ "10.0.0.0/24" ];
            };
          }
        ];

        # Log sources to ingest
        acquisitions = [
          {
            source = "file";
            filenames = [ "/var/log/nginx/access.log" ];
            labels.type = "nginx";
          }
          {
            source = "journalctl";
            journalctl_filter = [ "_SYSTEMD_UNIT=sshd.service" ];
            labels.type = "syslog";
          }
          {
            source = "journalctl";
            journalctl_filter = [ "_SYSTEMD_UNIT=authelia-main.service" ];
            labels.type = "syslog";
          }
        ];

        # Push notifications via ntfy.sh
        notifications = [
          {
            name = "ntfy_http";
            type = "http";
            log_level = "info";
            url = ntfyUrl;
            method = "POST";
            headers = {
              Title = "CrowdSec alert";
              Priority = "high";
              Tags = "rotating_light";
            };
            format = ''
              {{range . -}}
              {{.Scenario}} from {{.Source.IP}} ({{.Source.Cn}}) — {{len .Decisions}} decision(s) taken
              {{end -}}
            '';
          }
        ];

        # Override default profiles to attach the ntfy notifier
        profiles = [
          {
            name = "default_ip_remediation";
            filters = [ "Alert.Remediation == true && Alert.GetScope() == 'Ip'" ];
            decisions = [{ type = "ban"; duration = "4h"; }];
            notifications = [ "ntfy_http" ];
            on_success = "break";
          }
          {
            name = "default_range_remediation";
            filters = [ "Alert.Remediation == true && Alert.GetScope() == 'Range'" ];
            decisions = [{ type = "ban"; duration = "4h"; }];
            notifications = [ "ntfy_http" ];
            on_success = "break";
          }
        ];
      };
    };

    # Firewall bouncer enforces decisions via nftables; auto-registers with LAPI
    services.crowdsec-firewall-bouncer = {
      enable = true;
      registerBouncer.enable = true;
    };

    # The hub keeps tracking upstream master, but nixpkgs stable's crowdsec
    # binary is a few versions behind and doesn't know newer expr functions
    # (e.g. LookupFile, used by crowdsecurity/http-technology-probing). The
    # agent then refuses to load the entire bucket and crashes on startup.
    # Strip incompatible scenarios after crowdsec-setup repopulates the hub
    # but before crowdsec.service tries to load them.
    systemd.services.crowdsec-prune-incompatible-hub-items = {
      description = "Remove hub scenarios incompatible with the bundled crowdsec";
      after = [ "crowdsec-setup.service" ];
      before = [ "crowdsec.service" ];
      requiredBy = [ "crowdsec.service" ];
      serviceConfig = {
        Type = "oneshot";
        RemainAfterExit = true;
        ExecStart = "${pkgs.coreutils}/bin/rm -f /etc/crowdsec/scenarios/http-technology-probing.yaml";
      };
    };
  };
}
