Initial commit on next_release branch

This commit is contained in:
jokob-sk
2025-03-10 07:42:44 +11:00
parent f5713d4178
commit 432a4d9d69
52 changed files with 1508 additions and 218 deletions

81
back/workflows.json Executable file
View File

@@ -0,0 +1,81 @@
[
{
"name": "Sample Device Update Workflow",
"trigger": {
"object_type": "Devices",
"event_type": "update"
},
"conditions": [
{
"logic": "AND",
"conditions": [
{
"field": "devVendor",
"operator": "contains",
"value": "Google"
},
{
"field": "devIsNew",
"operator": "equals",
"value": "1"
},
{
"logic": "OR",
"conditions": [
{
"field": "devIsNew",
"operator": "equals",
"value": "1"
},
{
"field": "devName",
"operator": "contains",
"value": "Google"
}
]
}
]
}
],
"actions": [
{
"type": "update_field",
"field": "devIsNew",
"value": "0"
},
{
"type": "run_plugin",
"plugin": "SMTP",
"params": {
"message": "New device from Google detected."
}
}
]
},
{
"name": "Sample Plugin Object Workflow",
"trigger": {
"object_type": "Plugins_Objects",
"event_type": "create"
},
"conditions": [
{
"logic": "AND",
"conditions": [
{
"field": "Plugin",
"operator": "equals",
"value": "ARPSCAN"
},
{
"field": "Status",
"operator": "equals",
"value": "missing-in-last-scan"
}
]
}
],
"actions": [
]
}
]

View File

@@ -61,6 +61,8 @@ services:
- ${DEV_LOCATION}/front/cloud_services.php:/app/front/cloud_services.php
- ${DEV_LOCATION}/front/report.php:/app/front/report.php
- ${DEV_LOCATION}/front/workflows.php:/app/front/workflows.php
- ${DEV_LOCATION}/front/workflowsCore.php:/app/front/workflowsCore.php
- ${DEV_LOCATION}/front/appEvents.php:/app/front/appEvents.php
- ${DEV_LOCATION}/front/appEventsCore.php:/app/front/appEventsCore.php
- ${DEV_LOCATION}/front/multiEditCore.php:/app/front/multiEditCore.php
- ${DEV_LOCATION}/front/plugins:/app/front/plugins

View File

@@ -37,6 +37,7 @@ export INSTALL_DIR=/app # Specify the installation directory here
# DO NOT CHANGE ANYTHING BELOW THIS LINE!
CONF_FILE="app.conf"
WF_FILE="workflows.json"
NGINX_CONF_FILE=netalertx.conf
DB_FILE="app.db"
FULL_FILEDB_PATH="${INSTALL_DIR}/db/${DB_FILE}"
@@ -54,8 +55,6 @@ if [[ $EUID -ne 0 ]]; then
exit 1
fi
echo "[INSTALL] Copy starter ${DB_FILE} and ${CONF_FILE} if they don't exist"
# DANGER ZONE: ALWAYS_FRESH_INSTALL
if [ "$ALWAYS_FRESH_INSTALL" = true ]; then
echo "[INSTALL] ❗ ALERT /db and /config folders are cleared because the ALWAYS_FRESH_INSTALL is set to: $ALWAYS_FRESH_INSTALL"
@@ -96,8 +95,11 @@ if [ -f "${INSTALL_DIR_OLD}/config/${OLD_APP_NAME}.conf" ]; then
fi
# 🔺 FOR BACKWARD COMPATIBILITY - REMOVE AFTER 12/12/2025
# Copy starter .db and .conf if they don't exist
echo "[INSTALL] Copy starter ${DB_FILE} and ${CONF_FILE} if they don't exist"
# Copy starter app.db, app.conf, workflows.json if they don't exist
cp -na "${INSTALL_DIR}/back/${CONF_FILE}" "${INSTALL_DIR}/config/${CONF_FILE}"
cp -na "${INSTALL_DIR}/back/${WF_FILE}" "${INSTALL_DIR}/config/${WF_FILE}"
cp -na "${INSTALL_DIR}/back/${DB_FILE}" "${FULL_FILEDB_PATH}"
# if custom variables not set we do not need to do anything
@@ -143,6 +145,7 @@ fi
# Create the execution_queue.log and app_front.log files if they don't exist
touch "${INSTALL_DIR}"/log/{app.log,execution_queue.log,app_front.log,app.php_errors.log,stderr.log,stdout.log,db_is_locked.log}
touch "${INSTALL_DIR}"/api/user_notifications.json
# Create plugins sub-directory if it doesn't exist in case a custom log folder is used
mkdir -p "${INSTALL_DIR}"/log/plugins

21
front/appEvents.php Executable file
View File

@@ -0,0 +1,21 @@
<?php
require 'php/templates/header.php';
require 'php/templates/notification.php';
?>
<!-- ----------------------------------------------------------------------- -->
<!-- Page ------------------------------------------------------------------ -->
<div class="content-wrapper">
<?php
require 'appEventsCore.php';
?>
</div>
<?php
require 'php/templates/footer.php';
?>

View File

@@ -1,12 +1,12 @@
<section class="content">
<div class="nav-tabs-custom app-event-content" style="margin-bottom: 0px;">
<ul id="tabs-location" class="nav nav-tabs col-sm-2">
<li class="left-nav"><a class="col-sm-12" href="#" id="" data-toggle="tab">Events</a></li>
</ul>
<div id="tabs-content-location" class="tab-content col-sm-10">
<table class="table table-striped" id="appevents-table" data-my-dbtable="AppEvents"></table>
</div>
<div class="nav-tabs-custom app-event-content" style="margin-bottom: 0px;">
<ul id="tabs-location" class="nav nav-tabs col-sm-2 hidden">
<li class="left-nav"><a class="col-sm-12" href="#" id="" data-toggle="tab">Events</a></li>
</ul>
<div id="tabs-content-location" class="tab-content col-sm-12">
<table class="table table-striped" id="appevents-table" data-my-dbtable="AppEvents"></table>
</div>
</div>
</section>
@@ -18,75 +18,110 @@ showSpinner()
$(document).ready(function() {
// Load JSON data from the provided URL
$.getJSON('/php/server/query_json.php?file=table_appevents.json', function(data) {
// Process the JSON data and generate UI dynamically
processData(data)
// Load JSON data from the provided URL
$.getJSON('/php/server/query_json.php?file=table_appevents.json', function(data) {
// Process the JSON data and generate UI dynamically
processData(data)
// hide loading dialog
hideSpinner()
});
// hide loading dialog
hideSpinner()
});
});
function processData(data) {
// Create an object to store unique ObjectType values as app event identifiers
var appEventIdentifiers = {};
// Create an object to store unique ObjectType values as app event identifiers
var appEventIdentifiers = {};
// Array to accumulate data for DataTable
var allData = [];
// Array to accumulate data for DataTable
var allData = [];
// Iterate through the data and generate tabs and content dynamically
$.each(data.data, function(index, item) {
// Accumulate data for DataTable
allData.push(item);
});
// Initialize DataTable for all app events
// Iterate through the data and generate tabs and content dynamically
$.each(data.data, function(index, item) {
$('#appevents-table').DataTable({
data: allData,
paging: true,
lengthChange: true,
lengthMenu: [[10, 25, 50, 100, 500, -1], [10, 25, 50, 100, 500, 'All']],
searching: true,
ordering: true,
info: true,
autoWidth: false,
pageLength: 25, // Set the default paging to 25
columns: [
{ data: 'DateTimeCreated', title: getString('AppEvents_DateTimeCreated') },
{ data: 'AppEventType', title: getString('AppEvents_Type') },
{ data: 'ObjectType', title: getString('AppEvents_ObjectType') },
{ data: 'ObjectPrimaryID', title: getString('AppEvents_ObjectPrimaryID') },
{ data: 'ObjectSecondaryID', title: getString('AppEvents_ObjectSecondaryID') },
{ data: 'ObjectStatus', title: getString('AppEvents_ObjectStatus') },
{ data: 'Extra', title: getString('AppEvents_Extra') },
{ data: 'ObjectPlugin', title: getString('AppEvents_Plugin') },
// Add other columns as needed
],
// Add column-specific configurations if needed
columnDefs: [
{ className: 'text-center', targets: [3] },
{ width: '80px', targets: [6] },
// ... Add other columnDefs as needed
// Full MAC
{targets: [3, 4],
'createdCell': function (td, cellData, rowData, row, col) {
if (!emptyArr.includes(cellData)){
$(td).html (createDeviceLink(cellData));
} else {
$(td).html ('');
}
} },
]
});
// Accumulate data for DataTable
allData.push(item);
});
console.log(allData);
// Initialize DataTable for all app events
$('#appevents-table').DataTable({
data: allData,
paging: true,
lengthChange: true,
lengthMenu: [[10, 25, 50, 100, 500, -1], [10, 25, 50, 100, 500, 'All']],
searching: true,
ordering: true,
info: true,
autoWidth: false,
pageLength: 25, // Set the default paging to 25
columns: [
{ data: 'DateTimeCreated', title: getString('AppEvents_DateTimeCreated') },
{ data: 'AppEventProcessed', title: getString('AppEvents_AppEventProcessed') },
{ data: 'AppEventType', title: getString('AppEvents_Type') },
{ data: 'ObjectType', title: getString('AppEvents_ObjectType') },
{ data: 'ObjectPrimaryID', title: getString('AppEvents_ObjectPrimaryID') },
{ data: 'ObjectSecondaryID', title: getString('AppEvents_ObjectSecondaryID') },
{ data: 'ObjectStatus', title: getString('AppEvents_ObjectStatus') },
{ data: 'ObjectPlugin', title: getString('AppEvents_Plugin') },
{ data: 'ObjectGUID', title: "GUID" },
// Add other columns as needed
],
// Add column-specific configurations if needed
columnDefs: [
{ className: 'text-center', targets: [4] },
{ width: '80px', targets: [7] },
// ... Add other columnDefs as needed
// Full MAC
{targets: [4, 5],
'createdCell': function (td, cellData, rowData, row, col) {
if (!emptyArr.includes(cellData)){
$(td).html (createDeviceLink(cellData));
} else {
$(td).html ('');
}
} },
// Processed
{targets: [1],
'createdCell': function (td, cellData, rowData, row, col) {
// console.log(cellData);
$(td).html (cellData);
}
},
// Datetime
{targets: [0],
'createdCell': function (td, cellData, rowData, row, col) {
let timezone = $("#NAX_TZ").html(); // e.g., 'Europe/Berlin'
let utcDate = new Date(cellData + ' UTC'); // Adding ' UTC' makes it interpreted as UTC time
// Format the date in the desired timezone
let options = {
year: 'numeric',
month: 'short',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false, // Use 24-hour format
timeZone: timezone // Use the specified timezone
};
let localDate = new Intl.DateTimeFormat('en-GB', options).format(utcDate);
// Update the table cell
$(td).html(localDate);
}
},
]
});
// Activate the first tab
$('#tabs-location li:first-child').addClass('active');
$('#tabs-content-location .tab-pane:first-child').addClass('active');
// Activate the first tab
$('#tabs-location li:first-child').addClass('active');
$('#tabs-content-location .tab-pane:first-child').addClass('active');
}
</script>

