Initial commit
This commit is contained in:
		
						commit
						ceda8957b7
					
				
					 14 changed files with 408 additions and 0 deletions
				
			
		
							
								
								
									
										42
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -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 | ||||||
							
								
								
									
										8
									
								
								.idea/.gitignore
									
										
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.idea/.gitignore
									
										
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -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 | ||||||
							
								
								
									
										6
									
								
								.idea/inspectionProfiles/profiles_settings.xml
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.idea/inspectionProfiles/profiles_settings.xml
									
										
									
										generated
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | ||||||
|  | <component name="InspectionProjectProfileManager"> | ||||||
|  |   <settings> | ||||||
|  |     <option name="USE_PROJECT_PROFILE" value="false" /> | ||||||
|  |     <version value="1.0" /> | ||||||
|  |   </settings> | ||||||
|  | </component> | ||||||
							
								
								
									
										6
									
								
								.idea/misc.xml
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.idea/misc.xml
									
										
									
										generated
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | ||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="Black"> | ||||||
|  |     <option name="sdkName" value="Python 3.11 (sentinel)" /> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
							
								
								
									
										8
									
								
								.idea/modules.xml
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.idea/modules.xml
									
										
									
										generated
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | ||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="ProjectModuleManager"> | ||||||
|  |     <modules> | ||||||
|  |       <module fileurl="file://$PROJECT_DIR$/.idea/sentinel.iml" filepath="$PROJECT_DIR$/.idea/sentinel.iml" /> | ||||||
|  |     </modules> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
							
								
								
									
										10
									
								
								.idea/sentinel.iml
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								.idea/sentinel.iml
									
										
									
										generated
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <module type="PYTHON_MODULE" version="4"> | ||||||
|  |   <component name="NewModuleRootManager"> | ||||||
|  |     <content url="file://$MODULE_DIR$"> | ||||||
|  |       <excludeFolder url="file://$MODULE_DIR$/.venv" /> | ||||||
|  |     </content> | ||||||
|  |     <orderEntry type="jdk" jdkName="Python 3.11 (sentinel)" jdkType="Python SDK" /> | ||||||
|  |     <orderEntry type="sourceFolder" forTests="false" /> | ||||||
|  |   </component> | ||||||
|  | </module> | ||||||
							
								
								
									
										14
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							|  | @ -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. | ||||||
							
								
								
									
										26
									
								
								app.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								app.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -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) | ||||||
							
								
								
									
										100
									
								
								event_processor.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								event_processor.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -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}") | ||||||
							
								
								
									
										18
									
								
								models.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								models.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -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'<Event {self.id} Type: {self.event_type} Source: {self.source_ip} Severity: {self.severity}>' | ||||||
							
								
								
									
										3
									
								
								requirements.txt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								requirements.txt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | ||||||
|  | Flask | ||||||
|  | Flask-SQLAlchemy | ||||||
|  | pysnmp | ||||||
							
								
								
									
										61
									
								
								snmp_listener.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								snmp_listener.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -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() | ||||||
							
								
								
									
										53
									
								
								syslog_listener.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								syslog_listener.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -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() | ||||||
							
								
								
									
										53
									
								
								templates/index.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								templates/index.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,53 @@ | ||||||
|  | <!DOCTYPE html> | ||||||
|  | <html lang="en"> | ||||||
|  | <head> | ||||||
|  |   <meta charset="UTF-8" /> | ||||||
|  |   <title>SentinelSNMP - All Events</title> | ||||||
|  |   <style> | ||||||
|  |     body { font-family: Arial, sans-serif; margin: 20px; } | ||||||
|  |     h1 { color: #333; } | ||||||
|  |     table { border-collapse: collapse; width: 100%; margin-top: 20px; } | ||||||
|  |     th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } | ||||||
|  |     th { background-color: #f2f2f2; color: #555; } | ||||||
|  |     tr:nth-child(even) { background-color: #f9f9f9; } | ||||||
|  |     tr:hover { background-color: #f1f1f1; } | ||||||
|  | 
 | ||||||
|  |     /* Severity Colors */ | ||||||
|  |     .severity-critical { background-color: #ffcccc; } /* Light red */ | ||||||
|  |     .severity-high { background-color: #ffe0b3; }    /* Light orange */ | ||||||
|  |     .severity-medium { background-color: #ffffcc; }   /* Light yellow */ | ||||||
|  |     .severity-low { background-color: #e6ffe6; }      /* Light green */ | ||||||
|  |     .severity-informational { background-color: #e0f2f7; } /* Light blue */ | ||||||
|  |     .severity-unknown { background-color: #f0f0f0; }   /* Light grey */ | ||||||
|  |   </style> | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  |   <h1>SentinelSNMP - Recent Events</h1> | ||||||
|  |   <table> | ||||||
|  |     <thead> | ||||||
|  |       <tr> | ||||||
|  |         <th>Timestamp (UTC)</th> | ||||||
|  |         <th>Type</th> | ||||||
|  |         <th>Severity</th> | ||||||
|  |         <th>Source IP</th> | ||||||
|  |         <th>OID (if SNMP)</th> | ||||||
|  |         <th>Value / Message</th> | ||||||
|  |       </tr> | ||||||
|  |     </thead> | ||||||
|  |     <tbody> | ||||||
|  |       {% for event in events %} | ||||||
|  |       <tr class="severity-{{ event.severity.lower() }}"> | ||||||
|  |         <td>{{ event.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</td> | ||||||
|  |         <td>{{ event.event_type.replace('_', ' ').title() }}</td> | ||||||
|  |         <td>{{ event.severity }}</td> | ||||||
|  |         <td>{{ event.source_ip }}</td> | ||||||
|  |         <td>{{ event.oid if event.oid else 'N/A' }}</td> | ||||||
|  |         <td>{{ event.value }}</td> | ||||||
|  |       </tr> | ||||||
|  |       {% else %} | ||||||
|  |       <tr><td colspan="6">No events found.</td></tr> | ||||||
|  |       {% endfor %} | ||||||
|  |     </tbody> | ||||||
|  |   </table> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Blake Ridgway
						Blake Ridgway