add examples and test VM
This commit is contained in:
149
nix/module.nix
Normal file
149
nix/module.nix
Normal file
@@ -0,0 +1,149 @@
|
||||
{
|
||||
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;
|
||||
stateDir = "/var/lib/panoptikon";
|
||||
in
|
||||
lib.mkIf cfg.enable {
|
||||
users.extraUsers.panoptikon = {
|
||||
isSystemUser = true;
|
||||
createHome = true;
|
||||
home = stateDir;
|
||||
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:
|
||||
let
|
||||
# Absolute paths for the state files
|
||||
currentFile = "${stateDir}/${watcherName}";
|
||||
oldFile = "${stateDir}/${watcherName}.old";
|
||||
in
|
||||
lib.nameValuePair "panoptikon-${watcherName}" {
|
||||
enable = true;
|
||||
startAt = watcherOptions.frequency;
|
||||
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = "panoptikon";
|
||||
Group = "panoptikon";
|
||||
|
||||
# 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;
|
||||
|
||||
Restart = "on-failure";
|
||||
LoadCredential = watcherOptions.loadCredential;
|
||||
};
|
||||
|
||||
environment.PANOPTIKON_WATCHER = watcherName;
|
||||
|
||||
script = ''
|
||||
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.
|
||||
${lib.strings.concatMapStringsSep "\n" (
|
||||
reporter: ''echo "$diff_output" | ${reporter} || :''
|
||||
) watcherOptions.reporters}
|
||||
fi
|
||||
|
||||
# 5. Rotate state
|
||||
mv "${currentFile}" "${oldFile}"
|
||||
'';
|
||||
}
|
||||
) cfg.watchers;
|
||||
};
|
||||
}
|
||||
125
nix/overlay.nix
Normal file
125
nix/overlay.nix
Normal file
@@ -0,0 +1,125 @@
|
||||
final: prev:
|
||||
let
|
||||
lib = prev.lib;
|
||||
in
|
||||
{
|
||||
panoptikonReporters = (prev.panoptikonWatchers or { }) // {
|
||||
kpaste-irc =
|
||||
{
|
||||
target,
|
||||
retiolumLink ? false,
|
||||
server ? "irc.r",
|
||||
messagePrefix ? "change detected: ",
|
||||
nick ? ''"$PANOPTIKON_WATCHER"-watcher'',
|
||||
}:
|
||||
prev.writers.writeDash "kpaste-irc-reporter" ''
|
||||
KPASTE_CONTENT_TYPE=text/plain ${prev.kpaste}/bin/kpaste \
|
||||
| ${prev.gnused}/bin/sed -n "${if retiolumLink then "2" else "3"}s/^/${messagePrefix}/p" \
|
||||
| ${prev.nur.repos.mic92.ircsink}/bin/ircsink \
|
||||
--nick ${nick} \
|
||||
--server ${server} \
|
||||
--target ${target}
|
||||
'';
|
||||
irc =
|
||||
{
|
||||
target, # e.g., "#mychannel"
|
||||
server ? "irc.libera.chat",
|
||||
port ? "6667",
|
||||
nick ? "panoptikon-bot",
|
||||
messagePrefix ? "change detected: ",
|
||||
}:
|
||||
prev.writers.writeDash "irc-reporter" ''
|
||||
MESSAGE=$(cat | tr -d '\n\r')
|
||||
# Use netcat to send raw IRC commands
|
||||
(
|
||||
echo "NICK ${nick}"
|
||||
echo "USER ${nick} 8 * :Panoptikon Watcher"
|
||||
sleep 2 # Give the server a moment to process login
|
||||
echo "JOIN ${target}"
|
||||
echo "PRIVMSG ${target} :${messagePrefix} $MESSAGE"
|
||||
echo "QUIT"
|
||||
) | ${prev.netcat}/bin/nc -w 10 ${server} ${port}
|
||||
'';
|
||||
telegram =
|
||||
{
|
||||
tokenPath, # Bot API Token
|
||||
chatId,
|
||||
messagePrefix ? "Change detected in $PANOPTIKON_WATCHER: ",
|
||||
}:
|
||||
prev.writers.writeDash "telegram-reporter" ''
|
||||
if [ ! -f "${tokenPath}" ]; then
|
||||
echo "Error: Token file ${tokenPath} not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
TOKEN=$(cat "${tokenPath}")
|
||||
MESSAGE=$(cat)
|
||||
${prev.curl}/bin/curl -s -X POST "https://api.telegram.org/bot''${TOKEN}/sendMessage" \
|
||||
-d chat_id="${chatId}" \
|
||||
-d text="${messagePrefix} $MESSAGE"
|
||||
'';
|
||||
matrix =
|
||||
{
|
||||
homeserver, # e.g., "https://matrix.org"
|
||||
roomId, # e.g., "!roomid:matrix.org"
|
||||
tokenPath,
|
||||
messagePrefix ? "Change detected in $PANOPTIKON_WATCHER: ",
|
||||
}:
|
||||
prev.writers.writeDash "matrix-reporter" ''
|
||||
if [ ! -f "${tokenPath}" ]; then
|
||||
echo "Error: Token file ${tokenPath} not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
TOKEN=$(cat "${tokenPath}")
|
||||
MESSAGE=$(cat)
|
||||
TXN_ID=$(date +%s%N)
|
||||
${prev.curl}/bin/curl -s -X PUT "${homeserver}/_matrix/client/r0/rooms/${lib.escapeShellArg roomId}/send/m.room.message/''${TXN_ID}?access_token=''${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"msgtype\": \"m.text\", \"body\": \"${messagePrefix} $MESSAGE\"}"
|
||||
'';
|
||||
wall =
|
||||
{
|
||||
messagePrefix ? "PANOPTIKON ALERT ($PANOPTIKON_WATCHER):",
|
||||
}:
|
||||
prev.writers.writeDash "wall-reporter" ''
|
||||
MESSAGE=$(cat)
|
||||
echo -e "${messagePrefix}\n$MESSAGE" | ${prev.util-linux}/bin/wall
|
||||
'';
|
||||
mail =
|
||||
{
|
||||
recipient, # e.g. "admin@example.com"
|
||||
subjectPrefix ? "[Panoptikon] Change detected:",
|
||||
}:
|
||||
prev.writers.writeDash "mail-reporter" ''
|
||||
MESSAGE=$(cat)
|
||||
echo "$MESSAGE" | ${prev.mailutils}/bin/mail -s "${subjectPrefix} $PANOPTIKON_WATCHER" "${recipient}"
|
||||
'';
|
||||
};
|
||||
panoptikonWatchers = (prev.panoptikonWatchers or { }) // {
|
||||
plain =
|
||||
address:
|
||||
prev.writers.writeDash "watch-url" ''
|
||||
${prev.curl}/bin/curl -sSL ${lib.escapeShellArg address}
|
||||
'';
|
||||
html =
|
||||
address:
|
||||
prev.writers.writeDash "watch-html" ''
|
||||
${prev.curl}/bin/curl -sSL ${lib.escapeShellArg address} \
|
||||
| ${prev.python3Packages.html2text}/bin/html2text --decode-errors=ignore
|
||||
'';
|
||||
htmlSelector =
|
||||
selector: address:
|
||||
prev.writers.writeDash "watch-html-selector" ''
|
||||
${prev.curl}/bin/curl -sSL ${lib.escapeShellArg address} \
|
||||
| ${prev.htmlq}/bin/htmlq ${lib.escapeShellArg selector} \
|
||||
| ${prev.python3Packages.html2text}/bin/html2text
|
||||
'';
|
||||
json =
|
||||
{
|
||||
jqScript ? ".",
|
||||
}:
|
||||
address:
|
||||
prev.writers.writeDash "watch-json" ''
|
||||
${prev.curl}/bin/curl -sSL ${lib.escapeShellArg address} | ${prev.jq}/bin/jq -f ${prev.writeText "script.jq" jqScript}
|
||||
'';
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user