View File

@@ -1840,6 +1840,47 @@ input[readonly] {
height:50px;
}
/* -----------------------------------------------------------------------------
Workflows
----------------------------------------------------------------------------- */
#workflowContainerWrap .panel-collapse
{
padding: 5px;
}
.workflows .btn-secondary{
color: #000;
}
.workflows .condition-list button
{
margin: 2px;
}
.workflows button
{
/* width:100%; */
}
#workflowContainerWrap
{
display: contents;
}
.workflow-card, .condition-list, .actions-list
{
display: grid;
padding: 5px;
padding-left: 10px;
}
.condition
{
padding: 5px;
padding-left: 10px;
}
/* -----------------------------------------------------------------------------
Floating edit button
----------------------------------------------------------------------------- */

View File

@@ -123,7 +123,7 @@
<!-- page script ----------------------------------------------------------- -->
<script>
var deviceStatus = 'all';
var tableRows = getCache ("nax_parTableRows") == "" ? 10 : getCache ("nax_parTableRows") ;
var tableRows = getCache ("nax_parTableRows") == "" ? 20 : getCache ("nax_parTableRows") ;
var tableOrder = getCache ("nax_parTableOrder") == "" ? [[3,'desc'], [0,'asc']] : JSON.parse(getCache ("nax_parTableOrder")) ;
var tableColumnHide = [];
@@ -737,7 +737,7 @@ function initializeDatatable (status) {
},
'paging' : true,
'lengthChange' : true,
'lengthMenu' : [[10, 25, 50, 100, 500, 100000], [10, 25, 50, 100, 500, getString('Device_Tablelenght_all')]],
'lengthMenu' : [[10, 20, 25, 50, 100, 500, 100000], [10, 20, 25, 50, 100, 500, getString('Device_Tablelenght_all')]],
'searching' : true,
'ordering' : true,

View File

@@ -17,7 +17,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
// Check if file parameter is provided
if ($file) {
// Define the folder where files are located
$filePath = "/app/api/" . basename($file);
if ($file == "workflows.json")
{
$filePath = "/app/config/" . basename($file);
} else
{
$filePath = "/app/api/" . basename($file);
}
// Check if the file exists
if (file_exists($filePath)) {
@@ -34,5 +40,38 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
http_response_code(400);
echo json_encode(["error" => "Missing 'file' parameter"]);
}
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Read the input JSON data
$inputData = file_get_contents("php://input");
$decodedData = json_decode($inputData, true);
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
echo json_encode(["error" => "Invalid JSON data"]);
exit;
}
// Check if file parameter is provided and is workflows.json
if (!isset($_GET['file']) || $_GET['file'] !== "workflows.json") {
http_response_code(400);
echo json_encode(["error" => "Invalid or missing file parameter"]);
exit;
}
$file = $_GET['file'];
$filePath = "/app/config/" . basename($file);
// Save new workflows.json (replace existing content)
if (file_put_contents($filePath, json_encode($decodedData, JSON_PRETTY_PRINT))) {
http_response_code(200);
echo json_encode(["success" => "Workflows replaced successfully"]);
} else {
http_response_code(500);
echo json_encode(["error" => "Failed to update workflows.json"]);
}
}
else {
http_response_code(405);
echo json_encode(["error" => "Method Not Allowed"]);
}
?>

View File

@@ -150,7 +150,8 @@
let formattedDateTime = `${day}-${month}-${year} ${hour}:${minute}:${second}`;
if (document.getElementById) {
document.getElementById("PIA_Servertime_place").innerHTML = '(' + formattedDateTime + ')';
document.getElementById("NAX_Servertime_plc").innerHTML = '(' + formattedDateTime + ')';
document.getElementById("NAX_TZ").innerHTML = timeZone;
}
setTimeout(update_servertime, 1000); // Call recursively every second
@@ -234,7 +235,13 @@
<!-- Server Name -->
<li>
<div class="header-server-time small">
<div><?php echo gethostname();?></div> <div><span id="PIA_Servertime_place"></span></div>
<div>
<?php echo gethostname();?>
</div>
<div>
<span id="NAX_Servertime_plc"></span>
<span id="NAX_TZ" class="hidden"></span>
</div>
</div>
</li>
@@ -414,18 +421,22 @@
</li>
<!-- Integrations menu item -->
<li class=" treeview <?php if (in_array (basename($_SERVER['SCRIPT_NAME']), array('plugins.php', 'workflows.php' ) ) ){ echo 'active menu-open'; } ?>">
<li class=" treeview <?php if (in_array (basename($_SERVER['SCRIPT_NAME']), array('plugins.php', 'workflows.php', 'appEvents.php' ) ) ){ echo 'active menu-open'; } ?>">
<a href="#">
<i class="fa fa-fw fa-plug"></i> <span><?= lang('Navigation_Integrations');?></span>
<span class="pull-right-container">
<i class="fa fa-angle-left pull-right"></i>
</span>
</a>
<ul class="treeview-menu " style="display: <?php if (in_array (basename($_SERVER['SCRIPT_NAME']), array('plugins.php', 'workflows.php' ) ) ){ echo 'block'; } else {echo 'none';} ?>;">
<ul class="treeview-menu " style="display: <?php if (in_array (basename($_SERVER['SCRIPT_NAME']), array('plugins.php', 'workflows.php', 'appEvents.php' ) ) ){ echo 'block'; } else {echo 'none';} ?>;">
<li>
<div class="info-icon-nav"> </div>
<a href="workflows.php"><?= lang('Navigation_Workflows');?></a>
</li>
<li>
<div class="info-icon-nav"> </div>
<a href="appEvents.php"><?= lang('Navigation_AppEvents');?></a>
</li>
<li>
<a href="plugins.php"><?= lang("Navigation_Plugins");?> </a>
</li>

View File

@@ -8,6 +8,7 @@
"About_Design": "",
"About_Exit": "",
"About_Title": "",
"AppEvents_AppEventProcessed": "",
"AppEvents_DateTimeCreated": "",
"AppEvents_Extra": "",
"AppEvents_GUID": "",
@@ -464,6 +465,7 @@
"NETWORK_DEVICE_TYPES_description": "",
"NETWORK_DEVICE_TYPES_name": "",
"Navigation_About": "",
"Navigation_AppEvents": "",
"Navigation_Devices": "",
"Navigation_Donations": "",
"Navigation_Events": "",

View File

