add examples and test VM
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.qcow2
|
||||||
19
LICENSE
Normal file
19
LICENSE
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
Copyright © 2026 Kierán Meinhardt
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the “Software”), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
231
README.md
231
README.md
@@ -1,9 +1,8 @@
|
|||||||
# Panoptikon – Watch the world from NixOS
|
# Panoptikon – Watch the world from NixOS
|
||||||
|
A NixOS module for monitoring website content and command output changes.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
A NixOS module for monitoring website content and command output changes.
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Panoptikon is a generic command output and website watcher that periodically runs scripts and reports changes. It's designed to be flexible and can monitor anything from API endpoints to system metrics.
|
Panoptikon is a generic command output and website watcher that periodically runs scripts and reports changes. It's designed to be flexible and can monitor anything from API endpoints to system metrics.
|
||||||
@@ -25,11 +24,6 @@ Add Panoptikon to your NixOS configuration:
|
|||||||
{ config, pkgs, ... }:
|
{ config, pkgs, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
imports = [
|
|
||||||
# Add the Panoptikon module
|
|
||||||
(pkgs.callPackage (builtins.fetchTarball https://github.com/kfm/panoptikon-library/archive/main.tar.gz) { }).nixosModules.default
|
|
||||||
];
|
|
||||||
|
|
||||||
# Enable Panoptikon service
|
# Enable Panoptikon service
|
||||||
services.panoptikon.enable = true;
|
services.panoptikon.enable = true;
|
||||||
|
|
||||||
@@ -51,21 +45,21 @@ Add Panoptikon to your NixOS configuration:
|
|||||||
services.panoptikon.watchers = {
|
services.panoptikon.watchers = {
|
||||||
# Monitor GitHub metadata
|
# Monitor GitHub metadata
|
||||||
github-meta = {
|
github-meta = {
|
||||||
script = pkgs.writers.writeDash "github-meta" '''
|
script = pkgs.writers.writeDash "github-meta" ''
|
||||||
${pkgs.curl}/bin/curl -sSL https://api.github.com/meta | ${pkgs.jq}/bin/jq
|
${pkgs.curl}/bin/curl -sSL https://api.github.com/meta | ${pkgs.jq}/bin/jq
|
||||||
''';
|
'';
|
||||||
frequency = "*:0/5"; # Every 5 minutes
|
frequency = "*:0/5"; # Every 5 minutes
|
||||||
reporters = [
|
reporters = [
|
||||||
# Report changes to Telegram
|
# Report changes to Telegram
|
||||||
(pkgs.writers.writeDash "telegram-reporter" '''
|
(pkgs.writers.writeDash "telegram-reporter" ''
|
||||||
${pkgs.curl}/bin/curl -X POST https://api.telegram.org/bot''${TOKEN}/sendMessage \
|
${pkgs.curl}/bin/curl -X POST https://api.telegram.org/bot''${TOKEN}/sendMessage \
|
||||||
-d chat_id=123456 \
|
-d chat_id=123456 \
|
||||||
-d text="$(cat)"
|
-d text="$(cat)"
|
||||||
''')
|
'')
|
||||||
# Also show desktop notifications
|
# Also show desktop notifications
|
||||||
(pkgs.writers.writeDash "notify" '''
|
(pkgs.writers.writeDash "notify" ''
|
||||||
${pkgs.libnotify}/bin/notify-send "$PANOPTIKON_WATCHER has changed."
|
${pkgs.libnotify}/bin/notify-send "$PANOPTIKON_WATCHER has changed."
|
||||||
''')
|
'')
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,92 +79,24 @@ Add Panoptikon to your NixOS configuration:
|
|||||||
|
|
||||||
# Monitor a local command
|
# Monitor a local command
|
||||||
disk-space = {
|
disk-space = {
|
||||||
script = pkgs.writers.writeDash "disk-space" '''
|
script = pkgs.writers.writeDash "disk-space" ''
|
||||||
df -h / | tail -1 | awk '{print $5 " used"}'
|
df -h / | tail -1 | awk '{print $5 " used
|
||||||
''';
|
}'';
|
||||||
frequency = "*:0/30"; # Every 30 minutes
|
frequency = "*:0/30"; # Every 30 minutes
|
||||||
reporters = [
|
reporters = [
|
||||||
# Log to systemd journal
|
# Log to systemd journal
|
||||||
(pkgs.writers.writeDash "journal-log" '''
|
(pkgs.writers.writeDash "journal-log" ''
|
||||||
journalctl -t panoptikon-disk-space --since "1 hour ago" | tail -5
|
journalctl -t panoptikon-disk-space --since "1 hour ago" | tail -5
|
||||||
''')
|
'')
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Advanced Configuration
|
|
||||||
|
|
||||||
#### Using Secrets
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
services.panoptikon.watchers = {
|
|
||||||
private-api = {
|
|
||||||
script = pkgs.writers.writeDash "private-api" '''
|
|
||||||
${pkgs.curl}/bin/curl -sSL \
|
|
||||||
-H "Authorization: Bearer $API_TOKEN" \
|
|
||||||
https://api.example.com/data
|
|
||||||
''';
|
|
||||||
frequency = "hourly";
|
|
||||||
loadCredential = [ "API_TOKEN" ];
|
|
||||||
reporters = [
|
|
||||||
(pkgs.writers.writeDash "secure-reporter" '''
|
|
||||||
${pkgs.curl}/bin/curl -X POST https://secure.example.com/report \
|
|
||||||
-d "$(cat)"
|
|
||||||
''')
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### JSON API Monitoring
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
services.panoptikon.watchers = {
|
|
||||||
crypto-prices = {
|
|
||||||
script = pkgs.panoptikon.urlJSON {
|
|
||||||
jqScript = ".[0] | { name: .name, price: .quote.USD.price }";
|
|
||||||
} "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=bitcoin";
|
|
||||||
frequency = "*:0/15"; # Every 15 minutes
|
|
||||||
reporters = [
|
|
||||||
(pkgs.writers.writeDash "price-alert" '''
|
|
||||||
price=$(echo "$(cat)" | ${pkgs.jq}/bin/jq -r '.price')
|
|
||||||
if (( $(echo "$price > 60000" | bc -l) )); then
|
|
||||||
${pkgs.libnotify}/bin/notify-send "Bitcoin price: $$price"
|
|
||||||
fi
|
|
||||||
''')
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Available Helper Scripts
|
|
||||||
|
|
||||||
Panoptikon provides several helper scripts in the overlay:
|
|
||||||
|
|
||||||
### URL Monitoring
|
|
||||||
|
|
||||||
- `pkgs.panoptikon.url`: Fetch and convert HTML to text
|
|
||||||
- `pkgs.panoptikon.urlSelector`: Extract specific HTML elements using CSS selectors
|
|
||||||
- `pkgs.panoptikon.urlJSON`: Fetch JSON and apply jq transformations
|
|
||||||
|
|
||||||
### Reporters
|
|
||||||
|
|
||||||
- `pkgs.panoptikon.kpaste-irc`: Report changes to IRC via kpaste
|
|
||||||
- `target`: IRC target (channel or user)
|
|
||||||
- `server`: IRC server (default: irc.r)
|
|
||||||
- `messagePrefix`: Prefix for messages (default: "change detected: ")
|
|
||||||
- `nick`: Nick to use (default: watcher name + "-watcher")
|
|
||||||
- `retiolumLink`: Use retiolum link (default: false)
|
|
||||||
|
|
||||||
## Service Management
|
## Service Management
|
||||||
|
|
||||||
### Systemd Integration
|
### systemd Integration
|
||||||
|
|
||||||
Each watcher gets its own systemd service and timer:
|
Each watcher gets its own systemd service and timer:
|
||||||
|
|
||||||
@@ -190,11 +116,10 @@ systemctl start panoptikon-github-meta
|
|||||||
|
|
||||||
### Timer Configuration
|
### Timer Configuration
|
||||||
|
|
||||||
Timers use systemd.timer syntax. Common examples:
|
Timers use systemd timer syntax. Common examples:
|
||||||
|
|
||||||
- `*:0/5` - Every 5 minutes
|
- `*:0/5` - Every 5 minutes
|
||||||
- `daily` - Once per day
|
- `daily` - Once per day
|
||||||
- `Mon..Fri 9:00-17:00` - Weekdays during business hours
|
|
||||||
- `*:0/15` - Every 15 minutes
|
- `*:0/15` - Every 15 minutes
|
||||||
- `weekly` - Once per week
|
- `weekly` - Once per week
|
||||||
|
|
||||||
@@ -204,135 +129,13 @@ See [systemd.time(7)](https://www.freedesktop.org/software/systemd/man/systemd.t
|
|||||||
|
|
||||||
- Watchers run as the `panoptikon` system user
|
- Watchers run as the `panoptikon` system user
|
||||||
- Scripts are executed in `/var/lib/panoptikon`
|
- Scripts are executed in `/var/lib/panoptikon`
|
||||||
- Use `loadCredential` to securely pass secrets
|
- Use `LoadCredential=` to securely pass secrets
|
||||||
- Scripts should be written defensively (use `set -euo pipefail`)
|
- Scripts should be written defensively (use `set -euo pipefail`)
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
1. **No output**: Check if the script runs correctly manually
|
|
||||||
2. **Permission denied**: Ensure the panoptikon user can access required resources
|
|
||||||
3. **Network issues**: Add `network-online.target` as a dependency
|
|
||||||
4. **Rate limiting**: Add randomized delays using `RandomizedDelaySec`
|
|
||||||
|
|
||||||
### Debug Mode
|
|
||||||
|
|
||||||
Enable debug logging:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
services.panoptikon.watchers.github-meta = {
|
|
||||||
# ...
|
|
||||||
script = ''
|
|
||||||
set -x # Enable debug output
|
|
||||||
${pkgs.curl}/bin/curl -sSL https://api.github.com/meta | ${pkgs.jq}/bin/jq
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
### Monitor Cryptocurrency Prices
|
See the [examples directory](./examples/) for complete configurations.
|
||||||
|
|
||||||
```nix
|
Run `nix run .#panoptikon-vm` to start a VM with Panoptikon and example watchers pre-configured.
|
||||||
{
|
|
||||||
services.panoptikon.enable = true;
|
|
||||||
|
|
||||||
services.panoptikon.watchers = {
|
|
||||||
bitcoin-price = {
|
|
||||||
script = pkgs.panoptikon.urlJSON {
|
|
||||||
jqScript = ".[0] | { name: .name, price: .quote.USD.price, change: .quote.USD.percent_change_24h }";
|
|
||||||
} "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=bitcoin";
|
|
||||||
frequency = "*:0/30";
|
|
||||||
reporters = [
|
|
||||||
(pkgs.writers.writeDash "btc-alert" '''
|
|
||||||
price=$(echo "$(cat)" | ${pkgs.jq}/bin/jq -r '.price')
|
|
||||||
change=$(echo "$(cat)" | ${pkgs.jq}/bin/jq -r '.change')
|
|
||||||
|
|
||||||
${pkgs.libnotify}/bin/notify-send \
|
|
||||||
"Bitcoin: $$price ($$change%)"
|
|
||||||
''')
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Monitor System Metrics
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
services.panoptikon.enable = true;
|
|
||||||
|
|
||||||
services.panoptikon.watchers = {
|
|
||||||
load-average = {
|
|
||||||
script = pkgs.writers.writeDash "load-average" '''
|
|
||||||
uptime | awk -F'load average:' '{print $2}' | awk '{print $1 " (1m), " $2 " (5m), " $3 " (15m)"}'
|
|
||||||
''';
|
|
||||||
frequency = "*:0/2"; # Every 2 minutes
|
|
||||||
reporters = [
|
|
||||||
(pkgs.writers.writeDash "load-alert" '''
|
|
||||||
load=$(echo "$(cat)" | awk '{print $1}' | tr -d ',')
|
|
||||||
if (( $(echo "$load > 2.0" | bc -l) )); then
|
|
||||||
${pkgs.libnotify}/bin/notify-send "High load: $$load"
|
|
||||||
fi
|
|
||||||
''')
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Monitor NixOS Updates
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
services.panoptikon.enable = true;
|
|
||||||
|
|
||||||
services.panoptikon.watchers = {
|
|
||||||
nixos-updates = {
|
|
||||||
script = pkgs.panoptikon.url "https://nixos.org/blog/";
|
|
||||||
frequency = "daily";
|
|
||||||
reporters = [
|
|
||||||
(pkgs.writers.writeDash "update-notify" '''
|
|
||||||
${pkgs.libnotify}/bin/notify-send "NixOS blog updated: $(cat | head -5)"
|
|
||||||
''')
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
### Adding Custom Reporters
|
|
||||||
|
|
||||||
Create a new reporter in the overlay:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
final: prev: {
|
|
||||||
panoptikon = prev.panoptikon // {
|
|
||||||
my-custom-reporter = {
|
|
||||||
endpoint,
|
|
||||||
authToken ? "",
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
prev.writers.writeDash "my-reporter" ''
|
|
||||||
${prev.curl}/bin/curl -X POST ''${endpoint} \
|
|
||||||
-H "Authorization: Bearer ''${authToken} \
|
|
||||||
-d "$(cat)"
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Contributing
|
|
||||||
|
|
||||||
1. Fork the repository
|
|
||||||
2. Create a feature branch
|
|
||||||
3. Add tests if applicable
|
|
||||||
4. Submit a pull request
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT License
|
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
# Advanced Panoptikon configuration with secrets and custom reporters
|
|
||||||
|
|
||||||
{
|
|
||||||
# Load secrets from agenix
|
|
||||||
secrets = import ../../secrets { };
|
|
||||||
|
|
||||||
services.panoptikon.enable = true;
|
|
||||||
|
|
||||||
services.panoptikon.watchers = {
|
|
||||||
# Monitor a private API with authentication
|
|
||||||
private-api = {
|
|
||||||
script = pkgs.writers.writeDash "private-api" '''
|
|
||||||
set -euo pipefail
|
|
||||||
${pkgs.curl}/bin/curl -sSL \
|
|
||||||
-H "Authorization: Bearer $API_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
https://api.example.com/data
|
|
||||||
''';
|
|
||||||
frequency = "hourly";
|
|
||||||
loadCredential = [ "API_TOKEN" ];
|
|
||||||
reporters = [
|
|
||||||
# Custom reporter that sends to a webhook
|
|
||||||
(pkgs.writers.writeDash "webhook-reporter" '''
|
|
||||||
${pkgs.curl}/bin/curl -X POST \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{\"watcher\": \"$PANOPTIKON_WATCHER\", \"changes\": $(cat)}" \
|
|
||||||
https://hooks.example.com/panoptikon
|
|
||||||
''')
|
|
||||||
# Also log to systemd journal
|
|
||||||
(pkgs.writers.writeDash "journal-log" '''
|
|
||||||
journalctl -t panoptikon-private-api --since "1 hour ago" | tail -5
|
|
||||||
''')
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
# Monitor cryptocurrency prices with alerts
|
|
||||||
crypto-monitor = {
|
|
||||||
script = pkgs.panoptikon.urlJSON {
|
|
||||||
jqScript = ".[0] | {
|
|
||||||
name: .name,
|
|
||||||
price: .quote.USD.price,
|
|
||||||
change24h: .quote.USD.percent_change_24h,
|
|
||||||
marketCap: .quote.USD.market_cap
|
|
||||||
}";
|
|
||||||
} "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=bitcoin";
|
|
||||||
frequency = "*:0/15";
|
|
||||||
reporters = [
|
|
||||||
(pkgs.writers.writeDash "btc-alert" '''
|
|
||||||
price=$(echo "$(cat)" | ${pkgs.jq}/bin/jq -r '.price')
|
|
||||||
change=$(echo "$(cat)" | ${pkgs.jq}/bin/jq -r '.change24h')
|
|
||||||
|
|
||||||
# Alert if price > $60,000 or change > 5%
|
|
||||||
if (( $(echo "$price > 60000" | bc -l) )) || (( $(echo "$change > 5" | bc -l) )); then
|
|
||||||
${pkgs.libnotify}/bin/notify-send \
|
|
||||||
"BTC Alert: $$price ($$change% change)"
|
|
||||||
fi
|
|
||||||
''')
|
|
||||||
# Log to file
|
|
||||||
(pkgs.writers.writeDash "price-logger" '''
|
|
||||||
echo "$(date): $(cat)" >> /var/log/panoptikon/btc-prices.log
|
|
||||||
''')
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
# Monitor system load with thresholds
|
|
||||||
system-health = {
|
|
||||||
script = pkgs.writers.writeDash "system-health" '''
|
|
||||||
set -euo pipefail
|
|
||||||
load=$(uptime | awk -F'load average:' '{print $2}' | awk '{print $1}' | tr -d ',')
|
|
||||||
mem=$(free -m | awk 'NR==2{printf "%.1f%%", $3*100/$2 }')
|
|
||||||
disk=$(df / | awk 'NR==2{printf "%.1f%%", $5}')
|
|
||||||
|
|
||||||
echo "load: $$load, mem: $$mem, disk: $$disk"
|
|
||||||
''';
|
|
||||||
frequency = "*:0/5";
|
|
||||||
reporters = [
|
|
||||||
(pkgs.writers.writeDash "health-alert" '''
|
|
||||||
load=$(echo "$(cat)" | awk -F',' '{print $1}' | awk '{print $2}')
|
|
||||||
mem=$(echo "$(cat)" | awk -F',' '{print $2}' | awk '{print $2}')
|
|
||||||
disk=$(echo "$(cat)" | awk -F',' '{print $3}' | awk '{print $2}')
|
|
||||||
|
|
||||||
# Alert if load > 2.0, mem > 80%, or disk > 90%
|
|
||||||
if (( $(echo "$load > 2.0" | bc -l) )) || (( $(echo "${mem%%%} > 80" | bc -l) )) || (( $(echo "${disk%%%} > 90" | bc -l) )); then
|
|
||||||
${pkgs.libnotify}/bin/notify-send \
|
|
||||||
"System Alert: Load=$$load, Mem=$$mem, Disk=$$disk"
|
|
||||||
fi
|
|
||||||
''')
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# Add monitoring user
|
|
||||||
users.extraUsers.panoptikon = {
|
|
||||||
isSystemUser = true;
|
|
||||||
createHome = true;
|
|
||||||
home = "/var/lib/panoptikon";
|
|
||||||
group = "panoptikon";
|
|
||||||
description = "Panoptikon monitoring service";
|
|
||||||
openssh.authorizedKeys.keys = [
|
|
||||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK..." # Monitoring access key
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
# Configure log rotation
|
|
||||||
services.logrotate = {
|
|
||||||
enable = true;
|
|
||||||
config = {
|
|
||||||
rotate = 14;
|
|
||||||
compress = true;
|
|
||||||
delaycompress = true;
|
|
||||||
missingok = true;
|
|
||||||
notifempty = true;
|
|
||||||
create = "644 panoptikon panoptikon";
|
|
||||||
};
|
|
||||||
files = [
|
|
||||||
"/var/log/panoptikon/*.log"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
18
examples/bitcoin.nix
Normal file
18
examples/bitcoin.nix
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
|
services.panoptikon.enable = true;
|
||||||
|
services.panoptikon.watchers = {
|
||||||
|
bitcoin-price = {
|
||||||
|
script = pkgs.panoptikonWatchers.json {
|
||||||
|
jqScript = ".[]|{name: .name, price: .current_price, change: .price_change_24h}";
|
||||||
|
} "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=bitcoin";
|
||||||
|
frequency = "*:0/30";
|
||||||
|
reporters = [
|
||||||
|
(pkgs.panoptikonReporters.telegram {
|
||||||
|
chatId = "123";
|
||||||
|
tokenPath = pkgs.writeText "my-telegram-token.txt" "123:abc";
|
||||||
|
})
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
15
examples/nixos.nix
Normal file
15
examples/nixos.nix
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
|
services.panoptikon.enable = true;
|
||||||
|
|
||||||
|
services.panoptikon.watchers = {
|
||||||
|
nixos-updates = {
|
||||||
|
script = pkgs.panoptikonWatchers.html "https://nixos.org/blog/";
|
||||||
|
frequency = "daily";
|
||||||
|
reporters = [
|
||||||
|
(pkgs.panoptikonReporters.mail { recipient = "admin@example.org"; })
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,30 +1,45 @@
|
|||||||
# Simple Panoptikon configuration
|
{ pkgs, ... }:
|
||||||
|
|
||||||
{
|
{
|
||||||
services.panoptikon.enable = true;
|
services.panoptikon.enable = true;
|
||||||
|
|
||||||
services.panoptikon.watchers = {
|
services.panoptikon.watchers = {
|
||||||
# Monitor GitHub metadata every 5 minutes
|
|
||||||
github-meta = {
|
github-meta = {
|
||||||
script = pkgs.writers.writeDash "github-meta" '''
|
script = pkgs.panoptikonWatchers.json { } "https://api.github.com/meta";
|
||||||
${pkgs.curl}/bin/curl -sSL https://api.github.com/meta | ${pkgs.jq}/bin/jq
|
|
||||||
''';
|
|
||||||
frequency = "*:0/5";
|
frequency = "*:0/5";
|
||||||
reporters = [
|
reporters = [
|
||||||
(pkgs.writers.writeDash "notify" '''
|
(pkgs.panoptikonReporters.wall { })
|
||||||
${pkgs.libnotify}/bin/notify-send "$PANOPTIKON_WATCHER has changed."
|
(pkgs.panoptikonReporters.irc {
|
||||||
''')
|
target = "kmein";
|
||||||
|
})
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
cock-canary = {
|
||||||
|
script = pkgs.panoptikonWatchers.plain "https://cock.li/canary.asc.txt";
|
||||||
|
frequency = "daily";
|
||||||
|
reporters = [
|
||||||
|
(pkgs.panoptikonReporters.irc {
|
||||||
|
target = "kmein";
|
||||||
|
})
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
"4d2-canary" = {
|
||||||
|
script = pkgs.panoptikonWatchers.plain "https://4d2.org/canary.txt";
|
||||||
|
frequency = "daily";
|
||||||
|
reporters = [
|
||||||
|
(pkgs.panoptikonReporters.irc {
|
||||||
|
target = "kmein";
|
||||||
|
})
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
# Monitor a website for news
|
# Monitor a website for news
|
||||||
nixos-news = {
|
nixos-news = {
|
||||||
script = pkgs.panoptikon.urlSelector "#news h2" "https://nixos.org/blog/";
|
script = pkgs.panoptikonWatchers.htmlSelector "article h2" "https://nixos.org/blog/";
|
||||||
frequency = "daily";
|
frequency = "daily";
|
||||||
reporters = [
|
reporters = [
|
||||||
(pkgs.writers.writeDash "news-alert" '''
|
(pkgs.panoptikonReporters.wall { })
|
||||||
${pkgs.libnotify}/bin/notify-send "New NixOS blog post: $(cat | head -1)"
|
|
||||||
''')
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
16
examples/system.nix
Normal file
16
examples/system.nix
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
|
services.panoptikon.enable = true;
|
||||||
|
|
||||||
|
services.panoptikon.watchers = {
|
||||||
|
boot-space = {
|
||||||
|
script = pkgs.writers.writeDash "load-average" ''
|
||||||
|
${pkgs.coreutils}/bin/df -h /boot
|
||||||
|
'';
|
||||||
|
frequency = "*:0/2";
|
||||||
|
reporters = [
|
||||||
|
(pkgs.panoptikonReporters.wall { })
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
49
flake.nix
49
flake.nix
@@ -1,13 +1,50 @@
|
|||||||
{
|
{
|
||||||
description = "Panoptikon - Website and command output monitoring";
|
description = "Panoptikon – Watch the world from NixOS";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = inputs: {
|
outputs =
|
||||||
nixosModules.default = ./module.nix;
|
inputs:
|
||||||
overlays.default = ./overlay.nix;
|
let
|
||||||
};
|
eachSupportedSystem = inputs.nixpkgs.lib.genAttrs inputs.nixpkgs.lib.systems.flakeExposed;
|
||||||
}
|
inherit (inputs.nixpkgs) lib;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
nixosModules.default = import nix/module.nix;
|
||||||
|
overlays.default = import nix/overlay.nix;
|
||||||
|
|
||||||
|
apps = eachSupportedSystem (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
nixosSystem = lib.nixosSystem {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
modules = [
|
||||||
|
inputs.self.nixosModules.default
|
||||||
|
{
|
||||||
|
nixpkgs.overlays = [ inputs.self.overlay ];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
virtualisation.vmVariant = {
|
||||||
|
virtualisation.graphics = false;
|
||||||
|
};
|
||||||
|
services.getty.autologinUser = "root";
|
||||||
|
system.stateVersion = lib.trivial.release;
|
||||||
|
}
|
||||||
|
./examples/simple.nix
|
||||||
|
./examples/bitcoin.nix
|
||||||
|
./examples/nixos.nix
|
||||||
|
./examples/system.nix
|
||||||
|
];
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
panoptikon-vm = {
|
||||||
|
type = "app";
|
||||||
|
program = lib.getExe nixosSystem.config.system.build.vm;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -65,12 +65,13 @@
|
|||||||
config =
|
config =
|
||||||
let
|
let
|
||||||
cfg = config.services.panoptikon;
|
cfg = config.services.panoptikon;
|
||||||
|
stateDir = "/var/lib/panoptikon";
|
||||||
in
|
in
|
||||||
lib.mkIf cfg.enable {
|
lib.mkIf cfg.enable {
|
||||||
users.extraUsers.panoptikon = {
|
users.extraUsers.panoptikon = {
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
createHome = true;
|
createHome = true;
|
||||||
home = "/var/lib/panoptikon";
|
home = stateDir;
|
||||||
group = "panoptikon";
|
group = "panoptikon";
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,37 +86,62 @@
|
|||||||
|
|
||||||
systemd.services = lib.attrsets.mapAttrs' (
|
systemd.services = lib.attrsets.mapAttrs' (
|
||||||
watcherName: watcherOptions:
|
watcherName: watcherOptions:
|
||||||
|
let
|
||||||
|
# Absolute paths for the state files
|
||||||
|
currentFile = "${stateDir}/${watcherName}";
|
||||||
|
oldFile = "${stateDir}/${watcherName}.old";
|
||||||
|
in
|
||||||
lib.nameValuePair "panoptikon-${watcherName}" {
|
lib.nameValuePair "panoptikon-${watcherName}" {
|
||||||
enable = true;
|
enable = true;
|
||||||
startAt = watcherOptions.frequency;
|
startAt = watcherOptions.frequency;
|
||||||
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
User = "panoptikon";
|
User = "panoptikon";
|
||||||
Group = "panoptikon";
|
Group = "panoptikon";
|
||||||
WorkingDirectory = "/var/lib/panoptikon";
|
|
||||||
RestartSec = toString (60 * 60);
|
# 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";
|
Restart = "on-failure";
|
||||||
LoadCredential = watcherOptions.loadCredential;
|
LoadCredential = watcherOptions.loadCredential;
|
||||||
};
|
};
|
||||||
unitConfig = {
|
|
||||||
StartLimitIntervalSec = "300";
|
|
||||||
StartLimitBurst = "5";
|
|
||||||
};
|
|
||||||
environment.PANOPTIKON_WATCHER = watcherName;
|
environment.PANOPTIKON_WATCHER = watcherName;
|
||||||
wants = [ "network-online.target" ];
|
|
||||||
script = ''
|
script = ''
|
||||||
set -fux
|
set -efu # Removed -x to keep tokens out of logs if they leak via env
|
||||||
${watcherOptions.script} > ${lib.escapeShellArg watcherName}
|
|
||||||
diff_output=$(${pkgs.diffutils}/bin/diff --new-file ${
|
# 1. Run watcher and save to the persistent state dir
|
||||||
lib.escapeShellArg (watcherName + ".old")
|
${watcherOptions.script} > "${currentFile}"
|
||||||
} ${lib.escapeShellArg watcherName} || :)
|
|
||||||
if [ -n "$diff_output" ]
|
# 2. Ensure .old exists so diff doesn't crash on first run
|
||||||
then
|
[ -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" (
|
${lib.strings.concatMapStringsSep "\n" (
|
||||||
reporter: ''echo "$diff_output" | ${reporter} || :''
|
reporter: ''echo "$diff_output" | ${reporter} || :''
|
||||||
) watcherOptions.reporters}
|
) watcherOptions.reporters}
|
||||||
fi
|
fi
|
||||||
mv ${lib.escapeShellArg watcherName} ${lib.escapeShellArg (watcherName + ".old")}
|
|
||||||
|
# 5. Rotate state
|
||||||
|
mv "${currentFile}" "${oldFile}"
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
) cfg.watchers;
|
) 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}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
47
overlay.nix
47
overlay.nix
@@ -1,47 +0,0 @@
|
|||||||
final: prev: {
|
|
||||||
panoptikon =
|
|
||||||
let
|
|
||||||
lib = prev.lib;
|
|
||||||
in
|
|
||||||
lib.recursiveUpdate prev.panoptikon {
|
|
||||||
url =
|
|
||||||
address:
|
|
||||||
prev.writers.writeDash "watch-url" ''
|
|
||||||
${prev.curl}/bin/curl -sSL ${lib.escapeShellArg address} \
|
|
||||||
| ${prev.python3Packages.html2text}/bin/html2text --decode-errors=ignore
|
|
||||||
'';
|
|
||||||
urlSelector =
|
|
||||||
selector: address:
|
|
||||||
prev.writers.writeDash "watch-url-selector" ''
|
|
||||||
${prev.curl}/bin/curl -sSL ${lib.escapeShellArg address} \
|
|
||||||
| ${prev.htmlq}/bin/htmlq ${lib.escapeShellArg selector} \
|
|
||||||
| ${prev.python3Packages.html2text}/bin/html2text
|
|
||||||
'';
|
|
||||||
urlJSON =
|
|
||||||
{
|
|
||||||
jqScript ? ".",
|
|
||||||
}:
|
|
||||||
address:
|
|
||||||
prev.writers.writeDash "watch-url-json" ''
|
|
||||||
${prev.curl}/bin/curl -sSL ${lib.escapeShellArg address} | ${prev.jq}/bin/jq -f ${prev.writeText "script.jq" jqScript}
|
|
||||||
'';
|
|
||||||
|
|
||||||
# reporter scripts
|
|
||||||
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}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user