mirror of
https://github.com/infinition/Bjorn.git
synced 2026-03-15 17:01:58 +00:00
- Implemented LokiUtils class with GET and POST endpoints for managing scripts, jobs, and payloads. - Added SentinelUtils class with GET and POST endpoints for managing events, rules, devices, and notifications. - Both classes include error handling and JSON response formatting.
315 lines
12 KiB
Python
315 lines
12 KiB
Python
"""
|
|
Sentinel DB operations — events, rules, known devices baseline.
|
|
"""
|
|
import json
|
|
import logging
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from logger import Logger
|
|
|
|
logger = Logger(name="db_utils.sentinel", level=logging.DEBUG)
|
|
|
|
|
|
class SentinelOps:
|
|
def __init__(self, base):
|
|
self.base = base
|
|
|
|
def create_tables(self):
|
|
"""Create all Sentinel tables."""
|
|
|
|
# Known device baselines — MAC → expected behavior
|
|
self.base.execute("""
|
|
CREATE TABLE IF NOT EXISTS sentinel_devices (
|
|
mac_address TEXT PRIMARY KEY,
|
|
alias TEXT,
|
|
trusted INTEGER DEFAULT 0,
|
|
watch INTEGER DEFAULT 1,
|
|
first_seen TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
last_seen TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
expected_ips TEXT DEFAULT '',
|
|
expected_ports TEXT DEFAULT '',
|
|
notes TEXT DEFAULT ''
|
|
)
|
|
""")
|
|
|
|
# Events / alerts log
|
|
self.base.execute("""
|
|
CREATE TABLE IF NOT EXISTS sentinel_events (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
event_type TEXT NOT NULL,
|
|
severity TEXT DEFAULT 'info',
|
|
title TEXT NOT NULL,
|
|
details TEXT DEFAULT '',
|
|
mac_address TEXT,
|
|
ip_address TEXT,
|
|
acknowledged INTEGER DEFAULT 0,
|
|
notified INTEGER DEFAULT 0,
|
|
meta_json TEXT DEFAULT '{}'
|
|
)
|
|
""")
|
|
self.base.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_sentinel_events_ts "
|
|
"ON sentinel_events(timestamp DESC)"
|
|
)
|
|
self.base.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_sentinel_events_type "
|
|
"ON sentinel_events(event_type)"
|
|
)
|
|
self.base.execute(
|
|
"CREATE INDEX IF NOT EXISTS idx_sentinel_events_ack "
|
|
"ON sentinel_events(acknowledged)"
|
|
)
|
|
|
|
# Configurable rules (AND/OR composable)
|
|
self.base.execute("""
|
|
CREATE TABLE IF NOT EXISTS sentinel_rules (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
enabled INTEGER DEFAULT 1,
|
|
trigger_type TEXT NOT NULL,
|
|
conditions TEXT DEFAULT '{}',
|
|
logic TEXT DEFAULT 'AND',
|
|
actions TEXT DEFAULT '["notify_web"]',
|
|
cooldown_s INTEGER DEFAULT 60,
|
|
last_fired TEXT,
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
""")
|
|
|
|
# ARP cache snapshots for spoof detection
|
|
self.base.execute("""
|
|
CREATE TABLE IF NOT EXISTS sentinel_arp_cache (
|
|
mac_address TEXT NOT NULL,
|
|
ip_address TEXT NOT NULL,
|
|
first_seen TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
last_seen TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (mac_address, ip_address)
|
|
)
|
|
""")
|
|
|
|
# Insert default rules if empty
|
|
existing = self.base.query("SELECT COUNT(*) AS c FROM sentinel_rules")
|
|
if existing and existing[0].get('c', 0) == 0:
|
|
self._insert_default_rules()
|
|
|
|
def _insert_default_rules(self):
|
|
"""Seed default Sentinel rules."""
|
|
defaults = [
|
|
{
|
|
"name": "New Device Detected",
|
|
"trigger_type": "new_device",
|
|
"conditions": "{}",
|
|
"logic": "AND",
|
|
"actions": '["notify_web"]',
|
|
"cooldown_s": 0,
|
|
},
|
|
{
|
|
"name": "Device Joined Network",
|
|
"trigger_type": "device_join",
|
|
"conditions": "{}",
|
|
"logic": "AND",
|
|
"actions": '["notify_web"]',
|
|
"cooldown_s": 30,
|
|
},
|
|
{
|
|
"name": "Device Left Network",
|
|
"trigger_type": "device_leave",
|
|
"conditions": "{}",
|
|
"logic": "AND",
|
|
"actions": '["notify_web"]',
|
|
"cooldown_s": 30,
|
|
},
|
|
{
|
|
"name": "ARP Spoofing Detected",
|
|
"trigger_type": "arp_spoof",
|
|
"conditions": "{}",
|
|
"logic": "AND",
|
|
"actions": '["notify_web", "notify_discord"]',
|
|
"cooldown_s": 10,
|
|
},
|
|
{
|
|
"name": "Port Change on Host",
|
|
"trigger_type": "port_change",
|
|
"conditions": "{}",
|
|
"logic": "AND",
|
|
"actions": '["notify_web"]',
|
|
"cooldown_s": 120,
|
|
},
|
|
{
|
|
"name": "Rogue DHCP Server",
|
|
"trigger_type": "rogue_dhcp",
|
|
"conditions": "{}",
|
|
"logic": "AND",
|
|
"actions": '["notify_web", "notify_discord"]',
|
|
"cooldown_s": 60,
|
|
},
|
|
]
|
|
for rule in defaults:
|
|
self.base.execute(
|
|
"""INSERT INTO sentinel_rules
|
|
(name, trigger_type, conditions, logic, actions, cooldown_s)
|
|
VALUES (?, ?, ?, ?, ?, ?)""",
|
|
(rule["name"], rule["trigger_type"], rule["conditions"],
|
|
rule["logic"], rule["actions"], rule["cooldown_s"])
|
|
)
|
|
|
|
# ── Events ──────────────────────────────────────────────────────────
|
|
|
|
def insert_event(self, event_type: str, severity: str, title: str,
|
|
details: str = "", mac: str = "", ip: str = "",
|
|
meta: Optional[Dict] = None) -> int:
|
|
return self.base.execute(
|
|
"""INSERT INTO sentinel_events
|
|
(event_type, severity, title, details, mac_address, ip_address, meta_json)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
(event_type, severity, title, details, mac, ip,
|
|
json.dumps(meta or {}))
|
|
)
|
|
|
|
def get_events(self, limit: int = 100, offset: int = 0,
|
|
event_type: str = "", unread_only: bool = False) -> List[Dict]:
|
|
sql = "SELECT * FROM sentinel_events WHERE 1=1"
|
|
params: list = []
|
|
if event_type:
|
|
sql += " AND event_type = ?"
|
|
params.append(event_type)
|
|
if unread_only:
|
|
sql += " AND acknowledged = 0"
|
|
sql += " ORDER BY timestamp DESC LIMIT ? OFFSET ?"
|
|
params.extend([limit, offset])
|
|
return self.base.query(sql, params)
|
|
|
|
def count_unread(self) -> int:
|
|
row = self.base.query_one(
|
|
"SELECT COUNT(*) AS c FROM sentinel_events WHERE acknowledged = 0"
|
|
)
|
|
return int(row.get("c", 0)) if row else 0
|
|
|
|
def acknowledge_event(self, event_id: int):
|
|
self.base.execute(
|
|
"UPDATE sentinel_events SET acknowledged = 1 WHERE id = ?",
|
|
(event_id,)
|
|
)
|
|
|
|
def acknowledge_all(self):
|
|
self.base.execute("UPDATE sentinel_events SET acknowledged = 1")
|
|
|
|
def clear_events(self):
|
|
self.base.execute("DELETE FROM sentinel_events")
|
|
|
|
# ── Rules ───────────────────────────────────────────────────────────
|
|
|
|
def get_rules(self) -> List[Dict]:
|
|
return self.base.query("SELECT * FROM sentinel_rules ORDER BY id")
|
|
|
|
def get_enabled_rules(self, trigger_type: str = "") -> List[Dict]:
|
|
if trigger_type:
|
|
return self.base.query(
|
|
"SELECT * FROM sentinel_rules WHERE enabled = 1 AND trigger_type = ?",
|
|
(trigger_type,)
|
|
)
|
|
return self.base.query(
|
|
"SELECT * FROM sentinel_rules WHERE enabled = 1"
|
|
)
|
|
|
|
def upsert_rule(self, data: Dict) -> Dict:
|
|
rule_id = data.get("id")
|
|
if rule_id:
|
|
self.base.execute(
|
|
"""UPDATE sentinel_rules SET
|
|
name=?, enabled=?, trigger_type=?, conditions=?,
|
|
logic=?, actions=?, cooldown_s=?
|
|
WHERE id=?""",
|
|
(data["name"], int(data.get("enabled", 1)),
|
|
data["trigger_type"], json.dumps(data.get("conditions", {})),
|
|
data.get("logic", "AND"),
|
|
json.dumps(data.get("actions", ["notify_web"])),
|
|
int(data.get("cooldown_s", 60)), rule_id)
|
|
)
|
|
else:
|
|
self.base.execute(
|
|
"""INSERT INTO sentinel_rules
|
|
(name, enabled, trigger_type, conditions, logic, actions, cooldown_s)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
(data["name"], int(data.get("enabled", 1)),
|
|
data["trigger_type"], json.dumps(data.get("conditions", {})),
|
|
data.get("logic", "AND"),
|
|
json.dumps(data.get("actions", ["notify_web"])),
|
|
int(data.get("cooldown_s", 60)))
|
|
)
|
|
return {"status": "ok"}
|
|
|
|
def delete_rule(self, rule_id: int):
|
|
self.base.execute("DELETE FROM sentinel_rules WHERE id = ?", (rule_id,))
|
|
|
|
def update_rule_fired(self, rule_id: int):
|
|
self.base.execute(
|
|
"UPDATE sentinel_rules SET last_fired = CURRENT_TIMESTAMP WHERE id = ?",
|
|
(rule_id,)
|
|
)
|
|
|
|
# ── Devices baseline ────────────────────────────────────────────────
|
|
|
|
def get_known_device(self, mac: str) -> Optional[Dict]:
|
|
return self.base.query_one(
|
|
"SELECT * FROM sentinel_devices WHERE mac_address = ?", (mac,)
|
|
)
|
|
|
|
def upsert_device(self, mac: str, **kwargs):
|
|
existing = self.get_known_device(mac)
|
|
if existing:
|
|
sets = []
|
|
params = []
|
|
for k, v in kwargs.items():
|
|
if k in ("alias", "trusted", "watch", "expected_ips",
|
|
"expected_ports", "notes"):
|
|
sets.append(f"{k} = ?")
|
|
params.append(v)
|
|
sets.append("last_seen = CURRENT_TIMESTAMP")
|
|
if sets:
|
|
params.append(mac)
|
|
self.base.execute(
|
|
f"UPDATE sentinel_devices SET {', '.join(sets)} WHERE mac_address = ?",
|
|
params
|
|
)
|
|
else:
|
|
self.base.execute(
|
|
"""INSERT INTO sentinel_devices
|
|
(mac_address, alias, trusted, watch, expected_ips, expected_ports, notes)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
(mac, kwargs.get("alias", ""),
|
|
int(kwargs.get("trusted", 0)),
|
|
int(kwargs.get("watch", 1)),
|
|
kwargs.get("expected_ips", ""),
|
|
kwargs.get("expected_ports", ""),
|
|
kwargs.get("notes", ""))
|
|
)
|
|
|
|
def get_all_known_devices(self) -> List[Dict]:
|
|
return self.base.query("SELECT * FROM sentinel_devices ORDER BY last_seen DESC")
|
|
|
|
# ── ARP cache ───────────────────────────────────────────────────────
|
|
|
|
def update_arp_entry(self, mac: str, ip: str):
|
|
self.base.execute(
|
|
"""INSERT INTO sentinel_arp_cache (mac_address, ip_address)
|
|
VALUES (?, ?)
|
|
ON CONFLICT(mac_address, ip_address)
|
|
DO UPDATE SET last_seen = CURRENT_TIMESTAMP""",
|
|
(mac, ip)
|
|
)
|
|
|
|
def get_arp_for_ip(self, ip: str) -> List[Dict]:
|
|
return self.base.query(
|
|
"SELECT * FROM sentinel_arp_cache WHERE ip_address = ?", (ip,)
|
|
)
|
|
|
|
def get_arp_for_mac(self, mac: str) -> List[Dict]:
|
|
return self.base.query(
|
|
"SELECT * FROM sentinel_arp_cache WHERE mac_address = ?", (mac,)
|
|
)
|
|
|
|
def get_full_arp_cache(self) -> List[Dict]:
|
|
return self.base.query("SELECT * FROM sentinel_arp_cache ORDER BY last_seen DESC")
|