@@ -8,6 +8,7 @@
"About_Design": "Dissenyat per:",
"About_Exit": "Sortir",
"About_Title": "Escàner de seguretat de xarxa i marc de notificacions",
"AppEvents_AppEventProcessed": "",
"AppEvents_DateTimeCreated": "Logged",
"AppEvents_Extra": "Extra",
"AppEvents_GUID": "GUID d'esdeveniments d'Aplicació",
@@ -464,6 +465,7 @@
"NETWORK_DEVICE_TYPES_description": "Quins tipus de dispositius es poden utilitzar com a dispositius de xarxa a la vista \"xarxa\". El tipus de dispositiu ha de coincidir exactament amb la configuració <code>Tipus</code> dels detalls de dispositiu. Afegir-ho al dispositiu fent servir el botó <code>+</code>. No elimini els tipus existents, només afegir-ne nous.",
"NETWORK_DEVICE_TYPES_name": "Tipus de dispositiu de xarxa",
"Navigation_About": "Sobre",
"Navigation_AppEvents": "",
"Navigation_Devices": "Dispositius",
"Navigation_Donations": "Donacions",
"Navigation_Events": "Esdeveniments",
@@ -716,4 +718,4 @@
"settings_update_item_warning": "Actualitza el valor sota. Sigues curós de seguir el format anterior. <b>No hi ha validació.</b>",
"test_event_icon": "fa-vial-circle-check",
"test_event_tooltip": "Deseu els canvis primer abans de comprovar la configuració."
}
}

View File

@@ -8,6 +8,7 @@
"About_Design": "",
"About_Exit": "",
"About_Title": "",
"AppEvents_AppEventProcessed": "",
"AppEvents_DateTimeCreated": "",
"AppEvents_Extra": "",
"AppEvents_GUID": "",
@@ -464,6 +465,7 @@
"NETWORK_DEVICE_TYPES_description": "",
"NETWORK_DEVICE_TYPES_name": "",
"Navigation_About": "",
"Navigation_AppEvents": "",
"Navigation_Devices": "",
"Navigation_Donations": "",
"Navigation_Events": "",

View File

@@ -16,6 +16,7 @@
"About_Design": "Entworfen für:",
"About_Exit": "Abmelden",
"About_Title": "Netzwerksicherheitsscanner und Benachrichtigungsframework",
"AppEvents_AppEventProcessed": "",
"AppEvents_DateTimeCreated": "protokolliert",
"AppEvents_Extra": "Extra",
"AppEvents_GUID": "Anwendungsereignis-GUID",
@@ -500,6 +501,7 @@
"NTFY_display_name": "NTFY",
"NTFY_icon": "<i class=\"fa fa-terminal\"></i>",
"Navigation_About": "Über",
"Navigation_AppEvents": "",
"Navigation_Devices": "Geräte",
"Navigation_Donations": "Spenden",
"Navigation_Events": "Ereignisse",
@@ -797,4 +799,4 @@
"settings_update_item_warning": "",
"test_event_icon": "",
"test_event_tooltip": "Speichere die Änderungen, bevor Sie die Einstellungen testen."
}
}

View File

@@ -8,6 +8,7 @@
"About_Design": "Designed for:",
"About_Exit": "Sign out",
"About_Title": "Network security scanner & notification framework",
"AppEvents_AppEventProcessed": "Processed",
"AppEvents_DateTimeCreated": "Logged",
"AppEvents_Extra": "Extra",
"AppEvents_GUID": "Application Event GUID",
@@ -464,6 +465,7 @@
"NETWORK_DEVICE_TYPES_description": "Which device types are allowed to be used as network devices in the Network view. The device type has to match exactly the <code>Type</code> setting on a specific device in Device details. Add it on the Device via the <code>+</code> button. Do not remove existing types, only add new ones.",
"NETWORK_DEVICE_TYPES_name": "Network device types",
"Navigation_About": "About",
"Navigation_AppEvents": "App Events",
"Navigation_Devices": "Devices",
"Navigation_Donations": "Donations",
"Navigation_Events": "Events",

View File

@@ -16,6 +16,7 @@
"About_Design": "Diseñado para:",
"About_Exit": "Salir",
"About_Title": "Escáner de seguridad de la red y marco de notificaciones",
"AppEvents_AppEventProcessed": "",
"AppEvents_DateTimeCreated": "Registrado",
"AppEvents_Extra": "Extra",
"AppEvents_GUID": "GUID del evento de aplicación",
@@ -498,6 +499,7 @@
"NTFY_display_name": "NTFY",
"NTFY_icon": "<i class=\"fa fa-terminal\"></i>",
"Navigation_About": "Acerca de",
"Navigation_AppEvents": "",
"Navigation_Devices": "Dispositivos",
"Navigation_Donations": "Donaciones",
"Navigation_Events": "Eventos",

View File

@@ -8,6 +8,7 @@
"About_Design": "Conçu pour:",
"About_Exit": "Se déconnecter",
"About_Title": "Analyse de la sécurité du réseau et cadre de notification",
"AppEvents_AppEventProcessed": "",
"AppEvents_DateTimeCreated": "Connecté",
"AppEvents_Extra": "Extra",
"AppEvents_GUID": "GUID dévénements de l'application",
@@ -464,6 +465,7 @@
"NETWORK_DEVICE_TYPES_description": "Les types d'appareils autorisés à être utilisés comme appareils réseau dans la vue Réseau. Le type d'appareils doit être identique au paramètre <code>Type</code> d'un appareil dans le détail des appareils. Ajouter le sur l'appareil grâce au bouton <code>+</code>. Ne pas supprimer de valeurs, seulement en ajouter de nouvelles.",
"NETWORK_DEVICE_TYPES_name": "Type d'appareil réseau",
"Navigation_About": "À propos",
"Navigation_AppEvents": "",
"Navigation_Devices": "Appareils",
"Navigation_Donations": "Dons",
"Navigation_Events": "Évènements",
@@ -716,4 +718,4 @@
"settings_update_item_warning": "Mettre à jour la valeur ci-dessous. Veillez à bien suivre le même format qu'auparavant. <b>Il n'y a pas de pas de contrôle.</b>",
"test_event_icon": "fa-vial-circle-check",
"test_event_tooltip": "Enregistrer d'abord vos modifications avant de tester vôtre paramétrage."
}
}

View File

@@ -8,6 +8,7 @@
"About_Design": "Progettato per:",
"About_Exit": "Esci",
"About_Title": "Scanner di sicurezza di rete e framework di notifica",
"AppEvents_AppEventProcessed": "",
"AppEvents_DateTimeCreated": "Loggato",
"AppEvents_Extra": "Extra",
"AppEvents_GUID": "GUID evento applicazione",
@@ -464,6 +465,7 @@
"NETWORK_DEVICE_TYPES_description": "Quali tipi di dispositivo possono essere utilizzati come dispositivi di rete nella vista Rete. Il tipo di dispositivo deve corrispondere esattamente all'impostazione <code>Tipo</code> su un dispositivo specifico nei Dettagli dispositivo. Aggiungilo sul Dispositivo tramite il pulsante <code>+</code>. Non rimuovere i tipi esistenti, aggiungine solo di nuovi.",
"NETWORK_DEVICE_TYPES_name": "Tipi di dispositivi di rete",
"Navigation_About": "Informazioni su",
"Navigation_AppEvents": "",
"Navigation_Devices": "Dispositivi",
"Navigation_Donations": "Donazioni",
"Navigation_Events": "Eventi",
@@ -716,4 +718,4 @@
"settings_update_item_warning": "Aggiorna il valore qui sotto. Fai attenzione a seguire il formato precedente. <b>La convalida non viene eseguita.</b>",
"test_event_icon": "fa-vial-circle-check",
"test_event_tooltip": "Salva le modifiche prima di provare le nuove impostazioni."
}
}

View File

@@ -8,6 +8,7 @@
"About_Design": "Designet for:",
"About_Exit": "Logg ut",
"About_Title": "Nettverkssikkerhetsskanner og varslingsrammeverk",
"AppEvents_AppEventProcessed": "",
"AppEvents_DateTimeCreated": "Logget",
"AppEvents_Extra": "Ekstra",
"AppEvents_GUID": "Applikasjon Hendelse GUID",
@@ -464,6 +465,7 @@
"NETWORK_DEVICE_TYPES_description": "Hvilke enhetstyper som tillates å brukes som nettverksenheter i nettverksvisningen. Enhetstypen må samsvare med nøyaktig <code>Type</code> Innstillingen på en bestemt enhet i enhetsdetaljer. Ikke fjern eksisterende typer, legg bare til nye.",
"NETWORK_DEVICE_TYPES_name": "Nettverksenhetstyper",
"Navigation_About": "Om",
"Navigation_AppEvents": "",
"Navigation_Devices": "Enheter",
"Navigation_Donations": "Donasjoner",
"Navigation_Events": "Hendelser",

View File

