Files
panoptikon/nix/module.nix

150 lines
4.9 KiB
Nix
Raw Normal View History

2026-02-20 17:22:14 +01:00
{
config,
lib,
pkgs,
...
}:
{
options.services.panoptikon = {
enable = lib.mkEnableOption "Generic command output / website watcher";
watchers = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule (watcher: {
options = {
script = lib.mkOption {
type = lib.types.path;
description = ''
A script whose stdout is to be watched.
'';
example = ''
pkgs.writers.writeDash "github-meta" '''
''${pkgs.curl}/bin/curl -sSL https://api.github.com/meta | ''${pkgs.jq}/bin/jq
'''
'';
};
frequency = lib.mkOption {
type = lib.types.str;
description = ''
How often to run the script. See systemd.time(7) for more information about the format.
'';
example = "*:0/3";
default = "daily";
};
loadCredential = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = ''
This can be used to pass secrets to the systemd service without adding them to the nix store.
'';
default = [ ];
};
reporters = lib.mkOption {
type = lib.types.listOf lib.types.path;
description = ''
A list of scripts that take the diff (if any) via stdin and report it (e.g. to IRC, Telegram or Prometheus). The name of the watcher will be in the $PANOPTIKON_WATCHER environment variable.
'';
example = ''
[
(pkgs.writers.writeDash "telegram-reporter" '''
''${pkgs.curl}/bin/curl -X POST https://api.telegram.org/bot''${TOKEN}/sendMessage \
-d chat_id=123456 \
-d text="$(cat)"
''')
(pkgs.writers.writeDash "notify" '''
''${pkgs.libnotify}/bin/notify-send "$PANOPTIKON_WATCHER has changed."
''')
]
'';
};
};
config = { };
})
);
};
};
config =
let
cfg = config.services.panoptikon;
2026-02-20 18:14:49 +01:00
stateDir = "/var/lib/panoptikon";
2026-02-20 17:22:14 +01:00
in
lib.mkIf cfg.enable {
users.extraUsers.panoptikon = {
isSystemUser = true;
createHome = true;
2026-02-20 18:14:49 +01:00
home = stateDir;
2026-02-20 17:22:14 +01:00
group = "panoptikon";
};
users.extraGroups.panoptikon = { };
systemd.timers = lib.attrsets.mapAttrs' (
watcherName: _:
lib.nameValuePair "panoptikon-${watcherName}" {
timerConfig.RandomizedDelaySec = toString (60 * 60);
}
) cfg.watchers;
systemd.services = lib.attrsets.mapAttrs' (
watcherName: watcherOptions:
2026-02-20 18:14:49 +01:00
let
# Absolute paths for the state files
currentFile = "${stateDir}/${watcherName}";
oldFile = "${stateDir}/${watcherName}.old";
in
2026-02-20 17:22:14 +01:00
lib.nameValuePair "panoptikon-${watcherName}" {
enable = true;
startAt = watcherOptions.frequency;
2026-02-20 18:14:49 +01:00
2026-02-20 17:22:14 +01:00
serviceConfig = {
Type = "oneshot";
User = "panoptikon";
Group = "panoptikon";
2026-02-20 18:14:49 +01:00
# ISOLATION: Start in a fresh, empty directory for every run
RuntimeDirectory = "panoptikon/${watcherName}";
WorkingDirectory = "/run/panoptikon/${watcherName}";
# PERSISTENCE: Only allow write access to the specific state directory
ReadWritePaths = [ stateDir ];
# HARDENING: Prevent the process from seeing most of the system
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
NoNewPrivileges = true;
2026-02-20 17:22:14 +01:00
Restart = "on-failure";
LoadCredential = watcherOptions.loadCredential;
};
2026-02-20 18:14:49 +01:00
2026-02-20 17:22:14 +01:00
environment.PANOPTIKON_WATCHER = watcherName;
2026-02-20 18:14:49 +01:00
2026-02-20 17:22:14 +01:00
script = ''
2026-02-20 18:14:49 +01:00
set -efu # Removed -x to keep tokens out of logs if they leak via env
# 1. Run watcher and save to the persistent state dir
${watcherOptions.script} > "${currentFile}"
# 2. Ensure .old exists so diff doesn't crash on first run
[ -f "${oldFile}" ] || touch "${oldFile}"
# 3. Compare
diff_output=$(${pkgs.diffutils}/bin/diff --new-file "${oldFile}" "${currentFile}" || :)
if [ -n "$diff_output" ]; then
# 4. Run reporters.
# They are running inside /run/panoptikon/${watcherName} (empty).
# They can't see or delete other .old files via relative paths.
2026-02-20 17:22:14 +01:00
${lib.strings.concatMapStringsSep "\n" (
reporter: ''echo "$diff_output" | ${reporter} || :''
) watcherOptions.reporters}
fi
2026-02-20 18:14:49 +01:00
# 5. Rotate state
mv "${currentFile}" "${oldFile}"
2026-02-20 17:22:14 +01:00
'';
}
) cfg.watchers;
};
}