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": [
{
"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<method>\\w+) for (?P<username>\\w+) from (?P<ip>[^\\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})"
}
}
]
}
}

View File

@@ -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
}
}

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')}")
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)

View File

@@ -1,2 +1,2 @@
requests
jsonschema>=4.0.0
jsonschema>=4.0.0