@@ -8,6 +8,7 @@
"About_Design": "Zaprojektowany dla:",
"About_Exit": "Wyloguj",
"About_Title": "Skaner bezpieczeństwa sieciowego i framwork powiadomień",
"AppEvents_AppEventProcessed": "",
"AppEvents_DateTimeCreated": "Zalogowany",
"AppEvents_Extra": "Ekstra",
"AppEvents_GUID": "Aplikacja GUID wydarzeń",
@@ -464,6 +465,7 @@
"NETWORK_DEVICE_TYPES_description": "Które typy urządzeń mają zezwolenie na bycie użytym jako urządzenia sieciowe w Widoku Sieci. Typ urządzenia musi dokładnie odpowiadać ustawieniu <code>Typ</code> na konkretnym urządzeniu w Szczegółach urządzenia. Nie usuwaj istniejących typów, tylko dodawaj nowe.",
"NETWORK_DEVICE_TYPES_name": "Typy urządzeń sieciowych",
"Navigation_About": "Informacje o",
"Navigation_AppEvents": "",
"Navigation_Devices": "Urządzenia",
"Navigation_Donations": "Dotacje",
"Navigation_Events": "Wydarzenia",
@@ -716,4 +718,4 @@
"settings_update_item_warning": "Zaktualizuj poniższą wartość. Zachowaj ostrożność i postępuj zgodnie z poprzednim formatem. <b>Walidacja nie jest wykonywana.</b>",
"test_event_icon": "fa-vial-circle-check",
"test_event_tooltip": "Zapisz zmiany zanim będziesz testować swoje ustawienia."
}
}

View File

@@ -8,6 +8,7 @@
"About_Design": "Desenvolvido por:",
"About_Exit": "Sair",
"About_Title": "Analisador de segurança de rede & framework de notificação",
"AppEvents_AppEventProcessed": "",
"AppEvents_DateTimeCreated": "Registrado em",
"AppEvents_Extra": "Adicional",
"AppEvents_GUID": "Evento de aplicação GUID",
@@ -464,6 +465,7 @@
"NETWORK_DEVICE_TYPES_description": "",
"NETWORK_DEVICE_TYPES_name": "",
"Navigation_About": "",
"Navigation_AppEvents": "",
"Navigation_Devices": "",
"Navigation_Donations": "",
"Navigation_Events": "",
@@ -716,4 +718,4 @@
"settings_update_item_warning": "",
"test_event_icon": "",
"test_event_tooltip": ""
}
}

View File

@@ -8,6 +8,7 @@
"About_Design": "Разработан:",
"About_Exit": "Зарегистрироваться",
"About_Title": "Сетевой сканер и система уведомлений",
"AppEvents_AppEventProcessed": "",
"AppEvents_DateTimeCreated": "Журнал",
"AppEvents_Extra": "Дополнительно",
"AppEvents_GUID": "GUID события приложения",
@@ -464,6 +465,7 @@
"NETWORK_DEVICE_TYPES_description": "Какие типы устройств разрешено использовать в качестве сетевых устройств в представлении Сеть. Тип устройства должен точно соответствовать настройке <code>Type</code> для конкретного устройства в сведениях об устройстве. Добавьте его на устройство с помощью кнопки <code>+</code>. Не удаляйте существующие типы, а только добавляйте новые.",
"NETWORK_DEVICE_TYPES_name": "Типы сетевых устройств",
"Navigation_About": "О NetAlertX",
"Navigation_AppEvents": "",
"Navigation_Devices": "Устройства",
"Navigation_Donations": "Пожертвования",
"Navigation_Events": "События",
@@ -716,4 +718,4 @@
"settings_update_item_warning": "Обновить значение ниже. Будьте осторожны, следуя предыдущему формату. <b>Проверка не выполняется.</b>",
"test_event_icon": "fa-vial-circle-check",
"test_event_tooltip": "Сначала сохраните изменения, прежде чем проверять настройки."
}
}

View File

@@ -8,6 +8,7 @@
"About_Design": "",
"About_Exit": "Oturum kapat",
"About_Title": "",
"AppEvents_AppEventProcessed": "",
"AppEvents_DateTimeCreated": "",
"AppEvents_Extra": "Ekstra",
"AppEvents_GUID": "",
@@ -464,6 +465,7 @@
"NETWORK_DEVICE_TYPES_description": "",
"NETWORK_DEVICE_TYPES_name": "",
"Navigation_About": "Hakkında",
"Navigation_AppEvents": "",
"Navigation_Devices": "Cihazlar",
"Navigation_Donations": "",
"Navigation_Events": "",

View File

@@ -8,6 +8,7 @@
"About_Design": "Призначений для:",
"About_Exit": "Вийти",
"About_Title": "Сканер безпеки мережі та структура сповіщень",
"AppEvents_AppEventProcessed": "",
"AppEvents_DateTimeCreated": "Зареєстровано",
"AppEvents_Extra": "Екстра",
"AppEvents_GUID": "GUID події програми",
@@ -464,6 +465,7 @@
"NETWORK_DEVICE_TYPES_description": "Які типи пристроїв дозволено використовувати як мережеві пристрої в поданні мережі. Тип пристрою має точно відповідати налаштуванню <code>Тип</code> на певному пристрої в Деталях пристрою. Додайте його на пристрій за допомогою кнопки <code>+</code>. Не видаляйте існуючі типи, лише додайте нові.",
"NETWORK_DEVICE_TYPES_name": "Типи мережевих пристроїв",
"Navigation_About": "про",
"Navigation_AppEvents": "",
"Navigation_Devices": "Пристрої",
"Navigation_Donations": "Пожертви",
"Navigation_Events": "Події",
@@ -716,4 +718,4 @@
"settings_update_item_warning": "Оновіть значення нижче. Слідкуйте за попереднім форматом. <b>Перевірка не виконана.</b>",
"test_event_icon": "fa-vial-circle- check",
"test_event_tooltip": "Перш ніж перевіряти налаштування, збережіть зміни."
}
}

View File

@@ -8,6 +8,7 @@
"About_Design": "设计用于:",
"About_Exit": "登出",
"About_Title": "网络安全扫描器和通知框架",
"AppEvents_AppEventProcessed": "",
"AppEvents_DateTimeCreated": "已记录",
"AppEvents_Extra": "额外的",
"AppEvents_GUID": "应用程序事件 GUID",
@@ -464,6 +465,7 @@
"NETWORK_DEVICE_TYPES_description": "哪些设备类型允许在网络视图中用作网络设备。设备类型必须与设备详细信息中特定设备上的 <code>Type</code> 设置完全匹配。请勿删除现有类型,仅添加新类型。",
"NETWORK_DEVICE_TYPES_name": "网络设备类型",
"Navigation_About": "关于",
"Navigation_AppEvents": "",
"Navigation_Devices": "设备",
"Navigation_Donations": "捐款",
"Navigation_Events": "事件",

View File

