commit ceda8957b7f113026e6b62b1d219f97acc346e94 Author: Blake Ridgway Date: Sat Jul 12 10:06:52 2025 -0500 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f5d9592 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +env/ +venv/ +.venv/ +*/.pytest_cache/ +.mypy_cache/ +.dmypy.json +celerybeat-schedule +.vscode/ + +# Flask-specific +instance/ +.flaskenv + +# Database +# For SQLite, the database file +siem.db +*.sqlite3 +*.db +*.db-* # for potential Flask-SQLAlchemy-generated temp files + +# Logs +*.log +logs/ +run.log + +# IDEs +.idea/ # PyCharm +*.iml # PyCharm +*.ipr # PyCharm +*.iws # PyCharm +.vscode/ # VS Code (if not managed by specific user settings) + +# OS generated files +.DS_Store +.Trashes +Thumbs.db \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..52c9713 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..f5c9a13 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/sentinel.iml b/.idea/sentinel.iml new file mode 100644 index 0000000..c8e1e79 --- /dev/null +++ b/.idea/sentinel.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..66cc860 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# Sentinel + +**Sentinel** is a lightweight Security Information and Event Management (SIEM) application built with Flask, designed to collect and display network events from various sources using SNMP Traps and Syslog. It aims to provide basic visibility into network activity, security incidents, and operational events. + +## Features + +* **SNMP Trap Listener**: Receives and processes SNMP traps (v1/v2c) from network devices. +* **Syslog Listener**: Collects syslog messages over UDP from firewalls, routers, and other logging sources. +* **Unified Event Storage**: Stores both SNMP and Syslog events in a SQLite database for easy access and querying. +* **Event Normalization & Enrichment**: + * Maps common SNMP OIDs to human-readable names. + * Assigns severity (Critical, High, Medium, Low, Informational) to events based on predefined rules for both SNMP and Syslog messages. +* **Web-based Dashboard**: A simple Flask web interface to view recent collected events, including their type, source, and assigned severity. +* **Background Listeners**: SNMP and Syslog listeners run in separate threads, allowing continuous collection while the Flask web server operates. \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..c9b95c5 --- /dev/null +++ b/app.py @@ -0,0 +1,26 @@ +from flask import Flask, render_template +from models import db, Event +from snmp_listener import start_snmp_listener_in_thread +from syslog_listener import start_syslog_listener_in_thread + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///siem.db' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +db.init_app(app) + +initialized = False + +@app.route('/') +def index(): + global initialized + if not initialized: + with app.app_context(): + db.create_all() + start_snmp_listener_in_thread(port=1162) + start_syslog_listener_in_thread(port=1514) + initialized = True + events = Event.query.order_by(Event.timestamp.desc()).limit(100).all() + return render_template('index.html', events=events) + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/event_processor.py b/event_processor.py new file mode 100644 index 0000000..d0f352c --- /dev/null +++ b/event_processor.py @@ -0,0 +1,100 @@ +from models import db, Event +import re + +SNMP_OID_MAPPING = { + ".1.3.6.1.6.3.1.1.5.1": "SNMP Cold Start", + ".1.3.6.1.6.3.1.1.5.2": "SNMP Warm Start", + ".1.3.6.1.6.3.1.1.5.3": "SNMP Link Down", + ".1.3.6.1.6.3.1.1.5.4": "SNMP Link Up", + ".1.3.6.1.6.3.1.1.5.5": "SNMP Authentication Failure", + ".1.3.6.1.6.3.1.1.5.6": "SNMP EGP Neighbor Loss", + # TODO Add FortiGate/pfSense +} + +SEVERITY_RULES = { + "snmp_trap": { + "critical": [ + ".1.3.6.1.6.3.1.1.5.3", # Link Down + ".1.3.6.1.6.3.1.1.5.5", # Authentication Failure + "fail", "error", "down", "critical", "unauthorized", "deny" + ], + "high": [ + ".1.3.6.1.6.3.1.1.5.1", # Cold Start + "warn", "warning", "threshold", "exceeded" + ], + "informational": [ + ".1.3.6.1.6.3.1.1.5.4", # Link Up + "up", "start", "config", "change", "success", "info" + ] + }, + "syslog": { + "critical": [ + r"deny\s", r"rejected\s", r"failed\slogin", r"authentication\sfailed", + r"intrusion\sdetected", r"critical\serror" + ], + "high": [ + r"failed\s", r"blocked\s", r"unauthorized", r"warning", r"error" + ], + "informational": [ + r"login\ssuccess", r"accepted", r"connected", r"info", r"start", r"stop", r"status" + ] + } +} + +def get_event_severity(event_type, identifier, value=""): + """ + Determines the severity of an event based on its type, OID/keyword, and value. + Identifier can be OID for SNMP, or a keyword for Syslog. + """ + rules = SEVERITY_RULES.get(event_type, {}) + + # Check Critical rules + for pattern in rules.get("critical", []): + if event_type == "snmp_trap" and identifier == pattern: + return "Critical" + if event_type == "syslog" and re.search(pattern, value, re.IGNORECASE): + return "Critical" + + # Check High rules + for pattern in rules.get("high", []): + if event_type == "snmp_trap" and identifier == pattern: + return "High" + if event_type == "syslog" and re.search(pattern, value, re.IGNORECASE): + return "High" + + # Check Informational rules + for pattern in rules.get("informational", []): + if event_type == "snmp_trap" and identifier == pattern: + return "Informational" + if event_type == "syslog" and re.search(pattern, value, re.IGNORECASE): + return "Informational" + + return "Unknown" # Default if no rule matches + +def process_event_for_enrichment(event_id): + """ + Fetches an event by ID, enriches it, and updates it in the database. + This function should be called within a Flask app context. + """ + with db.session.begin_nested(): + event = db.session.get(Event, event_id) + if not event: + print(f"Event with ID {event_id} not found for enrichment.") + return + + original_oid_or_value = "" + if event.event_type == "snmp_trap": + if event.oid: + event.value_original_oid = event.oid + event.oid = SNMP_OID_MAPPING.get(event.oid, event.oid) + original_oid_or_value = event.value_original_oid + + event.severity = get_event_severity(event.event_type, original_oid_or_value, event.value) + + elif event.event_type == "syslog": + original_oid_or_value = event.value # Use full message for rules + event.severity = get_event_severity(event.event_type, None, event.value) + + db.session.add(event) + db.session.commit() + print(f"Event {event.id} enriched: Type={event.event_type}, Severity={event.severity}") \ No newline at end of file diff --git a/models.py b/models.py new file mode 100644 index 0000000..0aa3cc3 --- /dev/null +++ b/models.py @@ -0,0 +1,18 @@ +from flask_sqlalchemy import SQLAlchemy +from datetime import datetime + +db = SQLAlchemy() + +class Event(db.Model): + id = db.Column(db.Integer, primary_key=True) + timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + source_ip = db.Column(db.String(45), nullable=False) + event_type = db.Column(db.String(50), nullable=False) + protocol = db.Column(db.String(10), nullable=False) + oid = db.Column(db.String(255), nullable=True) + value_original_oid = db.Column(db.String(255), nullable=True) + value = db.Column(db.String(1024), nullable=False) + severity = db.Column(db.String(20), default="Unknown", nullable=False) + + def __repr__(self): + return f'' \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d7cb83e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Flask +Flask-SQLAlchemy +pysnmp \ No newline at end of file diff --git a/snmp_listener.py b/snmp_listener.py new file mode 100644 index 0000000..c3580d2 --- /dev/null +++ b/snmp_listener.py @@ -0,0 +1,61 @@ +import asyncio +import threading +from datetime import datetime +from pysnmp.entity import engine, config +from pysnmp.carrier.asyncio.dgram import udp +from pysnmp.entity.rfc3413 import ntfrcv +from flask import Flask +from models import db, Event +from event_processor import process_event_for_enrichment + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///siem.db' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +db.init_app(app) + +def trap_callback(snmpEngine, stateReference, contextEngineId, contextName, + varBinds, cbCtx): + source_ip = "unknown" + timestamp = datetime.utcnow() + + with app.app_context(): + event_ids_to_process = [] + for name, val in varBinds: + event = Event( + timestamp=timestamp, + source_ip=source_ip, + event_type="snmp_trap", + protocol="udp", + oid=name.prettyPrint(), + value=val.prettyPrint(), + severity="Unknown" + ) + db.session.add(event) + db.session.flush() + event_ids_to_process.append(event.id) + db.session.commit() + print(f'Received SNMP Trap (source IP: {source_ip})') + + for event_id in event_ids_to_process: + process_event_for_enrichment(event_id) + +async def run_snmp_listener(port=1162): + snmpEngine = engine.SnmpEngine() + + config.addTransport( + snmpEngine, + udp.domainName, + udp.UdpAsyncioTransport().openServerMode(('0.0.0.0', port)) + ) + + config.addV1System(snmpEngine, 'my-area', 'public') + ntfrcv.NotificationReceiver(snmpEngine, trap_callback) + + print(f'SNMP Trap listener started on 0.0.0.0:{port}') + await asyncio.Event().wait() + +def start_snmp_listener_in_thread(port=1162): + def runner(): + asyncio.run(run_snmp_listener(port)) + thread = threading.Thread(target=runner, daemon=True) + thread.start() \ No newline at end of file diff --git a/syslog_listener.py b/syslog_listener.py new file mode 100644 index 0000000..c7d76db --- /dev/null +++ b/syslog_listener.py @@ -0,0 +1,53 @@ +import socketserver +import threading +from datetime import datetime +from flask import Flask +from models import db, Event +from event_processor import process_event_for_enrichment + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///siem.db' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +db.init_app(app) + + +class SyslogUDPHandler(socketserver.BaseRequestHandler): + def handle(self): + data = bytes.decode(self.request[0].strip()) + socket = self.request[1] + source_ip = self.client_address[0] + timestamp = datetime.utcnow() + + message = data + + with app.app_context(): + event = Event( + timestamp=timestamp, + source_ip=source_ip, + event_type="syslog", + protocol="udp", + oid=None, + value=message[:1024], + severity="Unknown" + ) + db.session.add(event) + db.session.flush() + event_id = event.id + db.session.commit() + print(f"Received Syslog from {source_ip}: {message[:80]}...") + + process_event_for_enrichment(event_id) + +def run_syslog_listener(port=1514): + HOST, PORT = "0.0.0.0", port + try: + socketserver.UDPServer.allow_reuse_address = True + server = socketserver.UDPServer((HOST, PORT), SyslogUDPHandler) + print(f"Syslog listener started on 0.0.0.0:{PORT}") + server.serve_forever(poll_interval=0.5) + except Exception as e: + print(f"Error starting Syslog listener: {e}") + +def start_syslog_listener_in_thread(port=1514): + thread = threading.Thread(target=run_syslog_listener, args=(port,), daemon=True) + thread.start() \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..344b4a7 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,53 @@ + + + + + SentinelSNMP - All Events + + + +

SentinelSNMP - Recent Events

+ + + + + + + + + + + + + {% for event in events %} + + + + + + + + + {% else %} + + {% endfor %} + +
Timestamp (UTC)TypeSeveritySource IPOID (if SNMP)Value / Message
{{ event.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}{{ event.event_type.replace('_', ' ').title() }}{{ event.severity }}{{ event.source_ip }}{{ event.oid if event.oid else 'N/A' }}{{ event.value }}
No events found.
+ + \ No newline at end of file