mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2025-12-07 09:36:05 -08:00
Initial commit on next_release branch
This commit is contained in:
90
server/workflows/actions.py
Executable file
90
server/workflows/actions.py
Executable file
@@ -0,0 +1,90 @@
|
||||
import sys
|
||||
|
||||
# Register NetAlertX directories
|
||||
INSTALL_PATH="/app"
|
||||
sys.path.extend([f"{INSTALL_PATH}/server"])
|
||||
|
||||
import conf
|
||||
from logger import mylog, Logger
|
||||
from helper import get_setting_value, timeNowTZ
|
||||
|
||||
# Make sure log level is initialized correctly
|
||||
Logger(get_setting_value('LOG_LEVEL'))
|
||||
|
||||
from workflows.triggers import Trigger
|
||||
|
||||
class Action:
|
||||
"""Base class for all actions."""
|
||||
|
||||
def __init__(self, trigger):
|
||||
self.trigger = trigger
|
||||
|
||||
def execute(self, obj):
|
||||
"""Executes the action on the given object."""
|
||||
raise NotImplementedError("Subclasses must implement execute()")
|
||||
|
||||
|
||||
class UpdateFieldAction(Action):
|
||||
"""Action to update a specific field of an object."""
|
||||
|
||||
def __init__(self, field, value, trigger):
|
||||
super().__init__(trigger) # Call the base class constructor
|
||||
self.field = field
|
||||
self.value = value
|
||||
|
||||
def execute(self):
|
||||
mylog('verbose', [f"Updating field '{self.field}' to '{self.value}' for event object {self.trigger.object_type}"])
|
||||
|
||||
obj = self.trigger.object
|
||||
|
||||
if isinstance(obj, dict) and "ObjectGUID" in obj:
|
||||
plugin_instance = PluginObjectInstance(self.trigger.db)
|
||||
plugin_instance.updateField(obj["ObjectGUID"], self.field, self.value)
|
||||
elif isinstance(obj, dict) and "devGUID" in obj:
|
||||
device_instance = DeviceInstance(self.trigger.db)
|
||||
device_instance.updateField(obj["devGUID"], self.field, self.value)
|
||||
return obj
|
||||
|
||||
|
||||
class RunPluginAction(Action):
|
||||
"""Action to run a specific plugin."""
|
||||
|
||||
def __init__(self, plugin_name, params, trigger): # Add trigger
|
||||
super().__init__(trigger) # Call parent constructor
|
||||
self.plugin_name = plugin_name
|
||||
self.params = params
|
||||
|
||||
def execute(self):
|
||||
|
||||
obj = self.trigger.object
|
||||
|
||||
mylog('verbose', [f"Executing plugin '{self.plugin_name}' with parameters {self.params} for object {obj}"])
|
||||
# PluginManager.run(self.plugin_name, self.parameters)
|
||||
return obj
|
||||
|
||||
|
||||
class SendNotificationAction(Action):
|
||||
"""Action to send a notification."""
|
||||
|
||||
def __init__(self, method, message, trigger):
|
||||
super().__init__(trigger) # Call parent constructor
|
||||
self.method = method # Fix attribute name
|
||||
self.message = message
|
||||
|
||||
def execute(self):
|
||||
obj = self.trigger.object
|
||||
mylog('verbose', [f"Sending notification via '{self.method}': {self.message} for object {obj}"])
|
||||
# NotificationManager.send(self.method, self.message)
|
||||
return obj
|
||||
|
||||
|
||||
class ActionGroup:
|
||||
"""Handles multiple actions applied to an object."""
|
||||
|
||||
def __init__(self, actions):
|
||||
self.actions = actions
|
||||
|
||||
def execute(self, obj):
|
||||
for action in self.actions:
|
||||
action.execute(obj)
|
||||
return obj
|
||||
412
server/workflows/app_events.py
Executable file
412
server/workflows/app_events.py
Executable file
@@ -0,0 +1,412 @@
|
||||
import datetime
|
||||
import json
|
||||
import uuid
|
||||
import sys
|
||||
import pytz
|
||||
|
||||
# Register NetAlertX directories
|
||||
INSTALL_PATH="/app"
|
||||
sys.path.extend([f"{INSTALL_PATH}/server"])
|
||||
|
||||
# Register NetAlertX modules
|
||||
import conf
|
||||
from helper import get_setting_value, timeNowTZ
|
||||
# Make sure the TIMEZONE for logging is correct
|
||||
# conf.tz = pytz.timezone(get_setting_value('TIMEZONE'))
|
||||
|
||||
from logger import mylog, Logger, print_log, logResult
|
||||
|
||||
# Make sure log level is initialized correctly
|
||||
Logger(get_setting_value('LOG_LEVEL'))
|
||||
|
||||
from const import applicationPath, logPath, apiPath, confFileName, sql_generateGuid
|
||||
from helper import timeNowTZ
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Execution object handling
|
||||
#-------------------------------------------------------------------------------
|
||||
class AppEvent_obj:
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
|
||||
# drop table
|
||||
self.db.sql.execute("""DROP TABLE IF EXISTS "AppEvents" """)
|
||||
|
||||
# Drop all triggers
|
||||
self.db.sql.execute('DROP TRIGGER IF EXISTS trg_create_device;')
|
||||
self.db.sql.execute('DROP TRIGGER IF EXISTS trg_read_device;')
|
||||
self.db.sql.execute('DROP TRIGGER IF EXISTS trg_update_device;')
|
||||
self.db.sql.execute('DROP TRIGGER IF EXISTS trg_delete_device;')
|
||||
|
||||
self.db.sql.execute('DROP TRIGGER IF EXISTS trg_delete_plugin_object;')
|
||||
self.db.sql.execute('DROP TRIGGER IF EXISTS trg_create_plugin_object;')
|
||||
self.db.sql.execute('DROP TRIGGER IF EXISTS trg_update_plugin_object;')
|
||||
|
||||
# Create AppEvent table if missing
|
||||
self.db.sql.execute("""CREATE TABLE IF NOT EXISTS "AppEvents" (
|
||||
"Index" INTEGER,
|
||||
"GUID" TEXT UNIQUE,
|
||||
"AppEventProcessed" BOOLEAN,
|
||||
"DateTimeCreated" TEXT,
|
||||
"ObjectType" TEXT, -- ObjectType (Plugins, Notifications, Events)
|
||||
"ObjectGUID" TEXT,
|
||||
"ObjectPlugin" TEXT,
|
||||
"ObjectPrimaryID" TEXT,
|
||||
"ObjectSecondaryID" TEXT,
|
||||
"ObjectForeignKey" TEXT,
|
||||
"ObjectIndex" TEXT,
|
||||
"ObjectIsNew" BOOLEAN,
|
||||
"ObjectIsArchived" BOOLEAN,
|
||||
"ObjectStatusColumn" TEXT, -- Status (Notifications, Plugins), eve_EventType (Events)
|
||||
"ObjectStatus" TEXT, -- new_devices, down_devices, events, new, watched-changed, watched-not-changed, missing-in-last-scan, Device down, New Device, IP Changed, Connected, Disconnected, VOIDED - Disconnected, VOIDED - Connected, <missing event>
|
||||
"AppEventType" TEXT, -- "create", "update", "delete" (+TBD)
|
||||
"Helper1" TEXT,
|
||||
"Helper2" TEXT,
|
||||
"Helper3" TEXT,
|
||||
"Extra" TEXT,
|
||||
PRIMARY KEY("Index" AUTOINCREMENT)
|
||||
);
|
||||
""")
|
||||
|
||||
# -------------
|
||||
# Device events
|
||||
|
||||
sql_devices_mappedColumns = '''
|
||||
"GUID",
|
||||
"DateTimeCreated",
|
||||
"AppEventProcessed",
|
||||
"ObjectType",
|
||||
"ObjectGUID",
|
||||
"ObjectPrimaryID",
|
||||
"ObjectSecondaryID",
|
||||
"ObjectStatus",
|
||||
"ObjectStatusColumn",
|
||||
"ObjectIsNew",
|
||||
"ObjectIsArchived",
|
||||
"ObjectForeignKey",
|
||||
"AppEventType"
|
||||
'''
|
||||
|
||||
# Trigger for create event
|
||||
self.db.sql.execute(f'''
|
||||
CREATE TRIGGER IF NOT EXISTS "trg_create_device"
|
||||
AFTER INSERT ON "Devices"
|
||||
BEGIN
|
||||
INSERT INTO "AppEvents" (
|
||||
{sql_devices_mappedColumns}
|
||||
)
|
||||
VALUES (
|
||||
{sql_generateGuid},
|
||||
DATETIME('now'),
|
||||
FALSE,
|
||||
'Devices',
|
||||
NEW.devGUID,
|
||||
NEW.devMac,
|
||||
NEW.devLastIP,
|
||||
CASE WHEN NEW.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END,
|
||||
'devPresentLastScan',
|
||||
NEW.devIsNew,
|
||||
NEW.devIsArchived,
|
||||
NEW.devMac,
|
||||
'create'
|
||||
);
|
||||
END;
|
||||
''')
|
||||
|
||||
# 🔴 This would generate too many events, disabled for now
|
||||
# # Trigger for read event
|
||||
# self.db.sql.execute('''
|
||||
# TODO
|
||||
# ''')
|
||||
|
||||
# Trigger for update event
|
||||
self.db.sql.execute(f'''
|
||||
CREATE TRIGGER IF NOT EXISTS "trg_update_device"
|
||||
AFTER UPDATE ON "Devices"
|
||||
BEGIN
|
||||
INSERT INTO "AppEvents" (
|
||||
{sql_devices_mappedColumns}
|
||||
)
|
||||
VALUES (
|
||||
{sql_generateGuid},
|
||||
DATETIME('now'),
|
||||
FALSE,
|
||||
'Devices',
|
||||
NEW.devGUID,
|
||||
NEW.devMac,
|
||||
NEW.devLastIP,
|
||||
CASE WHEN NEW.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END,
|
||||
'devPresentLastScan',
|
||||
NEW.devIsNew,
|
||||
NEW.devIsArchived,
|
||||
NEW.devMac,
|
||||
'update'
|
||||
);
|
||||
END;
|
||||
''')
|
||||
|
||||
# Trigger for delete event
|
||||
self.db.sql.execute(f'''
|
||||
CREATE TRIGGER IF NOT EXISTS "trg_delete_device"
|
||||
AFTER DELETE ON "Devices"
|
||||
BEGIN
|
||||
INSERT INTO "AppEvents" (
|
||||
{sql_devices_mappedColumns}
|
||||
)
|
||||
VALUES (
|
||||
{sql_generateGuid},
|
||||
DATETIME('now'),
|
||||
FALSE,
|
||||
'Devices',
|
||||
OLD.devGUID,
|
||||
OLD.devMac,
|
||||
OLD.devLastIP,
|
||||
CASE WHEN OLD.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END,
|
||||
'devPresentLastScan',
|
||||
OLD.devIsNew,
|
||||
OLD.devIsArchived,
|
||||
OLD.devMac,
|
||||
'delete'
|
||||
);
|
||||
END;
|
||||
''')
|
||||
|
||||
|
||||
# -------------
|
||||
# Plugins_Objects events
|
||||
|
||||
sql_plugins_objects_mappedColumns = '''
|
||||
"GUID",
|
||||
"DateTimeCreated",
|
||||
"AppEventProcessed",
|
||||
"ObjectType",
|
||||
"ObjectGUID",
|
||||
"ObjectPlugin",
|
||||
"ObjectPrimaryID",
|
||||
"ObjectSecondaryID",
|
||||
"ObjectForeignKey",
|
||||
"ObjectStatusColumn",
|
||||
"ObjectStatus",
|
||||
"AppEventType"
|
||||
'''
|
||||
|
||||
# Create trigger for update event on Plugins_Objects
|
||||
self.db.sql.execute(f'''
|
||||
CREATE TRIGGER IF NOT EXISTS trg_update_plugin_object
|
||||
AFTER UPDATE ON Plugins_Objects
|
||||
BEGIN
|
||||
INSERT INTO AppEvents (
|
||||
{sql_plugins_objects_mappedColumns}
|
||||
)
|
||||
VALUES (
|
||||
{sql_generateGuid},
|
||||
DATETIME('now'),
|
||||
FALSE,
|
||||
'Plugins_Objects',
|
||||
NEW.ObjectGUID,
|
||||
NEW.Plugin,
|
||||
NEW.Object_PrimaryID,
|
||||
NEW.Object_SecondaryID,
|
||||
NEW.ForeignKey,
|
||||
'Status',
|
||||
NEW.Status,
|
||||
'update'
|
||||
);
|
||||
END;
|
||||
''')
|
||||
|
||||
# Create trigger for CREATE event on Plugins_Objects
|
||||
self.db.sql.execute(f'''
|
||||
CREATE TRIGGER IF NOT EXISTS trg_create_plugin_object
|
||||
AFTER INSERT ON Plugins_Objects
|
||||
BEGIN
|
||||
INSERT INTO AppEvents (
|
||||
{sql_plugins_objects_mappedColumns}
|
||||
)
|
||||
VALUES (
|
||||
{sql_generateGuid},
|
||||
DATETIME('now'),
|
||||
FALSE,
|
||||
'Plugins_Objects',
|
||||
NEW.ObjectGUID,
|
||||
NEW.Plugin,
|
||||
NEW.Object_PrimaryID,
|
||||
NEW.Object_SecondaryID,
|
||||
NEW.ForeignKey,
|
||||
'Status',
|
||||
NEW.Status,
|
||||
'create'
|
||||
);
|
||||
END;
|
||||
''')
|
||||
|
||||
# Create trigger for DELETE event on Plugins_Objects
|
||||
self.db.sql.execute(f'''
|
||||
CREATE TRIGGER IF NOT EXISTS trg_delete_plugin_object
|
||||
AFTER DELETE ON Plugins_Objects
|
||||
BEGIN
|
||||
INSERT INTO AppEvents (
|
||||
{sql_plugins_objects_mappedColumns}
|
||||
)
|
||||
VALUES (
|
||||
{sql_generateGuid},
|
||||
DATETIME('now'),
|
||||
FALSE,
|
||||
'Plugins_Objects',
|
||||
OLD.ObjectGUID,
|
||||
OLD.Plugin,
|
||||
OLD.Object_PrimaryID,
|
||||
OLD.Object_SecondaryID,
|
||||
OLD.ForeignKey,
|
||||
'Status',
|
||||
OLD.Status,
|
||||
'delete'
|
||||
);
|
||||
END;
|
||||
''')
|
||||
|
||||
self.save()
|
||||
|
||||
# -------------------------------------------------------------------------------
|
||||
# -------------------------------------------------------------------------------
|
||||
# below code is unused
|
||||
# -------------------------------------------------------------------------------
|
||||
|
||||
# Create a new DB entry if new notifications are available, otherwise skip
|
||||
def create(self, Extra="", **kwargs):
|
||||
# Check if nothing to report, end
|
||||
if not any(kwargs.values()):
|
||||
return False
|
||||
|
||||
# Continue and save into DB if notifications are available
|
||||
self.GUID = str(uuid.uuid4())
|
||||
self.DateTimeCreated = timeNowTZ()
|
||||
self.ObjectType = "Plugins" # Modify ObjectType as needed
|
||||
|
||||
# Optional parameters
|
||||
self.ObjectGUID = kwargs.get("ObjectGUID", "")
|
||||
self.ObjectPlugin = kwargs.get("ObjectPlugin", "")
|
||||
self.ObjectMAC = kwargs.get("ObjectMAC", "")
|
||||
self.ObjectIP = kwargs.get("ObjectIP", "")
|
||||
self.ObjectPrimaryID = kwargs.get("ObjectPrimaryID", "")
|
||||
self.ObjectSecondaryID = kwargs.get("ObjectSecondaryID", "")
|
||||
self.ObjectForeignKey = kwargs.get("ObjectForeignKey", "")
|
||||
self.ObjectIndex = kwargs.get("ObjectIndex", "")
|
||||
self.ObjectRowID = kwargs.get("ObjectRowID", "")
|
||||
self.ObjectStatusColumn = kwargs.get("ObjectStatusColumn", "")
|
||||
self.ObjectStatus = kwargs.get("ObjectStatus", "")
|
||||
|
||||
self.AppEventStatus = "new" # Modify AppEventStatus as needed
|
||||
self.Extra = Extra
|
||||
|
||||
self.upsert()
|
||||
|
||||
return True
|
||||
|
||||
def upsert(self):
|
||||
self.db.sql.execute("""
|
||||
INSERT OR REPLACE INTO AppEvents (
|
||||
"GUID",
|
||||
"DateTimeCreated",
|
||||
"ObjectType",
|
||||
"ObjectGUID",
|
||||
"ObjectPlugin",
|
||||
"ObjectMAC",
|
||||
"ObjectIP",
|
||||
"ObjectPrimaryID",
|
||||
"ObjectSecondaryID",
|
||||
"ObjectForeignKey",
|
||||
"ObjectIndex",
|
||||
"ObjectRowID",
|
||||
"ObjectStatusColumn",
|
||||
"ObjectStatus",
|
||||
"AppEventStatus",
|
||||
"Extra"
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
self.GUID,
|
||||
self.DateTimeCreated,
|
||||
self.ObjectType,
|
||||
self.ObjectGUID,
|
||||
self.ObjectPlugin,
|
||||
self.ObjectMAC,
|
||||
self.ObjectIP,
|
||||
self.ObjectPrimaryID,
|
||||
self.ObjectSecondaryID,
|
||||
self.ObjectForeignKey,
|
||||
self.ObjectIndex,
|
||||
self.ObjectRowID,
|
||||
self.ObjectStatusColumn,
|
||||
self.ObjectStatus,
|
||||
self.AppEventStatus,
|
||||
self.Extra
|
||||
))
|
||||
|
||||
self.save()
|
||||
|
||||
def save(self):
|
||||
# Commit changes
|
||||
self.db.commitDB()
|
||||
|
||||
|
||||
def getPluginObject(**kwargs):
|
||||
|
||||
# Check if nothing, end
|
||||
if not any(kwargs.values()):
|
||||
return None
|
||||
|
||||
# Optional parameters
|
||||
GUID = kwargs.get("GUID", "")
|
||||
Plugin = kwargs.get("Plugin", "")
|
||||
MAC = kwargs.get("MAC", "")
|
||||
IP = kwargs.get("IP", "")
|
||||
PrimaryID = kwargs.get("PrimaryID", "")
|
||||
SecondaryID = kwargs.get("SecondaryID", "")
|
||||
ForeignKey = kwargs.get("ForeignKey", "")
|
||||
Index = kwargs.get("Index", "")
|
||||
RowID = kwargs.get("RowID", "")
|
||||
|
||||
# we need the plugin
|
||||
if Plugin == "":
|
||||
return None
|
||||
|
||||
plugins_objects = apiPath + 'table_plugins_objects.json'
|
||||
|
||||
try:
|
||||
with open(plugins_objects, 'r') as json_file:
|
||||
|
||||
data = json.load(json_file)
|
||||
|
||||
for item in data.get("data",[]):
|
||||
if item.get("Index") == Index:
|
||||
return item
|
||||
|
||||
for item in data.get("data",[]):
|
||||
if item.get("ObjectPrimaryID") == PrimaryID and item.get("ObjectSecondaryID") == SecondaryID:
|
||||
return item
|
||||
|
||||
for item in data.get("data",[]):
|
||||
if item.get("ObjectPrimaryID") == MAC and item.get("ObjectSecondaryID") == IP:
|
||||
return item
|
||||
|
||||
for item in data.get("data",[]):
|
||||
if item.get("ObjectPrimaryID") == PrimaryID and item.get("ObjectSecondaryID") == IP:
|
||||
return item
|
||||
|
||||
for item in data.get("data",[]):
|
||||
if item.get("ObjectPrimaryID") == MAC and item.get("ObjectSecondaryID") == IP:
|
||||
return item
|
||||
|
||||
|
||||
mylog('debug', [f'[{module_name}] ⚠ ERROR - Object not found - GUID:{GUID} | Plugin:{Plugin} | MAC:{MAC} | IP:{IP} | PrimaryID:{PrimaryID} | SecondaryID:{SecondaryID} | ForeignKey:{ForeignKey} | Index:{Index} | RowID:{RowID} '])
|
||||
|
||||
return None
|
||||
|
||||
except (FileNotFoundError, json.JSONDecodeError, ValueError) as e:
|
||||
# Handle the case when the file is not found, JSON decoding fails, or data is not in the expected format
|
||||
mylog('none', [f'[{module_name}] ⚠ ERROR - JSONDecodeError or FileNotFoundError for file {plugins_objects}'])
|
||||
|
||||
return None
|
||||
|
||||
83
server/workflows/conditions.py
Executable file
83
server/workflows/conditions.py
Executable file
@@ -0,0 +1,83 @@
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
|
||||
# Register NetAlertX directories
|
||||
INSTALL_PATH="/app"
|
||||
sys.path.extend([f"{INSTALL_PATH}/server"])
|
||||
|
||||
import conf
|
||||
from logger import mylog, Logger
|
||||
from helper import get_setting_value, timeNowTZ
|
||||
|
||||
# Make sure log level is initialized correctly
|
||||
Logger(get_setting_value('LOG_LEVEL'))
|
||||
|
||||
class Condition:
|
||||
"""Evaluates a single condition."""
|
||||
|
||||
def __init__(self, condition_json):
|
||||
self.field = condition_json["field"]
|
||||
self.operator = condition_json["operator"]
|
||||
self.value = condition_json["value"]
|
||||
self.negate = condition_json.get("negate", False)
|
||||
|
||||
def evaluate(self, trigger):
|
||||
|
||||
# try finding the value of the field on the event triggering this workflow or thre object triggering the app event
|
||||
appEvent_value = trigger.event[self.field] if self.field in trigger.event.keys() else None
|
||||
eveObj_value = trigger.object[self.field] if self.field in trigger.object.keys() else None
|
||||
|
||||
|
||||
# proceed only if value found
|
||||
if appEvent_value is None and eveObj_value is None:
|
||||
return False
|
||||
elif appEvent_value is not None:
|
||||
obj_value = appEvent_value
|
||||
elif eveObj_value is not None:
|
||||
obj_value = eveObj_value
|
||||
|
||||
# process based on operators
|
||||
if self.operator == "equals":
|
||||
result = str(obj_value) == str(self.value)
|
||||
elif self.operator == "contains":
|
||||
result = str(self.value) in str(obj_value)
|
||||
elif self.operator == "regex":
|
||||
result = bool(re.match(self.value, str(obj_value)))
|
||||
else:
|
||||
m = f"[WF] Unsupported operator: {self.operator}"
|
||||
mylog('none', [m])
|
||||
raise ValueError(m)
|
||||
|
||||
return not result if self.negate else result
|
||||
|
||||
|
||||
class ConditionGroup:
|
||||
"""Handles condition groups with AND, OR logic, supporting nested groups."""
|
||||
|
||||
def __init__(self, group_json):
|
||||
|
||||
mylog('none', ["[WF] json.dumps(group_json)"])
|
||||
mylog('none', [json.dumps(group_json)])
|
||||
mylog('none', [group_json])
|
||||
|
||||
self.logic = group_json.get("logic", "AND").upper()
|
||||
self.conditions = []
|
||||
|
||||
for condition in group_json["conditions"]:
|
||||
if "field" in condition: # Simple condition
|
||||
self.conditions.append(Condition(condition))
|
||||
else: # Nested condition group
|
||||
self.conditions.append(ConditionGroup(condition))
|
||||
|
||||
def evaluate(self, event):
|
||||
results = [condition.evaluate(event) for condition in self.conditions]
|
||||
|
||||
if self.logic == "AND":
|
||||
return all(results)
|
||||
elif self.logic == "OR":
|
||||
return any(results)
|
||||
else:
|
||||
m = f"[WF] Unsupported logic: {self.logic}"
|
||||
mylog('none', [m])
|
||||
raise ValueError(m)
|
||||
154
server/workflows/manager.py
Executable file
154
server/workflows/manager.py
Executable file
@@ -0,0 +1,154 @@
|
||||
import sys
|
||||
import json
|
||||
|
||||
# Register NetAlertX directories
|
||||
INSTALL_PATH="/app"
|
||||
sys.path.extend([f"{INSTALL_PATH}/server"])
|
||||
|
||||
import conf
|
||||
from const import fullConfFolder
|
||||
import workflows.actions
|
||||
from logger import mylog, Logger
|
||||
from helper import get_setting_value, timeNowTZ
|
||||
|
||||
# Make sure log level is initialized correctly
|
||||
Logger(get_setting_value('LOG_LEVEL'))
|
||||
|
||||
from workflows.triggers import Trigger
|
||||
from workflows.conditions import ConditionGroup
|
||||
from workflows.actions import *
|
||||
|
||||
class WorkflowManager:
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
self.workflows = self.load_workflows()
|
||||
self.update_api = False
|
||||
|
||||
def load_workflows(self):
|
||||
"""Load workflows from workflows.json."""
|
||||
try:
|
||||
workflows_json_path = fullConfFolder + '/workflows.json'
|
||||
with open(workflows_json_path, 'r') as f:
|
||||
workflows = json.load(f)
|
||||
return workflows
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
mylog('none', ['[WF] Failed to load workflows.json'])
|
||||
return []
|
||||
|
||||
def get_new_app_events(self):
|
||||
"""Get new unprocessed events from the AppEvents table."""
|
||||
result = self.db.sql.execute("""
|
||||
SELECT * FROM AppEvents
|
||||
WHERE AppEventProcessed = 0
|
||||
ORDER BY DateTimeCreated ASC
|
||||
""").fetchall()
|
||||
return result
|
||||
|
||||
def process_event(self, event):
|
||||
"""Process the events. Check if events match a workflow trigger"""
|
||||
|
||||
mylog('verbose', [f"[WF] Processing event with GUID {event["GUID"]}"])
|
||||
|
||||
# Check if the trigger conditions match
|
||||
for workflow in self.workflows:
|
||||
|
||||
# construct trigger object which also evaluates if the current event triggers it
|
||||
trigger = Trigger(workflow["trigger"], event, self.db)
|
||||
|
||||
if trigger.triggered:
|
||||
|
||||
mylog('verbose', [f"[WF] Event with GUID '{event["GUID"]}' triggered the workflow '{workflow["name"]}'"])
|
||||
|
||||
self.execute_workflow(workflow, trigger)
|
||||
|
||||
# After processing the event, mark the event as processed (set AppEventProcessed to 1)
|
||||
self.db.sql.execute("""
|
||||
UPDATE AppEvents
|
||||
SET AppEventProcessed = 1
|
||||
WHERE "Index" = ?
|
||||
""", (event['Index'],)) # Pass the event's unique identifier
|
||||
self.db.commitDB()
|
||||
|
||||
|
||||
|
||||
def execute_workflow(self, workflow, trigger):
|
||||
"""Execute the actions in the given workflow if conditions are met."""
|
||||
|
||||
# Ensure conditions exist
|
||||
if not isinstance(workflow.get("conditions"), list):
|
||||
m = f"[WF] workflow['conditions'] must be a list"
|
||||
mylog('none', [m])
|
||||
raise ValueError(m)
|
||||
|
||||
# Evaluate each condition group separately
|
||||
for condition_group in workflow["conditions"]:
|
||||
|
||||
evaluator = ConditionGroup(condition_group)
|
||||
|
||||
if evaluator.evaluate(trigger): # If any group evaluates to True
|
||||
|
||||
mylog('none', [f"[WF] Workflow {workflow["name"]} will be executed - conditions were evalueted as TRUE"])
|
||||
mylog('debug', [f"[WF] Workflow condition_group: {condition_group}"])
|
||||
|
||||
self.execute_actions(workflow["actions"], trigger)
|
||||
return # Stop if a condition group succeeds
|
||||
|
||||
mylog('none', ["[WF] No condition group matched. Actions not executed."])
|
||||
|
||||
|
||||
def execute_actions(self, actions, trigger):
|
||||
"""Execute the actions defined in a workflow."""
|
||||
|
||||
for action in actions:
|
||||
if action["type"] == "update_field":
|
||||
field = action["field"]
|
||||
value = action["value"]
|
||||
action_instance = UpdateFieldAction(field, value, trigger)
|
||||
# indicate if the api has to be updated
|
||||
self.update_api = True
|
||||
|
||||
elif action["type"] == "run_plugin":
|
||||
plugin_name = action["plugin"]
|
||||
params = action["params"]
|
||||
action_instance = RunPluginAction(plugin_name, params, trigger)
|
||||
|
||||
# elif action["type"] == "send_notification":
|
||||
# method = action["method"]
|
||||
# message = action["message"]
|
||||
# action_instance = SendNotificationAction(method, message, trigger)
|
||||
|
||||
else:
|
||||
m = f"[WF] Unsupported action type: {action['type']}"
|
||||
mylog('none', [m])
|
||||
raise ValueError(m)
|
||||
|
||||
action_instance.execute() # Execute the action
|
||||
|
||||
# if result:
|
||||
# # Iterate through actions and execute them
|
||||
# for action in workflow["actions"]:
|
||||
# if action["type"] == "update_field":
|
||||
# # Action type is "update_field", so map to UpdateFieldAction
|
||||
# field = action["field"]
|
||||
# value = action["value"]
|
||||
# action_instance = UpdateFieldAction(field, value)
|
||||
# action_instance.execute(trigger.event)
|
||||
|
||||
# elif action["type"] == "run_plugin":
|
||||
# # Action type is "run_plugin", so map to RunPluginAction
|
||||
# plugin_name = action["plugin"]
|
||||
# params = action["params"]
|
||||
# action_instance = RunPluginAction(plugin_name, params)
|
||||
# action_instance.execute(trigger.event)
|
||||
# elif action["type"] == "send_notification":
|
||||
# # Action type is "send_notification", so map to SendNotificationAction
|
||||
# method = action["method"]
|
||||
# message = action["message"]
|
||||
# action_instance = SendNotificationAction(method, message)
|
||||
# action_instance.execute(trigger.event)
|
||||
# else:
|
||||
# # Handle unsupported action types
|
||||
# raise ValueError(f"Unsupported action type: {action['type']}")
|
||||
|
||||
|
||||
|
||||
62
server/workflows/triggers.py
Executable file
62
server/workflows/triggers.py
Executable file
@@ -0,0 +1,62 @@
|
||||
import sys
|
||||
|
||||
# Register NetAlertX directories
|
||||
INSTALL_PATH="/app"
|
||||
sys.path.extend([f"{INSTALL_PATH}/server"])
|
||||
|
||||
import conf
|
||||
from logger import mylog, Logger
|
||||
from helper import get_setting_value, timeNowTZ
|
||||
|
||||
# Make sure log level is initialized correctly
|
||||
Logger(get_setting_value('LOG_LEVEL'))
|
||||
|
||||
|
||||
class Trigger:
|
||||
"""Represents a trigger definition"""
|
||||
|
||||
def __init__(self, triggerJson, event, db):
|
||||
"""
|
||||
:param name: Friendly name of the trigger
|
||||
:param triggerJson: JSON trigger object {"object_type":"Devices",event_type":"update"}
|
||||
:param event: The actual event that the trigger is evaluated against
|
||||
:param db: DB connection in case trigger matches and object needs to be retrieved
|
||||
"""
|
||||
self.object_type = triggerJson["object_type"]
|
||||
self.event_type = triggerJson["event_type"]
|
||||
self.event = event # Store the triggered event context, if provided
|
||||
self.triggered = self.object_type == event["ObjectType"] and self.event_type == event["AppEventType"]
|
||||
|
||||
mylog('verbose', [f"[WF] self.triggered '{self.triggered}'"])
|
||||
|
||||
if self.triggered:
|
||||
# object type corresponds with the DB table name
|
||||
db_table = self.object_type
|
||||
|
||||
if db_table == "Devices":
|
||||
refField = "devGUID"
|
||||
elif db_table == "Plugins_Objects":
|
||||
refField = "ObjectGUID"
|
||||
else:
|
||||
m = f"[WF] Unsupported object_type: {self.object_type}"
|
||||
mylog('none', [m])
|
||||
raise ValueError(m)
|
||||
|
||||
query = f"""
|
||||
SELECT * FROM
|
||||
{db_table}
|
||||
WHERE {refField} = '{event["ObjectGUID"]}'
|
||||
"""
|
||||
|
||||
mylog('debug', [query])
|
||||
|
||||
result = db.sql.execute(query).fetchall()
|
||||
self.object = result[0]
|
||||
else:
|
||||
self.object = None
|
||||
|
||||
|
||||
def set_event(self, event):
|
||||
"""Set or update the event context for this trigger"""
|
||||
self.event = event
|
||||
|
||||
Reference in New Issue
Block a user