@@ -293,9 +293,6 @@ def create_sensor(mqtt_client, deviceId, deviceName, sensorType, sensorName, ico
# check previous configs
sensorConfig = sensor_config(deviceId, deviceName, sensorType, sensorName, icon, mac)
mylog('verbose', [f"[{pluginName}] Publishing sensor number {len(mqtt_sensors)}"])
# send if new
if sensorConfig.isNew:

View File

@@ -18,7 +18,7 @@ from const import pluginsPath, fullDbPath, logPath
from helper import timeNowTZ, get_setting_value
from notification import write_notification
from database import DB
from device import Device_obj
from models.device_instance import DeviceInstance
import conf
from pytz import timezone
@@ -53,8 +53,8 @@ def main():
# Initialize the Plugin obj output file
plugin_objects = Plugin_Objects(RESULT_FILE)
# Create a Device_obj instance
device_handler = Device_obj(db)
# Create a DeviceInstance instance
device_handler = DeviceInstance(db)
# Retrieve devices
unknown_devices = device_handler.getUnknown()

View File

@@ -139,6 +139,7 @@ def cleanup_database (dbPath, DAYS_TO_KEEP_EVENTS, HRS_TO_KEEP_NEWDEV, HRS_TO_KE
);"""
cursor.execute(delete_query)
conn.commit()
# -----------------------------------------------------

View File

@@ -23,7 +23,7 @@ from logger import mylog, Logger, append_line_to_file
from helper import timeNowTZ, get_setting_value
from const import logPath, applicationPath, fullDbPath
from database import DB
from device import Device_obj
from models.device_instance import DeviceInstance
import conf
from pytz import timezone
@@ -57,8 +57,8 @@ def main():
# Initialize the Plugin obj output file
plugin_objects = Plugin_Objects(RESULT_FILE)
# Create a Device_obj instance
device_handler = Device_obj(db)
# Create a DeviceInstance instance
device_handler = DeviceInstance(db)
# Retrieve devices
all_devices = device_handler.getAll()

View File

@@ -18,7 +18,7 @@ from const import pluginsPath, fullDbPath, logPath
from helper import timeNowTZ, get_setting_value
from notification import write_notification
from database import DB
from device import Device_obj
from models.device_instance import DeviceInstance
import conf
from pytz import timezone
@@ -53,8 +53,8 @@ def main():
# Initialize the Plugin obj output file
plugin_objects = Plugin_Objects(RESULT_FILE)
# Create a Device_obj instance
device_handler = Device_obj(db)
# Create a DeviceInstance instance
device_handler = DeviceInstance(db)
# Retrieve devices
unknown_devices = device_handler.getUnknown()

View File

@@ -24,7 +24,7 @@ from logger import mylog, Logger, append_line_to_file
from helper import timeNowTZ, get_setting_value, extract_between_strings, extract_ip_addresses, extract_mac_addresses
from const import logPath, applicationPath, fullDbPath
from database import DB
from device import Device_obj
from models.device_instance import DeviceInstance
import conf
from pytz import timezone

View File

@@ -23,7 +23,7 @@ from logger import mylog, Logger, append_line_to_file
from helper import timeNowTZ, get_setting_value
from const import logPath, applicationPath, fullDbPath
from database import DB
from device import Device_obj
from models.device_instance import DeviceInstance
import conf
from pytz import timezone
@@ -55,8 +55,8 @@ def main():
# Initialize the Plugin obj output file
plugin_objects = Plugin_Objects(RESULT_FILE)
# Create a Device_obj instance
device_handler = Device_obj(db)
# Create a DeviceInstance instance
device_handler = DeviceInstance(db)
# Retrieve devices
unknown_devices = device_handler.getUnknown()

View File

@@ -251,12 +251,12 @@ def main():
mylog("verbose", [f"[{pluginName}] starting execution"])
from database import DB
from device import Device_obj
from models.device_instance import DeviceInstance
db = DB() # instance of class DB
db.open()
# Create a Device_obj instance
device_handler = Device_obj(db)
# Create a DeviceInstance instance
device_handler = DeviceInstance(db)
# Retrieve configuration settings
# these should be self-explanatory
omada_sites = []

View File

@@ -19,7 +19,7 @@ from const import pluginsPath, fullDbPath, logPath
from helper import timeNowTZ, get_setting_value
from notification import write_notification
from database import DB
from device import Device_obj
from models.device_instance import DeviceInstance
import conf
# Make sure the TIMEZONE for logging is correct
@@ -54,8 +54,8 @@ def main():
db = DB() # instance of class DB
db.open()
# Create a Device_obj instance
device_handler = Device_obj(db)
# Create a DeviceInstance instance
device_handler = DeviceInstance(db)
# Retrieve devices
if 'offline' in devices_to_wake:

View File

@@ -10,7 +10,7 @@
<div class="content-wrapper">
<?php
require 'appEventsCore.php';
require 'workflowsCore.php';
?>

385
front/workflowsCore.php Executable file
View File

@@ -0,0 +1,385 @@
<?php
//------------------------------------------------------------------------------
// check if authenticated
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/security.php';
?>
<section class="content workflows">
<div id="workflowContainerWrap" class="bg-grey-dark color-palette col-sm-12 box-default box-info ">
<div id="workflowContainer"></div>
</div>
<div id="buttons" class="buttons col-sm-12">
<div class="add-workflow col-sm-6">
<button type="button" class="btn btn-primary btn-default pa-btn bg-green" id="save" onclick="addWorkflow()">
<?= lang('Gen_Add');?>
</button>
</div>
<div class="save-workflows col-sm-6">
<button type="button" class="btn btn-primary btn-default pa-btn bg-green" id="save" onclick="saveWorkflows()">
<?= lang('DevDetail_button_Save');?>
</button>
</div>
</div>
</section>
<script>
let workflows = [];
let fieldOptions = [
"devMac", "devName", "devOwner", "devType", "devVendor", "devFavorite",
"devGroup", "devComments", "devFirstConnection", "devLastConnection",
"devLastIP", "devStaticIP", "devScan", "devLogEvents", "devAlertEvents",
"devAlertDown", "devSkipRepeated", "devLastNotification", "devPresentLastScan",
"devIsNew", "devLocation", "devIsArchived", "devParentMAC", "devParentPort",
"devIcon", "devGUID", "devSite", "devSSID", "devSyncHubNode", "devSourcePlugin"
];
let triggerTypes = [
"Devices", "Plugins_Objects"
];
let operatorTypes = [
"equals", "contains" , "regex"
];
let actionTypes = [
"update_field", "run_plugin"
];
// --------------------------------------
// Retrieve and process the data
function getData() {
showSpinner();
getSetting()
$.get('php/server/query_json.php?file=workflows.json', function (res) {
workflows = res;
console.log(workflows);
renderWorkflows();
hideSpinner();
});
}
// --------------------------------------
// Render all workflows
function renderWorkflows() {
let $container = $("#workflowContainer");
$container.empty(); // Clear previous UI
$.each(workflows, function (index, wf) {
let $wfElement = generateWorkflowUI(wf, index);
$container.append($wfElement);
});
}
// --------------------------------------
// Generate UI for a single workflow
function generateWorkflowUI(wf, index) {
let $wfContainer = $("<div>", {
class: "workflow-card box box-solid box-primary panel panel-default",
id: `wf-${index}-container`
});
// Workflow Name
let $wfLinkWrap = $("<div>",
{
class: " ",
id: `wf-${index}-header`
}
)
let $wfHeaderLink = $("<a>",
{
"class": "",
"data-toggle": "collapse",
"data-parent": "#workflowContainer",
"aria-expanded": false,
"href" : `#wf-${index}-collapsible-panel`
}
)
let $wfHeaderHeading = $("<h4>",
{
class: "panel-title"
}
).text(wf.name)
$wfContainer.append($wfHeaderLink.append($wfLinkWrap.append($wfHeaderHeading)));
// Collapsible panel start
let $wfCollapsiblePanel = $("<div>", {
class: "panel-collapse collapse ",
id: `wf-${index}-collapsible-panel`
});
let $wfNameInput = createEditableInput("Workflow name", wf.name, `wf-name-${index}`, "workflow-name-input", function(newValue) {
console.log(`Saved new value: ${newValue}`);
wf.name = newValue; // Update the workflow object with the new name
});
$wfCollapsiblePanel.append($wfNameInput)
// Trigger Section with dropdowns
let $triggerSection = $("<div>",
{
class: "condition-list box box-secondary"
}
).append("<strong>Trigger:</strong> ");
let $triggerTypeDropdown = createEditableDropdown("Trigger Type", triggerTypes, wf.trigger.object_type, `trigger-${index}-type`, function(newValue) {
wf.trigger.object_type = newValue; // Update trigger's object_type
});
let $eventTypeDropdown = createEditableDropdown("Event Type", ["update", "create", "delete"], wf.trigger.event_type, `event-${index}-type`, function(newValue) {
wf.trigger.event_type = newValue; // Update trigger's event_type
});
$triggerSection.append($triggerTypeDropdown);
$triggerSection.append($eventTypeDropdown);
$wfCollapsiblePanel.append($triggerSection);
// Conditions
let $conditionsContainer = $("<div>").append("<strong>Conditions:</strong>");
$conditionsContainer.append(renderConditions(wf.conditions));
$wfCollapsiblePanel.append($conditionsContainer);
// Actions with action.field as dropdown
let $actionsContainer = $("<div>",
{
class: "actions-list box box-secondary"
}
).append("<strong>Actions:</strong>");
$.each(wf.actions, function (_, action) {
let $actionEl = $("<div>");
// Dropdown for action.field
let $fieldDropdown = createEditableDropdown("Field", fieldOptions, action.field, `action-${index}-field`, function(newValue) {
action.field = newValue; // Update action.field when a new value is selected
});
// Dropdown for action.type
let $actionDropdown= createEditableDropdown("Action", actionTypes, action.field, `action-${index}-type`, function(newValue) {
action.field = newValue; // Update action.field when a new value is selected
});
// Action Value Input (Editable)
let $actionValueInput = createEditableInput("Value", action.value, `action-${index}-value`, "action-value-input", function(newValue) {
action.value = newValue; // Update action.value when saved
});
$actionEl.append($actionDropdown);
$actionEl.append($fieldDropdown);
$actionEl.append($actionValueInput);
$actionsContainer.append($actionEl);
});
// add conditions group button
let $actionAddButton = $("<button>", {
class : "btn btn-secondary "
}).text("Add Action")
$actionsContainer.append($actionAddButton)
$wfCollapsiblePanel.append($actionsContainer);
$wfContainer.append($wfCollapsiblePanel)
return $wfContainer;
}
// --------------------------------------
// Render conditions recursively
function renderConditions(conditions) {
let $conditionList = $("<div>", {
class: "condition-list panel "
});
$.each(conditions, function (index, condition) {
if (condition.logic) {
let $nestedCondition = $("<div>",
{
class : "condition box box-secondary"
}
);
let $logicDropdown = createEditableDropdown("Logic Rules", ["AND", "OR"], condition.logic, `logic-${condition.field}`, function(newValue) {
condition.logic = newValue; // Update condition logic when a new value is selected
});
$nestedCondition.append($logicDropdown);
$conditionListNested = renderConditions(condition.conditions)
// add conditions group button
let $conditionAddButton = $("<button>", {
class : "btn btn-secondary "
}).text("Add Condition")
$conditionListNested.append($conditionAddButton);
$nestedCondition.append($conditionListNested); // Recursive call for nested conditions
$conditionList.append($nestedCondition);
} else {
let $conditionItem = $("<div>",
{
class: "panel"
});
// Create dropdown for condition field
let $fieldDropdown = createEditableDropdown("Field", fieldOptions, condition.field, `condition-${index}-field-${condition.field}`, function(newValue) {
condition.field = newValue; // Update condition field when a new value is selected
});
// Create dropdown for operator
let $operatorDropdown = createEditableDropdown("Operator", operatorTypes, condition.operator, `condition-${index}operator-${condition.field}`, function(newValue) {
condition.operator = newValue; // Update operator when a new value is selected
});
// Editable input for condition value
let $editableInput = createEditableInput("Condition Value", condition.value, `condition-${index}-value-${condition.field}`, "condition-value-input", function(newValue) {
condition.value = newValue; // Update condition value when saved
});
$conditionItem.append($fieldDropdown); // Append field dropdown
$conditionItem.append($operatorDropdown); // Append operator dropdown
$conditionItem.append($editableInput); // Append editable input for condition value
$conditionList.append($conditionItem);
}
});
// add conditions group button
let $conditionsGroupAddButton = $("<button>", {
class : "btn btn-secondary"
}).text("Add Condition Group")
$conditionList.append($conditionsGroupAddButton);
return $conditionList;
}
// --------------------------------------
// Render SELECT Dropdown with Predefined Values
function createEditableDropdown(labelText, options, selectedValue, id, onSave) {
let $wrapper = $("<div>", {
class: "form-group col-xs-12"
});
let $label = $("<label>", {
for: id,
class: "col-sm-4 col-xs-12 control-label "
}).text(labelText);
// Create select wrapper
let $selectWrapper = $("<div>", {
class: "col-sm-8 col-xs-12"
});
// Create select element
let $select = $("<select>", {
id: id,
class: "form-control col-sm-8 col-xs-12"
});
// Add options to the select dropdown
$.each(options, function (_, option) {
let $option = $("<option>", { value: option }).text(option);
if (option === selectedValue) {
$option.attr("selected", "selected"); // Set the default selection
}
$select.append($option);
});
// Trigger onSave when the selection changes
$select.on("change", function() {
let newValue = $select.val();
console.log(`Selected new value: ${newValue}`);
if (onSave && typeof onSave === "function") {
onSave(newValue); // Call onSave callback with the new value
}
});
$wrapper.append($label);
$wrapper.append($selectWrapper.append($select));
return $wrapper;
}
// --------------------------------------
// Render INPUT HTML element
function createEditableInput(labelText, value, id, className = "", onSave = null) {
// prepare wrapper
$wrapper = $("<div>", {
class: "form-group col-xs-12"
});
let $label = $("<label>", {
for: id,
class: "col-sm-4 col-xs-12 control-label "
}).text(labelText);
// Create input wrapper
let $inputWrapper = $("<div>", {
class: "col-sm-8 col-xs-12"
});
let $input = $("<input>", {
type: "text",
id: id,
value: value,
class: className + " col-sm-8 col-xs-12 form-control "
});
// Optional: Add a change event listener to update the workflow name
$input.on("change", function () {
let newValue = $input.val();
console.log(`Value changed to: ${newValue}`);
});
// Trigger onSave when the user presses Enter or the input loses focus
$input.on("blur keyup", function (e) {
if (e.type === "blur" || e.key === "Enter") {
if (onSave && typeof onSave === "function") {
onSave($input.val()); // Call the onSave callback with the new value
}
}
});
$wrapper.append($label)
$wrapper.append($inputWrapper.append($input))
return $wrapper;
}
// --------------------------------------
// Initialize
$(document).ready(function () {
getData();
});
</script>

View File

@@ -28,13 +28,14 @@ from logger import mylog
from helper import filePermissions, timeNowTZ, get_setting_value
from app_state import updateState
from api import update_api
from networkscan import process_scan
from scan.session_events import process_scan
from initialise import importConfigs
from database import DB
from reporting import get_notifications
from notification import Notification_obj
from plugin import run_plugin_scripts, check_and_run_user_event
from device import update_devices_names
from scan.device_handling import update_devices_names
from workflows.manager import WorkflowManager
#===============================================================================
#===============================================================================
@@ -79,6 +80,9 @@ def main ():
# Upgrade DB if needed
db.upgradeDB()
# Initialize the WorkflowManager
workflow_manager = WorkflowManager(db)
#===============================================================================
# This is the main loop of NetAlertX
#===============================================================================
@@ -180,15 +184,37 @@ def main ():
# Commit SQL
db.commitDB()
# Footer
mylog('verbose', ['[MAIN] Process: Idle'])
else:
# do something
# mylog('verbose', ['[MAIN] Waiting to start next loop'])
updateState("Process: Idle")
updateState("Process: Idle")
# WORKFLOWS handling
# ----------------------------------------
# Fetch new unprocessed events
new_events = workflow_manager.get_new_app_events()
# Process each new event and check triggers
if new_events:
updateState("Workflows: Start")
update_api_flag = False
for event in new_events:
mylog('debug', [f'[MAIN] Processing WORKFLOW app event with GUID {event["GUID"]}'])
# proceed to process events
workflow_manager.process_event(event)
if workflow_manager.update_api:
# Update API endpoints if needed
update_api_flag = True
if update_api_flag:
update_api(db, all_plugins, True)
updateState("Workflows: End")
#loop
time.sleep(5) # wait for N seconds

View File

@@ -71,7 +71,7 @@ sql_devices_all = """
FROM Devices
"""
sql_appevents = """select * from AppEvents"""
sql_appevents = """select * from AppEvents order by DateTimeCreated desc"""
# The below query calculates counts of devices in various categories:
# (connected/online, offline, down, new, archived),
# as well as a combined count for devices that match any status listed in the UI_MY_DEVICES setting

View File

@@ -3,6 +3,7 @@ from Crypto.Util.Padding import pad, unpad
import base64
import os
import hashlib
import uuid
# SIMPLE CRYPT - requeres C compiler -------------------------------------------------------------------------
@@ -56,4 +57,10 @@ def get_random_bytes(length):
# Format hexadecimal string with hyphens
formatted_hex = '-'.join(hex_string[i:i+2] for i in range(0, len(hex_string), 2))
return formatted_hex
return formatted_hex
#-------------------------------------------------------------------------------
def generate_deterministic_guid(plugin, primary_id, secondary_id):
"""Generates a deterministic GUID based on plugin, primary ID, and secondary ID."""
data = f"{plugin}-{primary_id}-{secondary_id}".encode("utf-8")
return str(uuid.UUID(hashlib.md5(data).hexdigest()))

View File

@@ -9,7 +9,7 @@ from const import fullDbPath, sql_devices_stats, sql_devices_all, sql_generateGu
from logger import mylog
from helper import json_obj, initOrSetParam, row_to_json, timeNowTZ
from appevent import AppEvent_obj
from workflows.app_events import AppEvent_obj
class DB():
"""
@@ -543,6 +543,7 @@ class DB():
sql_Plugins_Objects = """ CREATE TABLE IF NOT EXISTS Plugins_Objects(
"Index" INTEGER,
Plugin TEXT NOT NULL,
ObjectGUID TEXT,
Object_PrimaryID TEXT NOT NULL,
Object_SecondaryID TEXT NOT NULL,
DateTimeCreated TEXT NOT NULL,
@@ -589,6 +590,18 @@ class DB():
self.sql.execute('ALTER TABLE "Plugins_Objects" ADD COLUMN "HelpVal2" TEXT')
self.sql.execute('ALTER TABLE "Plugins_Objects" ADD COLUMN "HelpVal3" TEXT')
self.sql.execute('ALTER TABLE "Plugins_Objects" ADD COLUMN "HelpVal4" TEXT')
# plug_ObjectGUID_missing column
plug_ObjectGUID_missing = self.sql.execute ("""
SELECT COUNT(*) AS CNTREC FROM pragma_table_info('Plugins_Objects') WHERE name='ObjectGUID'
""").fetchone()[0] == 0
if plug_ObjectGUID_missing :
mylog('verbose', ["[upgradeDB] Adding ObjectGUID to the Plugins_Objects table"])
self.sql.execute("""
ALTER TABLE "Plugins_Objects" ADD "ObjectGUID" TEXT
""")
# -----------------------------------------
# REMOVE after 6/6/2025 - END
@@ -645,6 +658,17 @@ class DB():
self.sql.execute('ALTER TABLE "Plugins_Events" ADD COLUMN "HelpVal2" TEXT')
self.sql.execute('ALTER TABLE "Plugins_Events" ADD COLUMN "HelpVal3" TEXT')
self.sql.execute('ALTER TABLE "Plugins_Events" ADD COLUMN "HelpVal4" TEXT')
# plug_ObjectGUID_missing column
plug_ObjectGUID_missing = self.sql.execute ("""
SELECT COUNT(*) AS CNTREC FROM pragma_table_info('Plugins_Events') WHERE name='ObjectGUID'
""").fetchone()[0] == 0
if plug_ObjectGUID_missing :
mylog('verbose', ["[upgradeDB] Adding ObjectGUID to the Plugins_Events table"])
self.sql.execute("""
ALTER TABLE "Plugins_Events" ADD "ObjectGUID" TEXT
""")
# -----------------------------------------
# REMOVE after 6/6/2025 - END
@@ -703,6 +727,18 @@ class DB():
self.sql.execute('ALTER TABLE "Plugins_History" ADD COLUMN "HelpVal3" TEXT')
self.sql.execute('ALTER TABLE "Plugins_History" ADD COLUMN "HelpVal4" TEXT')
# plug_ObjectGUID_missing column
plug_ObjectGUID_missing = self.sql.execute ("""
SELECT COUNT(*) AS CNTREC FROM pragma_table_info('Plugins_History') WHERE name='ObjectGUID'
""").fetchone()[0] == 0
if plug_ObjectGUID_missing :
mylog('verbose', ["[upgradeDB] Adding ObjectGUID to the Plugins_History table"])
self.sql.execute("""
ALTER TABLE "Plugins_History" ADD "ObjectGUID" TEXT
""")
# -----------------------------------------
# REMOVE after 6/6/2025 - END
# -----------------------------------------

View File

@@ -1,31 +0,0 @@
import json
def update_value(json_data, object_path, key, value, target_property, desired_value):
# Helper function to traverse the JSON structure and get the target object
def traverse(obj, path):
keys = path.split(".")
for key in keys:
if isinstance(obj, list):
key = int(key)
obj = obj[key]
return obj
# Helper function to update the target property with the desired value
def update(obj, path, key, value, target_property, desired_value):
keys = path.split(".")
for i, key in enumerate(keys):
if isinstance(obj, list):
key = int(key)
# Check if we have reached the desired object
if i == len(keys) - 1 and obj[key][key] == value:
# Update the target property with the desired value
obj[key][target_property] = desired_value
else:
obj = obj[key]
return obj
# Get the target object based on the object path
target_obj = traverse(json_data, object_path)
# Update the value in the target object
updated_obj = update(json_data, object_path, key, value, target_property, desired_value)
return updated_obj

View File

@@ -0,0 +1,84 @@
import sys
# Register NetAlertX directories
INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/server"])
from logger import mylog, print_log
#-------------------------------------------------------------------------------
# Device object handling (WIP)
#-------------------------------------------------------------------------------
class DeviceInstance:
def __init__(self, db):
self.db = db
# Get all
def getAll(self):
self.db.sql.execute("""
SELECT * FROM Devices
""")
return self.db.sql.fetchall()
# Get all with unknown names
def getUnknown(self):
self.db.sql.execute("""
SELECT * FROM Devices WHERE devName in ("(unknown)", "(name not found)", "" )
""")
return self.db.sql.fetchall()
# Get specific column value based on devMac
def getValueWithMac(self, column_name, devMac):
query = f"SELECT {column_name} FROM Devices WHERE devMac = ?"
self.db.sql.execute(query, (devMac,))
result = self.db.sql.fetchone()
return result[column_name] if result else None
# Get all down
def getDown(self):
self.db.sql.execute("""
SELECT * FROM Devices WHERE devAlertDown = 1 and devPresentLastScan = 0
""")
return self.db.sql.fetchall()
# Get all down
def getOffline(self):
self.db.sql.execute("""
SELECT * FROM Devices WHERE devPresentLastScan = 0
""")
return self.db.sql.fetchall()
# Get a device by devGUID
def getByGUID(self, devGUID):
self.db.sql.execute("SELECT * FROM Devices WHERE devGUID = ?", (devGUID,))
result = self.db.sql.fetchone()
return dict(result) if result else None
# Check if a device exists by devGUID
def exists(self, devGUID):
self.db.sql.execute("SELECT COUNT(*) AS count FROM Devices WHERE devGUID = ?", (devGUID,))
result = self.db.sql.fetchone()
return result["count"] > 0
# Update a specific field for a device
def updateField(self, devGUID, field, value):
if not self.exists(devGUID):
m = f"[Device] In 'updateField': GUID {devGUID} not found."
mylog('none', m)
raise ValueError(m)
self.db.sql.execute(f"""
UPDATE Devices SET {field} = ? WHERE devGUID = ?
""", (value, devGUID))
self.db.sql.commit()
# Delete a device by devGUID
def delete(self, devGUID):
if not self.exists(devGUID):
m = f"[Device] In 'delete': GUID {devGUID} not found."
mylog('none', m)
raise ValueError(m)
self.db.sql.execute("DELETE FROM Devices WHERE devGUID = ?", (devGUID,))
self.db.sql.commit()

View File

@@ -0,0 +1,65 @@
import sys
# Register NetAlertX directories
INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/server"])
from logger import mylog, print_log
#-------------------------------------------------------------------------------
# Plugin object handling (WIP)
#-------------------------------------------------------------------------------
class PluginObjectInstance:
def __init__(self, db):
self.db = db
# Get all plugin objects
def getAll(self):
self.db.sql.execute("""
SELECT * FROM Plugins_Objects
""")
return self.db.sql.fetchall()
# Get plugin object by ObjectGUID
def getByGUID(self, ObjectGUID):
self.db.sql.execute("SELECT * FROM Plugins_Objects WHERE ObjectGUID = ?", (ObjectGUID,))
result = self.db.sql.fetchone()
return dict(result) if result else None
# Check if a plugin object exists by ObjectGUID
def exists(self, ObjectGUID):
self.db.sql.execute("SELECT COUNT(*) AS count FROM Plugins_Objects WHERE ObjectGUID = ?", (ObjectGUID,))
result = self.db.sql.fetchone()
return result["count"] > 0
# Get objects by plugin name
def getByPlugin(self, plugin):
self.db.sql.execute("SELECT * FROM Plugins_Objects WHERE Plugin = ?", (plugin,))
return self.db.sql.fetchall()
# Get objects by status
def getByStatus(self, status):
self.db.sql.execute("SELECT * FROM Plugins_Objects WHERE Status = ?", (status,))
return self.db.sql.fetchall()
# Update a specific field for a plugin object
def updateField(self, ObjectGUID, field, value):
if not self.exists(ObjectGUID):
m = f"[PluginObject] In 'updateField': GUID {ObjectGUID} not found."
mylog('none', m)
raise ValueError(m)
self.db.sql.execute(f"""
UPDATE Plugins_Objects SET {field} = ? WHERE ObjectGUID = ?
""", (value, ObjectGUID))
self.db.sql.commit()
# Delete a plugin object by ObjectGUID
def delete(self, ObjectGUID):
if not self.exists(ObjectGUID):
m = f"[PluginObject] In 'delete': GUID {ObjectGUID} not found."
mylog('none', m)
raise ValueError(m)
self.db.sql.execute("DELETE FROM Plugins_Objects WHERE ObjectGUID = ?", (ObjectGUID,))
self.db.sql.commit()

View File

@@ -18,6 +18,7 @@ from api import update_api
from plugin_utils import logEventStatusCounts, get_plugin_string, get_plugin_setting_obj, print_plugin_info, list_to_csv, combine_plugin_objects, resolve_wildcards_arr, handle_empty, custom_plugin_decoder, decode_and_rename_files
from notification import Notification_obj, write_notification
from user_events_queue import UserEventsQueue
from crypto_utils import generate_deterministic_guid
# Make sure log level is initialized correctly
Logger(get_setting_value('LOG_LEVEL'))
@@ -582,13 +583,14 @@ def process_plugin_events(db, plugin, plugEventsArr):
for plugObj in pluginObjects:
# keep old createdTime time if the plugObj already was created before
createdTime = plugObj.changed if plugObj.status == 'new' else plugObj.created
# 18 values without Index
# 19 values without Index
values = (
plugObj.pluginPref, plugObj.primaryId, plugObj.secondaryId, createdTime,
plugObj.changed, plugObj.watched1, plugObj.watched2, plugObj.watched3,
plugObj.watched4, plugObj.status, plugObj.extra, plugObj.userData,
plugObj.foreignKey, plugObj.syncHubNodeName,
plugObj.helpVal1, plugObj.helpVal2, plugObj.helpVal3, plugObj.helpVal4
plugObj.helpVal1, plugObj.helpVal2, plugObj.helpVal3, plugObj.helpVal4,
plugObj.objectGUID
)
if plugObj.status == 'new':
@@ -625,8 +627,8 @@ def process_plugin_events(db, plugin, plugEventsArr):
("Plugin", "Object_PrimaryID", "Object_SecondaryID", "DateTimeCreated",
"DateTimeChanged", "Watched_Value1", "Watched_Value2", "Watched_Value3",
"Watched_Value4", "Status", "Extra", "UserData", "ForeignKey", "SyncHubNodeName",
"HelpVal1", "HelpVal2", "HelpVal3", "HelpVal4")
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"HelpVal1", "HelpVal2", "HelpVal3", "HelpVal4", "ObjectGUID")
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", objects_to_insert
)
@@ -637,7 +639,8 @@ def process_plugin_events(db, plugin, plugEventsArr):
UPDATE Plugins_Objects
SET "Plugin" = ?, "Object_PrimaryID" = ?, "Object_SecondaryID" = ?, "DateTimeCreated" = ?,
"DateTimeChanged" = ?, "Watched_Value1" = ?, "Watched_Value2" = ?, "Watched_Value3" = ?,
"Watched_Value4" = ?, "Status" = ?, "Extra" = ?, "UserData" = ?, "ForeignKey" = ?, "SyncHubNodeName" = ?, "HelpVal1" = ?, "HelpVal2" = ?, "HelpVal3" = ?, "HelpVal4" = ?
"Watched_Value4" = ?, "Status" = ?, "Extra" = ?, "UserData" = ?, "ForeignKey" = ?, "SyncHubNodeName" = ?, "HelpVal1" = ?, "HelpVal2" = ?, "HelpVal3" = ?, "HelpVal4" = ?,
"ObjectGUID" = ?
WHERE "Index" = ?
""", objects_to_update
)
@@ -651,8 +654,9 @@ def process_plugin_events(db, plugin, plugEventsArr):
("Plugin", "Object_PrimaryID", "Object_SecondaryID", "DateTimeCreated",
"DateTimeChanged", "Watched_Value1", "Watched_Value2", "Watched_Value3",
"Watched_Value4", "Status", "Extra", "UserData", "ForeignKey", "SyncHubNodeName",
"HelpVal1", "HelpVal2", "HelpVal3", "HelpVal4")
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"HelpVal1", "HelpVal2", "HelpVal3", "HelpVal4",
"ObjectGUID")
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", events_to_insert
)
@@ -665,8 +669,9 @@ def process_plugin_events(db, plugin, plugEventsArr):
("Plugin", "Object_PrimaryID", "Object_SecondaryID", "DateTimeCreated",
"DateTimeChanged", "Watched_Value1", "Watched_Value2", "Watched_Value3",
"Watched_Value4", "Status", "Extra", "UserData", "ForeignKey", "SyncHubNodeName",
"HelpVal1", "HelpVal2", "HelpVal3", "HelpVal4")
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"HelpVal1", "HelpVal2", "HelpVal3", "HelpVal4",
"ObjectGUID")
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", history_to_insert
)
@@ -807,6 +812,7 @@ class plugin_object_class:
self.helpVal2 = objDbRow[16]
self.helpVal3 = objDbRow[17]
self.helpVal4 = objDbRow[18]
self.objectGUID = generate_deterministic_guid(self.pluginPref, self.primaryId, self.secondaryId)
# Check if self.status is valid

