Added the geolocation filter and improved the config file format

This commit is contained in:
2025-10-18 15:43:33 +02:00
parent b06a39df3a
commit 227f1d48e1
4 changed files with 81 additions and 99 deletions

View File

@@ -1,27 +1,24 @@
{ {
"log-fetchers": [ "log-fetchers": {
{ "loki-fileserver": {
"name": "loki-fileserver",
"type": "loki", "type": "loki",
"config": { "config": {
"url": "http://fileserver.home:3100" "url-from-env": "{LOKI_URL}"
} }
} }
], },
"alert-managers":[ "alert-managers": {
{ "gotify-paris": {
"name": "gotify-paris",
"type": "gotify", "type": "gotify",
"config": { "config": {
"url": "https://paris.nappez.com/gotify/message", "url-from-env": "{GOTIFY_URL}",
"token-from-env": "{GOTIFY_TOKEN}" "token-from-env": "{GOTIFY_TOKEN}"
} }
} }
], },
"log-alerts": [ "alerting-rules": {
{ "ssh-outside": {
"name": "SSH outside connection", "check-interval": 30,
"check-interval": 60,
"log-fetcher": { "log-fetcher": {
"name": "loki-fileserver", "name": "loki-fileserver",
"filters": { "filters": {
@@ -37,16 +34,21 @@
"config": { "config": {
"match": "Accepted (?P<method>\\w+) for (?P<username>\\w+) from (?P<ip>[^\\s]+)" "match": "Accepted (?P<method>\\w+) for (?P<username>\\w+) from (?P<ip>[^\\s]+)"
} }
},
{
"type": "geolocation",
"config": {
"source-field": "ip"
}
} }
], ],
"alert-manager": { "alert-manager": {
"name": "gotify-paris", "name": "gotify-paris",
"title": "Outside SSH login", "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})"
} }
}, },
{ "ssh-local": {
"name": "SSH local connection",
"check-interval": 30, "check-interval": 30,
"log-fetcher": { "log-fetcher": {
"name": "loki-fileserver", "name": "loki-fileserver",
@@ -71,5 +73,5 @@
"message": "New SSH login for {username} on {instance} from ip {ip} (method: {method})" "message": "New SSH login for {username} on {instance} from ip {ip} (method: {method})"
} }
} }
] }
} }

View File

@@ -2,80 +2,43 @@
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "Log Alert configuration schema", "title": "Log Alert configuration schema",
"type": "object", "type": "object",
"required": ["log-fetchers", "alert-managers", "log-alerts"], "required": ["log-fetchers", "alert-managers", "alerting-rules"],
"properties": { "properties": {
"log-fetchers": { "log-fetchers": {
"type": "array", "type": "object",
"items": { "additionalProperties": {
"type": "object", "type": "object",
"required": ["name", "type", "config"], "required": ["type", "config"],
"properties": { "properties": {
"name": { "type": "string" },
"type": { "type": "string", "enum": ["loki"] }, "type": { "type": "string", "enum": ["loki"] },
"config": { "config": { "type": "object" }
"type": "object", }
"properties": {
"url": { "type": "string", "format": "uri" }
},
"required": ["url"],
"additionalProperties": true
}
},
"additionalProperties": false
} }
}, },
"alert-managers": { "alert-managers": {
"type": "array", "type": "object",
"items": { "additionalProperties": {
"type": "object", "type": "object",
"required": ["name", "type", "config"], "required": ["type", "config"],
"properties": { "properties": {
"name": { "type": "string" },
"type": { "type": "string", "enum": ["gotify"] }, "type": { "type": "string", "enum": ["gotify"] },
"config": { "config": { "type": "object" }
"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
} }
}, },
"log-alerts": { "alerting-rules": {
"type": "array", "type": "object",
"items": { "additionalProperties": {
"type": "object", "type": "object",
"required": ["name", "log-fetcher", "filters", "alert-manager"], "required": ["check-interval", "log-fetcher", "filters", "alert-manager"],
"properties": { "properties": {
"name": { "type": "string" },
"check-interval": { "type": "number", "minimum": 0 }, "check-interval": { "type": "number", "minimum": 0 },
"log-fetcher": { "log-fetcher": {
"type": "object", "type": "object",
"required": ["name"], "required": ["name"],
"properties": { "properties": {
"name": { "type": "string" }, "name": { "type": "string" }
"filters": { }
"type": "object",
"properties": {
"labels": {
"type": "object",
"additionalProperties": { "type": "string" }
},
"text": { "type": "string" }
},
"additionalProperties": false
}
},
"additionalProperties": false
}, },
"filters": { "filters": {
"type": "array", "type": "array",
@@ -83,33 +46,22 @@
"type": "object", "type": "object",
"required": ["type", "config"], "required": ["type", "config"],
"properties": { "properties": {
"type": { "type": "string", "enum": ["regexp"] }, "type": { "type": "string", "enum": ["regexp", "geolocation"] },
"config": { "config": {
"type": "object", "type": "object"
"required": ["match"],
"properties": {
"match": { "type": "string" }
},
"additionalProperties": false
} }
}, }
"additionalProperties": false
} }
}, },
"alert-manager": { "alert-manager": {
"type": "object", "type": "object",
"required": ["name"], "required": ["name"],
"properties": { "properties": {
"name": { "type": "string" }, "name": { "type": "string" }
"title": { "type": "string" }, }
"message": { "type": "string" }
},
"additionalProperties": false
} }
}, }
"additionalProperties": false
} }
} }
}, }
"additionalProperties": false
} }

