From 0e177210f6925525b7049df0f65c5e6aa15a0f54 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 7 Jan 2025 02:52:19 +0300 Subject: [PATCH] login alerts --- README.md | 11 ++++- alerting/alerts.py | 3 -- alerting/enum.py | 2 +- assets/lego-login-alert | 2 + .../lego-monitoring.service | 0 config.example.json | 3 ++ misc/config.py | 6 +++ send_login_alert.py | 47 +++++++++++++++++++ wrappers/login_wrapper.sh | 12 +++++ wrappers/send_login_alert.sh | 4 ++ 10 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 assets/lego-login-alert rename lego-monitoring.service => assets/lego-monitoring.service (100%) create mode 100644 send_login_alert.py create mode 100755 wrappers/login_wrapper.sh create mode 100755 wrappers/send_login_alert.sh diff --git a/README.md b/README.md index 627f7a7..75eb583 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,17 @@ DISCLAIMER: This repository does not have anything to do with the LEGO Group. "l * Copy `config.example.json` to `config.json`, edit as necessary * Run `alerting/login.py` once to login into Matrix +### Setting up login alerts + +* Copy `lego-login-alert` to your `/etc/sudoers.d` +* Add this to your `/etc/ssh/sshd_config`: +``` +# login alerts +ForceCommand /opt/lego-monitoring/wrappers/login_wrapper.sh +``` + ## Running * `prettyprint.py` -- check and print all sensors * `service.py` -- launch service -* `lego-monitoring.service` is a systemd unit that starts `service.py` +* `assets/lego-monitoring.service` is a systemd unit that starts `service.py` diff --git a/alerting/alerts.py b/alerting/alerts.py index 743b1a0..aaf04c7 100644 --- a/alerting/alerts.py +++ b/alerting/alerts.py @@ -1,13 +1,10 @@ -import json from dataclasses import dataclass from typing import Optional -import aiofiles import nio from alerting.enum import AlertType, Severity from misc import cvars -from misc.common import CONFIG_FILE from misc.config import get_config diff --git a/alerting/enum.py b/alerting/enum.py index e902fce..87e4b2b 100644 --- a/alerting/enum.py +++ b/alerting/enum.py @@ -8,7 +8,7 @@ class AlertType(StrEnum): CPU = "CPU" TEMP = "TEMP" VULN = "VULN" - LOGIN = "LOGIN" # TODO + LOGIN = "LOGIN" SMART = "SMART" # TODO RAID = "RAID" DISKS = "DISKS" diff --git a/assets/lego-login-alert b/assets/lego-login-alert new file mode 100644 index 0000000..b816f8b --- /dev/null +++ b/assets/lego-login-alert @@ -0,0 +1,2 @@ +Defaults env_keep += "SSH_CLIENT" +ALL ALL=(ALL:ALL) NOPASSWD: /opt/lego-monitoring/wrappers/send_login_alert.sh diff --git a/lego-monitoring.service b/assets/lego-monitoring.service similarity index 100% rename from lego-monitoring.service rename to assets/lego-monitoring.service diff --git a/config.example.json b/config.example.json index 6aabe09..6fe847a 100644 --- a/config.example.json +++ b/config.example.json @@ -22,6 +22,9 @@ "severity": "CRITICAL" } ] + }, + "login": { + "hostname": "example.com" } } } diff --git a/misc/config.py b/misc/config.py index ebb6598..6b1fb0c 100644 --- a/misc/config.py +++ b/misc/config.py @@ -38,11 +38,17 @@ class CheckWearoutConfig(NestedDeserializableDataclass): disks: list[CheckWearoutDiskConfig] +@dataclass +class CheckLoginConfig: + hostname: str + + @dataclass class ChecksConfig(NestedDeserializableDataclass): docker_registry: CheckDockerRegistryConfig raid: CheckRaidConfig wearout: CheckWearoutConfig + login: CheckLoginConfig @dataclass diff --git a/send_login_alert.py b/send_login_alert.py new file mode 100644 index 0000000..b8f8898 --- /dev/null +++ b/send_login_alert.py @@ -0,0 +1,47 @@ +import asyncio +import os +import socket +import sys + +from alerting import alerts +from alerting.enum import AlertType, Severity +from misc.config import get_config + + +async def main(): + check_config = get_config().checks.login + + try: + from_where = os.environ["SSH_CLIENT"].split()[0] + except: + from_where = os.ttyname(sys.stdout.fileno()) + is_local = True + else: + is_local = False + + try: + actual_user = os.environ["SUDO_USER"] + except Exception as exc: + await alerts.send_alert( + alerts.Alert( + alert_type=AlertType.ERROR, + message=f"Failed to determine username for login from {from_where}, see logs", + severity=Severity.CRITICAL, + ) + ) + return + + if not is_local: + rdns_result = socket.getnameinfo((from_where, 0), 0)[0] + message = f"Login from {from_where} as {actual_user} on `{check_config.hostname}`" + html_message = f"Login from {from_where} ({rdns_result}) as {actual_user} on {check_config.hostname}" + else: + message = f"Login from {from_where} as {actual_user} on {check_config.hostname}" + html_message = f"Login from {from_where} as {actual_user} on {check_config.hostname}" + + alert = alerts.Alert(alert_type=AlertType.LOGIN, message=message, severity=Severity.INFO, html_message=html_message) + await alerts.send_alert(alert) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/wrappers/login_wrapper.sh b/wrappers/login_wrapper.sh new file mode 100755 index 0000000..a69899b --- /dev/null +++ b/wrappers/login_wrapper.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +mydir=$(dirname "$0") +sudo "$mydir/send_login_alert.sh" + +shell=$(getent passwd $LOGNAME | cut -d: -f7) +if [[ -n $SSH_ORIGINAL_COMMAND ]] # command given, so run it +then + exec "$shell" -c "$SSH_ORIGINAL_COMMAND" +else # no command, so interactive login shell + exec "$shell" -il +fi diff --git a/wrappers/send_login_alert.sh b/wrappers/send_login_alert.sh new file mode 100755 index 0000000..d10a433 --- /dev/null +++ b/wrappers/send_login_alert.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +mydir=$(dirname "$0") +"$mydir/../.venv/bin/python" "$mydir/../send_login_alert.py"