View File

@@ -6,7 +6,7 @@ from logger import mylog
from const import pluginsPath, logPath, apiPath
from helper import timeNowTZ, get_file_content, write_file, get_setting, get_setting_value, setting_value_to_python_type
from app_state import updateState
from crypto_utils import decrypt_data
from crypto_utils import decrypt_data, generate_deterministic_guid
module_name = 'Plugin utils'

View File

@@ -1,62 +1,17 @@
import sys
# Register NetAlertX directories
INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/server"])
import subprocess
import conf
import os
import re
from helper import timeNowTZ, get_setting, get_setting_value, list_to_where, resolve_device_name_dig, get_device_name_nbtlookup, get_device_name_nslookup, get_device_name_mdns, check_IP_format, sanitize_SQL_input
from logger import mylog, print_log
from const import vendorsPath, vendorsPathNewest, sql_generateGuid
#-------------------------------------------------------------------------------
# Device object handling (WIP)
#-------------------------------------------------------------------------------
class Device_obj:
def __init__(self, db):
self.db = db
# Get all
def getAll(self):
self.db.sql.execute("""
SELECT * FROM Devices
""")
return self.db.sql.fetchall()
# Get all with unknown names
def getUnknown(self):
self.db.sql.execute("""
SELECT * FROM Devices WHERE devName in ("(unknown)", "(name not found)", "" )
""")
return self.db.sql.fetchall()
# Get specific column value based on devMac
def getValueWithMac(self, column_name, devMac):
query = f"SELECT {column_name} FROM Devices WHERE devMac = ?"
self.db.sql.execute(query, (devMac,))
result = self.db.sql.fetchone()
return result[column_name] if result else None
# Get all down
def getDown(self):
self.db.sql.execute("""
SELECT * FROM Devices WHERE devAlertDown = 1 and devPresentLastScan = 0
""")
return self.db.sql.fetchall()
# Get all down
def getOffline(self):
self.db.sql.execute("""
SELECT * FROM Devices WHERE devPresentLastScan = 0
""")
return self.db.sql.fetchall()
from models.device_instance import DeviceInstance
#-------------------------------------------------------------------------------
# Removing devices from the CurrentScan DB table which the user chose to ignore by MAC or IP
@@ -535,7 +490,7 @@ def update_devices_names (db):
foundNbtLookup = 0
# Gen unknown devices
device_handler = Device_obj(db)
device_handler = DeviceInstance(db)
# Retrieve devices
unknownDevices = device_handler.getUnknown()