View File

@@ -103,6 +103,31 @@ class RegexpFilter(Filter):
logger.debug(f"Regex did not match for pattern '{self.match}' in log: {log.get('log')}") logger.debug(f"Regex did not match for pattern '{self.match}' in log: {log.get('log')}")
return None 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 # Gotify Alert Manager
class GotifyAlertManager(AlertManager): class GotifyAlertManager(AlertManager):
"""Concrete implementation for Gotify alert manager.""" """Concrete implementation for Gotify alert manager."""
@@ -129,7 +154,6 @@ class AlertRule:
"""Represents an alert rule with filters and alert template.""" """Represents an alert rule with filters and alert template."""
def __init__(self, log_fetchers: LogFetcher, alert_managers: AlertManager, config: Dict[str, Any]): 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.log_fetcher = log_fetchers[config["log-fetcher"]["name"]]
self.fetcher_filters = config["log-fetcher"].get("filters", {}) self.fetcher_filters = config["log-fetcher"].get("filters", {})
self.check_interval = config.get("check-interval", 60) self.check_interval = config.get("check-interval", 60)
@@ -137,6 +161,8 @@ class AlertRule:
for filter in config.get("filters", []): for filter in config.get("filters", []):
if filter["type"] == "regexp": if filter["type"] == "regexp":
self.filters.append(RegexpFilter(filter["config"])) self.filters.append(RegexpFilter(filter["config"]))
elif filter["type"] == "geolocation":
self.filters.append(GeolocationFilter(filter["config"]))
else: else:
raise ValueError(f"Unsupported filter type: {filter['type']}") raise ValueError(f"Unsupported filter type: {filter['type']}")
self.alert_manager = alert_managers[config["alert-manager"]["name"]] self.alert_manager = alert_managers[config["alert-manager"]["name"]]
@@ -146,7 +172,6 @@ class AlertRule:
self.next_run = time.time() self.next_run = time.time()
def run(self) -> None: 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) logs = self.log_fetcher.fetch_logs(self.fetcher_filters, self.last_run, self.next_run)
for log_entry in logs: for log_entry in logs:
logger.debug(f"Checking log: {log_entry['log']}") logger.debug(f"Checking log: {log_entry['log']}")
@@ -170,12 +195,14 @@ class LogAlertApp:
self.config = self._load_config(config_path) self.config = self._load_config(config_path)
logger.debug(f"Configuration loaded: {self.config}") logger.debug(f"Configuration loaded: {self.config}")
self.log_fetchers = {} self.log_fetchers = {}
for fetcher in self.config["log-fetchers"]: for key, fetcher in self.config["log-fetchers"].items():
self.log_fetchers[fetcher["name"]] = self._init_log_fetcher(fetcher) self.log_fetchers[key] = self._init_log_fetcher(fetcher)
self.alert_managers = {} self.alert_managers = {}
for manager in self.config["alert-managers"]: for key, manager in self.config["alert-managers"].items():
self.alert_managers[manager["name"]] = self._init_alert_manager(manager) self.alert_managers[key] = self._init_alert_manager(manager)
self.alert_rules = [AlertRule(self.log_fetchers, self.alert_managers, rule) for rule in self.config["log-alerts"]] 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]: def _load_config(self, config_path: str) -> Dict[str, Any]:
"""Load the configuration from a JSON file and validate it with JSON Schema.""" """Load the configuration from a JSON file and validate it with JSON Schema."""
@@ -243,8 +270,9 @@ class LogAlertApp:
def run(self) -> None: def run(self) -> None:
"""Fetch logs, check for matches, and send alerts.""" """Fetch logs, check for matches, and send alerts."""
while True: while True:
for rule in self.alert_rules: for name, rule in self.alert_rules.items():
if time.time() >= rule.next_run: if time.time() >= rule.next_run:
logger.debug(f"Processing rule: {name}")
rule.run() rule.run()
time.sleep(5) time.sleep(5)