diff --git a/log-alert/config.json b/log-alert/config.json index 74f8cc8..6b477e2 100644 --- a/log-alert/config.json +++ b/log-alert/config.json @@ -1,27 +1,24 @@ { - "log-fetchers": [ - { - "name": "loki-fileserver", + "log-fetchers": { + "loki-fileserver": { "type": "loki", "config": { - "url": "http://fileserver.home:3100" + "url-from-env": "{LOKI_URL}" } } - ], - "alert-managers":[ - { - "name": "gotify-paris", + }, + "alert-managers": { + "gotify-paris": { "type": "gotify", "config": { - "url": "https://paris.nappez.com/gotify/message", + "url-from-env": "{GOTIFY_URL}", "token-from-env": "{GOTIFY_TOKEN}" } } - ], - "log-alerts": [ - { - "name": "SSH outside connection", - "check-interval": 60, + }, + "alerting-rules": { + "ssh-outside": { + "check-interval": 30, "log-fetcher": { "name": "loki-fileserver", "filters": { @@ -37,16 +34,21 @@ "config": { "match": "Accepted (?P\\w+) for (?P\\w+) from (?P[^\\s]+)" } + }, + { + "type": "geolocation", + "config": { + "source-field": "ip" + } } ], "alert-manager": { "name": "gotify-paris", "title": "Outside SSH login", - "message": "New SSH login for {username} on {instance} from ip {ip} (method: {method})" + "message": "New SSH login for {username} on {instance} from ip {ip} (country: {country}, provider: {isp}, method: {method})" } }, - { - "name": "SSH local connection", + "ssh-local": { "check-interval": 30, "log-fetcher": { "name": "loki-fileserver", @@ -71,5 +73,5 @@ "message": "New SSH login for {username} on {instance} from ip {ip} (method: {method})" } } - ] + } } \ No newline at end of file diff --git a/log-alert/config.schema.json b/log-alert/config.schema.json index f018854..1c279e8 100644 --- a/log-alert/config.schema.json +++ b/log-alert/config.schema.json @@ -2,80 +2,43 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "Log Alert configuration schema", "type": "object", - "required": ["log-fetchers", "alert-managers", "log-alerts"], + "required": ["log-fetchers", "alert-managers", "alerting-rules"], "properties": { "log-fetchers": { - "type": "array", - "items": { + "type": "object", + "additionalProperties": { "type": "object", - "required": ["name", "type", "config"], + "required": ["type", "config"], "properties": { - "name": { "type": "string" }, "type": { "type": "string", "enum": ["loki"] }, - "config": { - "type": "object", - "properties": { - "url": { "type": "string", "format": "uri" } - }, - "required": ["url"], - "additionalProperties": true - } - }, - "additionalProperties": false + "config": { "type": "object" } + } } }, "alert-managers": { - "type": "array", - "items": { + "type": "object", + "additionalProperties": { "type": "object", - "required": ["name", "type", "config"], + "required": ["type", "config"], "properties": { - "name": { "type": "string" }, "type": { "type": "string", "enum": ["gotify"] }, - "config": { - "type": "object", - "properties": { - "url": { "type": "string", "format": "uri" }, - "token": { "type": "string" }, - "token-from-env": { "type": "string" } - }, - "required": ["url"], - "anyOf": [ - { "required": ["token"] }, - { "required": ["token-from-env"] } - ], - "additionalProperties": true - } - }, - "additionalProperties": false + "config": { "type": "object" } + } } }, - "log-alerts": { - "type": "array", - "items": { + "alerting-rules": { + "type": "object", + "additionalProperties": { "type": "object", - "required": ["name", "log-fetcher", "filters", "alert-manager"], + "required": ["check-interval", "log-fetcher", "filters", "alert-manager"], "properties": { - "name": { "type": "string" }, "check-interval": { "type": "number", "minimum": 0 }, "log-fetcher": { "type": "object", "required": ["name"], "properties": { - "name": { "type": "string" }, - "filters": { - "type": "object", - "properties": { - "labels": { - "type": "object", - "additionalProperties": { "type": "string" } - }, - "text": { "type": "string" } - }, - "additionalProperties": false - } - }, - "additionalProperties": false + "name": { "type": "string" } + } }, "filters": { "type": "array", @@ -83,33 +46,22 @@ "type": "object", "required": ["type", "config"], "properties": { - "type": { "type": "string", "enum": ["regexp"] }, + "type": { "type": "string", "enum": ["regexp", "geolocation"] }, "config": { - "type": "object", - "required": ["match"], - "properties": { - "match": { "type": "string" } - }, - "additionalProperties": false + "type": "object" } - }, - "additionalProperties": false + } } }, "alert-manager": { "type": "object", "required": ["name"], "properties": { - "name": { "type": "string" }, - "title": { "type": "string" }, - "message": { "type": "string" } - }, - "additionalProperties": false + "name": { "type": "string" } + } } - }, - "additionalProperties": false + } } } - }, - "additionalProperties": false + } } \ No newline at end of file diff --git a/log-alert/log-alert.py b/log-alert/log-alert.py index 085b901..f7c70d3 100755 --- a/log-alert/log-alert.py +++ b/log-alert/log-alert.py @@ -103,6 +103,31 @@ class RegexpFilter(Filter): logger.debug(f"Regex did not match for pattern '{self.match}' in log: {log.get('log')}") return None +# Geolocation Filter +class GeolocationFilter(Filter): + """Concrete implementation for Geolocation filter.""" + + def __init__(self, config: Dict[str, Any]): + self.source_field = config["source-field"] + + def filter(self, log: Dict[str, Any]) -> Dict[str, Any]: + ip_address = log.get("labels", {}).get(self.source_field) + if not ip_address: + logger.warning("No IP address found in log labels for geolocation") + else: + try: + response = requests.get(f"http://ip-api.com/json/{ip_address}").json() + if response["status"] == "success": + logger.debug(f"Found info {response} for IP {ip_address}") + del response["status"] + del response["query"] + log.setdefault("labels", {}).update(response) + else: + logger.warning("No info found for IP {ip_address}") + except requests.exceptions.RequestException as e: + logger.error(f"Error fetching geolocation for IP {ip_address}: {e}") + return log + # Gotify Alert Manager class GotifyAlertManager(AlertManager): """Concrete implementation for Gotify alert manager.""" @@ -129,7 +154,6 @@ class AlertRule: """Represents an alert rule with filters and alert template.""" def __init__(self, log_fetchers: LogFetcher, alert_managers: AlertManager, config: Dict[str, Any]): - self.name = config["name"] self.log_fetcher = log_fetchers[config["log-fetcher"]["name"]] self.fetcher_filters = config["log-fetcher"].get("filters", {}) self.check_interval = config.get("check-interval", 60) @@ -137,6 +161,8 @@ class AlertRule: for filter in config.get("filters", []): if filter["type"] == "regexp": self.filters.append(RegexpFilter(filter["config"])) + elif filter["type"] == "geolocation": + self.filters.append(GeolocationFilter(filter["config"])) else: raise ValueError(f"Unsupported filter type: {filter['type']}") self.alert_manager = alert_managers[config["alert-manager"]["name"]] @@ -146,7 +172,6 @@ class AlertRule: self.next_run = time.time() def run(self) -> None: - logger.debug(f"Processing rule: {self.name}") logs = self.log_fetcher.fetch_logs(self.fetcher_filters, self.last_run, self.next_run) for log_entry in logs: logger.debug(f"Checking log: {log_entry['log']}") @@ -170,12 +195,14 @@ class LogAlertApp: self.config = self._load_config(config_path) logger.debug(f"Configuration loaded: {self.config}") self.log_fetchers = {} - for fetcher in self.config["log-fetchers"]: - self.log_fetchers[fetcher["name"]] = self._init_log_fetcher(fetcher) + for key, fetcher in self.config["log-fetchers"].items(): + self.log_fetchers[key] = self._init_log_fetcher(fetcher) self.alert_managers = {} - for manager in self.config["alert-managers"]: - self.alert_managers[manager["name"]] = self._init_alert_manager(manager) - self.alert_rules = [AlertRule(self.log_fetchers, self.alert_managers, rule) for rule in self.config["log-alerts"]] + for key, manager in self.config["alert-managers"].items(): + self.alert_managers[key] = self._init_alert_manager(manager) + self.alert_rules = {} + for key, rule in self.config["alerting-rules"].items(): + self.alert_rules[key] = AlertRule(self.log_fetchers, self.alert_managers, rule) def _load_config(self, config_path: str) -> Dict[str, Any]: """Load the configuration from a JSON file and validate it with JSON Schema.""" @@ -243,8 +270,9 @@ class LogAlertApp: def run(self) -> None: """Fetch logs, check for matches, and send alerts.""" while True: - for rule in self.alert_rules: + for name, rule in self.alert_rules.items(): if time.time() >= rule.next_run: + logger.debug(f"Processing rule: {name}") rule.run() time.sleep(5) diff --git a/log-alert/requirements.txt b/log-alert/requirements.txt index b420af9..1d66c4f 100644 --- a/log-alert/requirements.txt +++ b/log-alert/requirements.txt @@ -1,2 +1,2 @@ requests -jsonschema>=4.0.0 +jsonschema>=4.0.0 \ No newline at end of file