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