diff --git a/keep/providers/snmp_provider/README.md b/keep/providers/snmp_provider/README.md new file mode 100644 index 0000000000..df392f5f00 --- /dev/null +++ b/keep/providers/snmp_provider/README.md @@ -0,0 +1,23 @@ +# SNMP Provider for Keep + +The SNMP provider allows Keep to receive SNMP traps and convert them into alerts. + +## Configuration + +The SNMP provider is a **consumer** provider, meaning it listens for incoming data. + +| Parameter | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `port` | `int` | Yes | `1162` | Port to listen for traps. | +| `community` | `str` | Yes | `public` | SNMP v1/v2c Community string. | + +## Usage + +When initialized, the SNMP provider starts a listener on the specified port. +Incoming SNMP traps are parsed, and the variable bindings are stored in the alert data. + +### Example SNMP Trap Mapping +A trap with OID `1.3.6.1.6.3.1.1.4.1.0` (standard for `snmpTrapOID`) will be converted to an alert with: +- **Name**: `SNMP Trap: 1.3.6.1.6.3.1.1.4.1.0` +- **Severity**: `CRITICAL` +- **Payload**: Full JSON-formatted trap data. diff --git a/keep/providers/snmp_provider/__init__.py b/keep/providers/snmp_provider/__init__.py new file mode 100644 index 0000000000..b523e3ec71 --- /dev/null +++ b/keep/providers/snmp_provider/__init__.py @@ -0,0 +1 @@ +from keep.providers.snmp_provider.snmp_provider import * diff --git a/keep/providers/snmp_provider/snmp_provider.py b/keep/providers/snmp_provider/snmp_provider.py new file mode 100644 index 0000000000..b471a42bbe --- /dev/null +++ b/keep/providers/snmp_provider/snmp_provider.py @@ -0,0 +1,142 @@ +import dataclasses +import logging + +import pydantic +from pysnmp.hlapi import * +from pysnmp.carrier.asyncore.dispatch import AsyncoreDispatcher +from pysnmp.carrier.asyncore.dgram import udp +from pyasn1.codec.ber import decoder +from pysnmp.proto import api + +from keep.contextmanager.contextmanager import ContextManager +from keep.providers.base.base_provider import BaseProvider +from keep.providers.models.provider_config import ProviderConfig, ProviderScope +from keep.api.models.alert import AlertDto, AlertSeverity, AlertStatus + + +@pydantic.dataclasses.dataclass +class SnmpProviderAuthConfig: + """ + SNMP authentication configuration. + """ + + port: int = dataclasses.field( + default=1162, + metadata={ + "required": True, + "description": "SNMP Trap listening port", + "hint": "Default is 1162 (standard 162 usually requires root)", + }, + ) + community: str = dataclasses.field( + default="public", + metadata={ + "required": True, + "description": "SNMP Community string (v1/v2c)", + "hint": "e.g. public", + "sensitive": True, + }, + ) + + +class SnmpProvider(BaseProvider): + """ + SNMP provider class for receiving traps. + """ + + PROVIDER_DISPLAY_NAME = "SNMP" + PROVIDER_CATEGORY = ["Monitoring"] + PROVIDER_TAGS = ["alert"] + + def __init__( + self, context_manager: ContextManager, provider_id: str, config: ProviderConfig + ): + super().__init__(context_manager, provider_id, config) + self.consume = False + + def validate_config(self): + """ + Validates required configuration for SNMP provider. + """ + self.authentication_config = SnmpProviderAuthConfig( + **self.config.authentication + ) + + def dispose(self): + """ + Dispose the provider. + """ + self.consume = False + + def _cbFun(self, snmpEngine, stateReference, contextEngineId, contextName, varBinds, cbCtx): + """ + Callback function for receiving traps. + """ + self.logger.info("SNMP Trap received") + trap_data = {} + for name, val in varBinds: + trap_data[name.prettyPrint()] = val.prettyPrint() + + try: + self._push_alert(trap_data) + except Exception: + self.logger.exception("Error pushing SNMP trap as alert") + + def start_consume(self): + """ + Start listening for SNMP traps. + """ + self.consume = True + port = self.authentication_config.port + community = self.authentication_config.community + + snmpEngine = SnmpEngine() + + # Transport setup + config.addTransport( + snmpEngine, + udp.domainName, + udp.UdpTransport().openServerMode(('0.0.0.0', port)) + ) + + # SNMPv1/2c setup + config.addV1System(snmpEngine, 'my-area', community) + + # Callback registration + ntfrcv.NotificationReceiver(snmpEngine, self._cbFun) + + self.logger.info(f"SNMP Trap listener started on port {port} with community '{community}'") + + snmpEngine.transportDispatcher.jobStarted(1) + + try: + while self.consume: + snmpEngine.transportDispatcher.runDispatcher() + except Exception: + self.logger.exception("SNMP Dispatcher error") + finally: + snmpEngine.transportDispatcher.closeDispatcher() + self.logger.info("SNMP Trap listener stopped") + + def stop_consume(self): + self.consume = False + + @staticmethod + def _format_alert(event: dict, provider_instance: "SnmpProvider" = None) -> AlertDto: + # Map SNMP varbinds to Keep Alert fields + # Common OIDs: + # sysUpTimeInstance = '1.3.6.1.2.1.1.3.0' + # snmpTrapOID = '1.3.6.1.6.3.1.1.4.1.0' + + trap_oid = event.get('1.3.6.1.6.3.1.1.4.1.0', 'Unknown OID') + + return AlertDto( + id=event.get('id', trap_oid), + name=f"SNMP Trap: {trap_oid}", + status=AlertStatus.FIRING, + severity=AlertSeverity.CRITICAL, # SNMP traps are usually critical events + message=str(event), + description="Received SNMP Trap", + source=["snmp"], + payload=event + ) diff --git a/pyproject.toml b/pyproject.toml index e4e70695be..eda8b680a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,7 @@ google-cloud-container = "^2.32.0" pympler = "^1.0.1" prettytable = "^3.9.0" kafka-python = "^2.0.2" +pysnmp = "^4.4.12" opentelemetry-exporter-otlp-proto-http = "^1.20.0" twilio = "^8.10.0" azure-identity = "^1.16.1"