View File

@@ -1,15 +1,16 @@
import sys
# Register NetAlertX directories
INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/server"])
import conf
from device import create_new_devices, print_scan_stats, save_scanned_devices, update_devices_data_from_scan, exclude_ignored_devices
from scan.device_handling import create_new_devices, print_scan_stats, save_scanned_devices, update_devices_data_from_scan, exclude_ignored_devices
from helper import timeNowTZ
from logger import mylog
from reporting import skip_repeated_notifications
#===============================================================================
# SCAN NETWORK
#===============================================================================

90
server/workflows/actions.py Executable file
View 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

View File

@@ -1,13 +1,28 @@
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 logger import logResult, mylog, print_log
from helper import timeNowTZ
#-------------------------------------------------------------------------------
# Execution object handling
#-------------------------------------------------------------------------------
@@ -32,6 +47,7 @@ class AppEvent_obj:
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,
@@ -59,7 +75,9 @@ class AppEvent_obj:
sql_devices_mappedColumns = '''
"GUID",
"DateTimeCreated",
"AppEventProcessed",
"ObjectType",
"ObjectGUID",
"ObjectPrimaryID",
"ObjectSecondaryID",
"ObjectStatus",
@@ -81,7 +99,9 @@ class AppEvent_obj:
VALUES (
{sql_generateGuid},
DATETIME('now'),
FALSE,
'Devices',
NEW.devGUID,
NEW.devMac,
NEW.devLastIP,
CASE WHEN NEW.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END,
@@ -111,7 +131,9 @@ class AppEvent_obj:
VALUES (
{sql_generateGuid},
DATETIME('now'),
FALSE,
'Devices',
NEW.devGUID,
NEW.devMac,
NEW.devLastIP,
CASE WHEN NEW.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END,
@@ -135,7 +157,9 @@ class AppEvent_obj:
VALUES (
{sql_generateGuid},
DATETIME('now'),
FALSE,
'Devices',
OLD.devGUID,
OLD.devMac,
OLD.devLastIP,
CASE WHEN OLD.devPresentLastScan = 1 THEN 'online' ELSE 'offline' END,
@@ -155,7 +179,9 @@ class AppEvent_obj:
sql_plugins_objects_mappedColumns = '''
"GUID",
"DateTimeCreated",
"AppEventProcessed",
"ObjectType",
"ObjectGUID",
"ObjectPlugin",
"ObjectPrimaryID",
"ObjectSecondaryID",
@@ -176,8 +202,10 @@ class AppEvent_obj:
VALUES (
{sql_generateGuid},
DATETIME('now'),
'Plugins_Objects',
NEW.Plugin,
FALSE,
'Plugins_Objects',
NEW.ObjectGUID,
NEW.Plugin,
NEW.Object_PrimaryID,
NEW.Object_SecondaryID,
NEW.ForeignKey,
@@ -199,7 +227,9 @@ class AppEvent_obj:
VALUES (
{sql_generateGuid},
DATETIME('now'),
FALSE,
'Plugins_Objects',
NEW.ObjectGUID,
NEW.Plugin,
NEW.Object_PrimaryID,
NEW.Object_SecondaryID,
@@ -222,7 +252,9 @@ class AppEvent_obj:
VALUES (
{sql_generateGuid},
DATETIME('now'),
FALSE,
'Plugins_Objects',
OLD.ObjectGUID,
OLD.Plugin,
OLD.Object_PrimaryID,
OLD.Object_SecondaryID,

83
server/workflows/conditions.py Executable file
View 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
View 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
View 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

74
test/workflows.json Executable file
View File

@@ -0,0 +1,74 @@
[
{
"name": "Sample Device Update Workflow",
"trigger": {
"object_type": "Devices",
"event_type": "update"
},
"conditions": [
{
"logic": "AND",
"conditions": [
{
"field": "devVendor",
"operator": "contains",
"value": "Google"
},
{
"field": "devIsNew",
"operator": "equals",
"value": "1"
},
{
"logic": "OR",
"conditions": [
{
"field": "devIsNew",
"operator": "equals",
"value": "1"
},
{
"field": "devName",
"operator": "contains",
"value": "Google"
}
]
}
]
}
],
"actions": [
{
"type": "update_field",
"field": "devIsNew",
"value": "0"
}
]
},
{
"name": "Sample Plugin Object Workflow",
"trigger": {
"object_type": "Plugins_Objects",
"event_type": "create"
},
"conditions": [
{
"logic": "AND",
"conditions": [
{
"field": "Plugin",
"operator": "equals",
"value": "ARPSCAN"
},
{
"field": "Status",
"operator": "equals",
"value": "missing-in-last-scan"
}
]
}
],
"actions": [
]
}
]