diff --git a/config/ns-clm.conf b/config/ns-clm.conf new file mode 100644 index 000000000..73be8abb8 --- /dev/null +++ b/config/ns-clm.conf @@ -0,0 +1 @@ +CONFIG_PACKAGE_ns-clm=y diff --git a/packages/ns-clm/Makefile b/packages/ns-clm/Makefile new file mode 100644 index 000000000..1a75b5367 --- /dev/null +++ b/packages/ns-clm/Makefile @@ -0,0 +1,59 @@ +# +# Copyright (C) 2026 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-only +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=ns-clm +PKG_VERSION:=0.0.1 +PKG_RELEASE:=1 + +PKG_BUILD_DIR:=$(BUILD_DIR)/ns-clm-$(PKG_VERSION) + +PKG_MAINTAINER:=Nethesis +PKG_LICENSE:=GPL-3.0-only + +include $(INCLUDE_DIR)/package.mk + +define Package/ns-clm + SECTION:=base + CATEGORY:=NethSecurity + TITLE:=Cloud Log Manager forwarder + URL:=https://github.com/NethServer/nethsecurity/ + DEPENDS:=+python3-urllib + PKGARCH:=all +endef + +define Package/ns-clm/description + Forward syslog messages to the Nethesis Cloud Log Manager service +endef + +define Package/ns-clm/conffiles +/etc/config/ns-clm +endef + +# this is required, otherwise compile will fail +define Build/Compile +endef + +define Package/ns-clm/prerm +#!/bin/sh +if [ -z "$${IPKG_INSTROOT}" ]; then + /etc/init.d/ns-clm stop 2>/dev/null + /etc/init.d/ns-clm disable 2>/dev/null +fi +exit 0 +endef + +define Package/ns-clm/install + $(INSTALL_DIR) $(1)/usr/sbin + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_DIR) $(1)/etc/uci-defaults + $(INSTALL_BIN) ./files/ns-clm-forwarder $(1)/usr/sbin/ + $(INSTALL_BIN) ./files/ns-clm.init $(1)/etc/init.d/ns-clm + $(INSTALL_CONF) ./files/config $(1)/etc/config/ns-clm +endef + +$(eval $(call BuildPackage,ns-clm)) diff --git a/packages/ns-clm/README.md b/packages/ns-clm/README.md new file mode 100644 index 000000000..56e821e74 --- /dev/null +++ b/packages/ns-clm/README.md @@ -0,0 +1,54 @@ +# ns-clm + +Cloud Log Manager (CLM) forwarder for NethSecurity. Reads syslog messages from `/var/log/messages` and forwards them to the Nethesis CLM service. + +## Requirements + +- A CLM UUID provided manually by the user + +## Configuration + +UCI configuration is stored in `/etc/config/ns-clm`: + +| Option | Default | Description | +|--------|---------|-------------| +| `enabled` | `0` | Enable/disable the forwarder | +| `uuid` | (empty) | Required CLM UUID used for registration and log forwarding | +| `address` | `https://nar.nethesis.it` | CLM server address | +| `tenant` | (empty) | CLM tenant identifier | +| `debug` | `0` | Enable debug output to stderr | + +The forwarder will not start until `uuid` is configured. + +Example setup: + +```bash +uci set ns-clm.config.uuid="L$(uuidgen)" +uci set ns-clm.config.tenant='12345' +uci set ns-clm.config.enabled='1' +uci commit ns-clm +reload_config +``` + +## Service management + +Only if the package is installed via opkg, the service must be enabled and started via the init script. If the packages is already part of the base image, the forwarder is automatically enabled and started on first boot, so no manual action is required. + +```bash +# Enable and start +/etc/init.d/ns-clm enable && /etc/init.d/ns-clm start + +# Stop and disable +/etc/init.d/ns-clm stop && /etc/init.d/ns-clm disable +``` + +## How it works + +1. On startup the daemon registers the appliance against the CLM `/adm/api/noauth_lmcheck/` endpoint using the configured UUID, tenant, hostname, and MAC address +2. It sends a startup event to the CLM syslog endpoint +3. It tails `/var/log/messages`, tracking its position via an offset file +4. New syslog lines are parsed and batched +5. Batches are sent as JSON to the CLM endpoint via HTTP POST +6. Log rotation is detected automatically (file shrinks → offset resets) +7. The daemon polls every 10 seconds for new lines +8. On shutdown (SIGTERM), the current offset is persisted for resume diff --git a/packages/ns-clm/files/config b/packages/ns-clm/files/config new file mode 100644 index 000000000..0bfd0812e --- /dev/null +++ b/packages/ns-clm/files/config @@ -0,0 +1,6 @@ +config main 'config' + option enabled '0' + option uuid '' + option address 'https://nar.nethesis.it' + option tenant '' + option debug '0' diff --git a/packages/ns-clm/files/ns-clm-forwarder b/packages/ns-clm/files/ns-clm-forwarder new file mode 100644 index 000000000..65ea955de --- /dev/null +++ b/packages/ns-clm/files/ns-clm-forwarder @@ -0,0 +1,308 @@ +#!/usr/bin/env python3 + +# +# Copyright (C) 2026 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-only +# + +import json +import os +import platform +import re +import signal +import sys +import time +import urllib.parse +import urllib.request +import urllib.error +from datetime import datetime + +#-------------------------------- CONFIG --------------------------------# + +MESSAGES_FILE = "/var/log/messages" +OFFSET_FILE = "/var/run/ns-clm/last_offset" +BATCH_SIZE_THRESHOLD = 16384 # 16 KB +POLL_INTERVAL = 10 # seconds +AGENT_VERSION = "1" + +try: + UUID = os.environ["CLM_UUID"] + HOSTNAME = os.environ.get("CLM_HOSTNAME", "nethsecurity") + ADDRESS = os.environ.get("CLM_ADDRESS", "https://nar.nethesis.it") + TENANT = os.environ.get("CLM_TENANT", "") + MACADDR = os.environ.get("CLM_MACADDR", "00:00:00:00:00:00") + DEBUG = os.environ.get("CLM_DEBUG", "0") == "1" +except Exception as exc: + print(f"Error reading configuration: {exc}", file=sys.stderr) + sys.exit(1) + +#------------------------------- GLOBALS --------------------------------# + +running = True + +#--------------------------------- CODE ---------------------------------# + +# Syslog severity name to numeric mapping +SEVERITY_NAMES = { + "emerg": 0, + "alert": 1, + "crit": 2, + "err": 3, + "warning": 4, + "notice": 5, + "info": 6, + "debug": 7, +} + +# Numeric severity to CLM LOGTYPE mapping (same as reference) +SEVERITY_LOGTYPE = { + 0: "Emergency", + 1: "Warning", + 2: "Error", + 3: "Error", + 4: "Warning", + 5: "Information", + 6: "Information", + 7: "Debug", +} + +# Regex for standard syslog format: "Mon DD HH:MM:SS hostname facility.severity program[pid]: message" +# Also handles format without facility.severity prefix +SYSLOG_RE = re.compile( + r"^(?P\w{3})\s+(?P\d{1,2})\s+(?P