Files
NetAlertX/search/search_index.json

1 line
553 KiB
JSON

{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"],"fields":{"title":{"boost":1000.0},"text":{"boost":1.0},"tags":{"boost":1000000.0}}},"docs":[{"location":"","title":"Home","text":"NetAlertX <p> Centralized network visibility and continuous asset discovery. </p> <p> NetAlertx delivers a scalable and secure solution for comprehensive network monitoring, supporting security awareness and operational efficiency. </p> Learn <p>Understand NetAlertX core features, discovery, and alerting concepts</p> Explore Features Install <p>Step-by-step installation guides for Docker, Home Assistant, Unraid, and bare-metal setups</p> View Installation Guides Notifications <p>Learn how NetAlertX provides device presence, alerting, and compliance-friendly monitoring</p> Explore Notifications Contribute <p>Source code, development environment setup, and contribution guidelines</p> Contribute on GitHub"},{"location":"#help-and-support","title":"Help and Support","text":"<p>If you need help or run into issues, here are some resources to guide you:</p> <p>Before opening an issue, please:</p> <ul> <li>Hover over settings, fields, or labels to see additional tooltips and guidance.</li> <li>Click ? (question-mark) icons next to various elements to view detailed information.</li> <li>Check common issues to see if your problem has already been reported.</li> <li>Look at closed issues for possible solutions to past problems.</li> <li>Enable debugging to gather more information: Debug Guide.</li> </ul> <p>Need more help? Join the community discussions or submit a support request:</p> <ul> <li>Visit the GitHub Discussions for community support.</li> <li>If you are experiencing issues that require immediate attention, consider opening an issue on our GitHub Issues page.</li> </ul>"},{"location":"#contributing","title":"Contributing","text":"<p>NetAlertX is open-source and welcomes contributions from the community! If you'd like to help improve the software, please follow the guidelines below:</p> <ul> <li>Fork the repository and make your changes.</li> <li>Submit a pull request with a detailed description of what you\u2019ve changed and why.</li> </ul> <p>For more information on contributing, check out our Dev Guide.</p>"},{"location":"#stay-updated","title":"Stay Updated","text":"<p>To keep up with the latest changes and updates to NetAlertX, please refer to the following resources:</p> <ul> <li>Releases</li> </ul> <p>Make sure to follow the project on GitHub to get notifications for new releases and important updates.</p>"},{"location":"#additional-info","title":"Additional info","text":"<ul> <li>Documentation Index: Check out the full documentation index for all the guides available.</li> </ul> <p>If you have any suggestions or improvements, please don\u2019t hesitate to contribute!</p> <p>NetAlertX is actively maintained. You can find the source code, report bugs, or request new features on our GitHub page.</p>"},{"location":"ADVISORY_EYES_ON_GLASS/","title":"Eyes on glass","text":""},{"location":"ADVISORY_EYES_ON_GLASS/#build-an-msp-wallboard-for-network-monitoring","title":"Build an MSP Wallboard for Network Monitoring","text":"<p>For Managed Service Providers (MSPs) and Network Operations Centers (NOC), \"Eyes on Glass\" monitoring requires a UI that is both self-healing (auto-refreshing) and focused only on critical data. By leveraging the UI Settings Plugin, you can transform NetAlertX from a management tool into a dedicated live monitor.</p> <p></p>"},{"location":"ADVISORY_EYES_ON_GLASS/#1-configure-auto-refresh-for-live-monitoring","title":"1. Configure Auto-Refresh for Live Monitoring","text":"<p>Static dashboards are the enemy of real-time response. NetAlertX allows you to force the UI to pull fresh data without manual page reloads.</p> <ul> <li>Setting: Locate the <code>UI_REFRESH</code> (or similar \"Auto-refresh UI\") setting within the UI Settings plugin.</li> <li>Optimal Interval: Set this between 60 to 120 seconds.</li> <li>Note: Refreshing too frequently (e.g., &lt;30s) on large networks can lead to high browser and server CPU usage.</li> </ul> <p></p>"},{"location":"ADVISORY_EYES_ON_GLASS/#2-streamlining-the-dashboard-msp-mode","title":"2. Streamlining the Dashboard (MSP Mode)","text":"<p>An MSP's focus is on what is broken, not what is working. Hide the noise to increase reaction speed.</p> <ul> <li>Hide Unnecessary Blocks: Under UI Settings, disable dashboard blocks that don't provide immediate utility, such as Online presence or Tiles.</li> <li>Hide virtual connections: You can specify which relationships shoudl be hidden from the main view to remove any virtual devices that are not essential from your views.</li> <li>Browser Full-Screen: Use the built-in \"Full Screen\" toggle in the top bar to remove browser chrome (URL bars/tabs) for a cleaner \"Wallboard\" look.</li> </ul>"},{"location":"ADVISORY_EYES_ON_GLASS/#3-creating-custom-noc-views","title":"3. Creating Custom NOC Views","text":"<p>Use the UI Filters in tandem with UI Settings to create custom views.</p> <p></p> Feature NOC/MSP Application Site-Specific Nodes Filter the view by a specific \"Sync Node\" or \"Location\" filter to monitor a single client site. Filter by Criticality Filter devices where <code>Group == \"Infrastructure\"</code> or <code>\"Server\"</code>. (depending on your predefined values) Predefined \"Down\" View Bookmark the URL with the <code>/devices.php#down</code> path to ensure the dashboard always loads into an \"Alert Only\" mode."},{"location":"ADVISORY_EYES_ON_GLASS/#4-browser-cache-stability","title":"4. Browser &amp; Cache Stability","text":"<p>Because the UI is a web application, long-running sessions can occasionally experience cache drift.</p> <ul> <li>Cache Refresh: If you notice the \"Show # Entries\" resetting or icons failing to load after days of uptime, use the Reload icon in the application header (not the browser refresh) to clear the internal app cache.</li> <li>Dedicated Hardware: For 24/7 monitoring, use a dedicated thin client or Raspberry Pi running in \"Kiosk Mode\" to prevent OS-level popups from obscuring the dashboard.</li> </ul> <p>Tip</p> <p>NetAlertX - Detailed Dashboard Guide This video provides a visual walkthrough of the NetAlertX dashboard features, including how to map and visualize devices which is crucial for setting up a clear \"Eyes on Glass\" monitoring environment.</p>"},{"location":"ADVISORY_EYES_ON_GLASS/#summary-checklist","title":"Summary Checklist","text":"<ul> <li> Automate Refresh: Set <code>UI_REFRESH</code> to 60-120s in UI Settings to ensure the dashboard stays current without manual intervention.</li> <li> Filter for Criticality: Bookmark the <code>/devices.php#down</code> view to instantly focus on offline assets rather than the entire inventory.</li> <li> Remove UI Noise: Use UI Settings to hide non-essential dashboard blocks (e.g., Tiles or remove Virtual Connections devices) to maximize screen real estate for alerts.</li> <li> Segment by Site: Use Location or Sync Node filters to create dedicated views for specific client networks or physical branches.</li> <li> Ensure Stability: Run on a dedicated \"Kiosk\" browser and use the internal Reload icon occasionally to maintain a clean application cache.</li> </ul>"},{"location":"ADVISORY_MULTI_NETWORK/","title":"Multi-network monitoring","text":""},{"location":"ADVISORY_MULTI_NETWORK/#advisory-best-practices-for-monitoring-multiple-networks-with-netalertx","title":"ADVISORY: Best Practices for Monitoring Multiple Networks with NetAlertX","text":""},{"location":"ADVISORY_MULTI_NETWORK/#1-define-monitoring-scope-architecture","title":"1. Define Monitoring Scope &amp; Architecture","text":"<p>Effective multi-network monitoring starts with understanding how NetAlertX \"sees\" your traffic.</p> <ul> <li>A. Understand Network Accessibility: Local ARP-based scanning (ARPSCAN) only discovers devices on directly accessible subnets due to Layer 2 limitations. It cannot traverse VPNs or routed borders without specific configuration.</li> <li>B. Plan Subnet &amp; Scan Interfaces: Explicitly configure each accessible segment in <code>SCAN_SUBNETS</code> with the corresponding interfaces.</li> <li>C. Remote &amp; Inaccessible Networks: For networks unreachable via ARP, use these strategies:</li> <li>Alternate Plugins: Supplement discovery with SNMPDSC or DHCP lease imports.</li> <li>Centralized Multi-Tenant Management using Sync Nodes: Run secondary NetAlertX instances on isolated networks and aggregate data using the SYNC plugin.</li> <li>Manual Entry: For static assets where only ICMP (ping) status is needed.</li> </ul> <p>Tip</p> <p>Explore the remote networks documentation for more details on how to set up the approaches menationed above.</p>"},{"location":"ADVISORY_MULTI_NETWORK/#2-automating-it-asset-inventory-with-workflows","title":"2. Automating IT Asset Inventory with Workflows","text":"<p>Workflows are the \"engine\" of NetAlertX, reducing manual overhead as your device list grows.</p> <ul> <li>A. Logical Ownership &amp; VLAN Tagging: Create a workflow triggered on Device Creation to:</li> <li>Inspect the IP/Subnet.</li> <li> <p>Set <code>devVlan</code> or <code>devOwner</code> custom fields automatically.</p> </li> <li> <p>B. Auto-Grouping: Use conditional logic to categorize devices.</p> </li> <li>Example: If <code>devLastIP == 10.10.20.*</code>, then <code>Set devLocation = \"BranchOffice\"</code>.</li> </ul> <pre><code>{\n \"name\": \"Assign Location - BranchOffice\",\n \"trigger\": {\n \"object_type\": \"Devices\",\n \"event_type\": \"update\"\n },\n \"conditions\": [\n {\n \"logic\": \"AND\",\n \"conditions\": [\n {\n \"field\": \"devLastIP\",\n \"operator\": \"contains\",\n \"value\": \"10.10.20.\"\n }\n ]\n }\n ],\n \"actions\": [\n {\n \"type\": \"update_field\",\n \"field\": \"devLocation\",\n \"value\": \"BranchOffice\"\n }\n ]\n}\n</code></pre> <ul> <li>C. Sync Node Tracking: When using multiple instances, ensure all synchub nodes have a descriptive <code>SYNC_node_name</code> name to distinguish between sites.</li> </ul> <p>Tip</p> <p>Always test new workflows in a \"Staging\" instance. A misconfigured workflow can trigger thousands of unintended updates across your database.</p>"},{"location":"ADVISORY_MULTI_NETWORK/#3-notification-strategy-low-noise-high-signal","title":"3. Notification Strategy: Low Noise, High Signal","text":"<p>A multi-network environment can generate significant \"alert fatigue.\" Use a layered filtering approach.</p> Level Strategy Recommended Action Device Silence Flapping Use \"Skip repeated notifications\" for unstable IoT devices. Plugin Tune Watchers Only enable <code>_WATCH</code> on reliable plugins (e.g., ICMP/SNMP). Global Filter Sections Limit <code>NTFPRCS_INCLUDED_SECTIONS</code> to <code>new_devices</code> and <code>down_devices</code>. <p>Tip</p> <p>Ignore Rules: Maintain strict Ignored MAC (<code>NEWDEV_ignored_MACs</code>) and Ignored IP (<code>NEWDEV_ignored_IPs</code>) lists for guest networks or broadcast scanners to keep your logs clean.</p>"},{"location":"ADVISORY_MULTI_NETWORK/#4-ui-filters-for-multi-network-clarity","title":"4. UI Filters for Multi-Network Clarity","text":"<p>Don't let a massive device list overwhelm you. Use the Multi-edit features to categorize devices and create focused views:</p> <ul> <li>By Zone: Filter by \"Location\", \"Site\" or \"Sync Node\" you et up in Section 2.</li> <li>By Criticality: Use custom the device Type field to separate \"Core Infrastructure\" from \"Ephemeral Clients.\"</li> <li>By Status: Use predefined views specifically for \"Devices currently Down\" to act as a Network Operations Center (NOC) dashboard.</li> </ul> <p>Tip</p> <p>If you are providing services as a Managed Service Provider (MSP) customize your default UI to be exactly how you need it, by hiding parts of the UI that you are not interested in, or by configuring a auto-refreshed screen monitoring your most important clients. See the Eyes on glass advisory for more details.</p>"},{"location":"ADVISORY_MULTI_NETWORK/#5-operational-stability-sync-health","title":"5. Operational Stability &amp; Sync Health","text":"<ul> <li>Health Checks: Regularly monitor the Logs to ensure remote nodes are reporting in.</li> <li>Backups: Use the CSV Devices Backup plugin. Standardize your workflow templates and back up you <code>/config</code> folders so that if a node fails, you can redeploy it with the same logic instantly.</li> </ul>"},{"location":"ADVISORY_MULTI_NETWORK/#6-optimize-performance","title":"6. Optimize Performance","text":"<p>As your environment grows, tuning the underlying engine is vital to maintain a snappy UI and reliable discovery cycles.</p> <ul> <li>Plugin Scheduling: Avoid \"Scan Storms\" by staggering plugin execution. Running intensive tasks like <code>NMAP</code> or <code>MASS_DNS</code> simultaneously can spike CPU and cause database locks.</li> <li>Database Health: Large-scale monitoring generates massive event logs. Use the DBCLNP (Database Cleanup) plugin to prune old records and keep the SQLite database performant.</li> <li>Resource Management: For high-device counts, consider increasing the memory limit for the container and utilizing <code>tmpfs</code> for temporary files to reduce SD card/disk I/O bottlenecks.</li> </ul> <p>Important</p> <p>For a deep dive into hardware requirements, database vacuuming, and specific environment variables for high-load instances, refer to the full Performance Optimization Guide.</p>"},{"location":"ADVISORY_MULTI_NETWORK/#summary-checklist","title":"Summary Checklist","text":"<ul> <li> Discovery: Are all subnets explicitly defined?</li> <li> Automation: Do new devices get auto-assigned to a VLAN/Owner?</li> <li> Noise Control: Are transient \"Down\" alerts delayed via <code>NTFPRCS_alert_down_time</code>?</li> <li> Remote Sites: Is the SYNC plugin authenticated and heartbeat-active?</li> </ul>"},{"location":"API/","title":"API Documentation","text":"<p>This API provides programmatic access to devices, events, sessions, metrics, network tools, and sync in NetAlertX. It is implemented as a REST and GraphQL server. All requests require authentication via API Token (<code>API_TOKEN</code> setting) unless explicitly noted. For example, to authorize a GraphQL request, you need to use a <code>Authorization: Bearer API_TOKEN</code> header as per example below:</p> <pre><code>curl 'http://host:GRAPHQL_PORT/graphql' \\\n -X POST \\\n -H 'Authorization: Bearer API_TOKEN' \\\n -H 'Content-Type: application/json' \\\n --data '{\n \"query\": \"query GetDevices($options: PageQueryOptionsInput) { devices(options: $options) { devices { rowid devMac devName devOwner devType devVendor devLastConnection devStatus } count } }\",\n \"variables\": {\n \"options\": {\n \"page\": 1,\n \"limit\": 10,\n \"sort\": [{ \"field\": \"devName\", \"order\": \"asc\" }],\n \"search\": \"\",\n \"status\": \"connected\"\n }\n }\n }'\n</code></pre> <p>The API server runs on <code>0.0.0.0:&lt;graphql_port&gt;</code> with CORS enabled for all main endpoints.</p> <p>CORS configuration: You can limit allowed CORS origins with the <code>CORS_ORIGINS</code> environment variable. Set it to a comma-separated list of origins (for example: <code>CORS_ORIGINS=\"https://example.com,http://localhost:3000\"</code>). The server parses this list at startup and only allows origins that begin with <code>http://</code> or <code>https://</code>. If <code>CORS_ORIGINS</code> is unset or parses to an empty list, the API falls back to a safe development default list (localhosts) and will include <code>*</code> as a last-resort permissive origin.</p>"},{"location":"API/#authentication","title":"Authentication","text":"<p>All endpoints require an API token provided in the HTTP headers:</p> <pre><code>Authorization: Bearer &lt;API_TOKEN&gt;\n</code></pre> <p>If the token is missing or invalid, the server will return:</p> <pre><code>{\n \"success\": false,\n \"message\": \"ERROR: Not authorized\",\n \"error\": \"Forbidden\"\n}\n</code></pre> <p>HTTP Status: 403 Forbidden</p>"},{"location":"API/#base-url","title":"Base URL","text":"<pre><code>http://&lt;server&gt;:&lt;GRAPHQL_PORT&gt;/\n</code></pre>"},{"location":"API/#endpoints","title":"Endpoints","text":"<p>Note</p> <p>You can explore the API endpoints by using the interactive API docs at <code>http://&lt;server&gt;:&lt;GRAPHQL_PORT&gt;/docs</code>. </p> <p>Tip</p> <p>When retrieving devices or settings try using the GraphQL API endpoint first as it is read-optimized.</p>"},{"location":"API/#standard-rest-endpoints","title":"Standard REST Endpoints","text":"<ul> <li>Device API Endpoints \u2013 Manage individual devices</li> <li>Devices Collection \u2013 Bulk operations on multiple devices</li> <li>Events \u2013 Device event logging and management</li> <li>Sessions \u2013 Connection sessions and history</li> <li>Settings \u2013 Settings</li> <li>Messaging:<ul> <li>In app messaging - In-app messaging</li> </ul> </li> <li>Metrics \u2013 Prometheus metrics and per-device status</li> <li>Network Tools \u2013 Utilities like Wake-on-LAN, traceroute, nslookup, nmap, and internet info</li> <li>Online History \u2013 Online/offline device records</li> <li>GraphQL \u2013 Advanced queries and filtering for Devices, Settings and Language Strings</li> <li>Sync \u2013 Synchronization between multiple NetAlertX instances</li> <li>Logs \u2013 Purging of logs and adding to the event execution queue for user triggered events</li> <li>DB query (\u26a0 Internal) - Low level database access - use other endpoints if possible</li> <li><code>/server</code> (\u26a0 Internal) - Backend server endpoint for internal communication only - do not use directly</li> </ul>"},{"location":"API/#mcp-server-bridge","title":"MCP Server Bridge","text":"<p>NetAlertX includes an MCP (Model Context Protocol) Server Bridge that provides AI assistants access to NetAlertX functionality through standardized tools. MCP endpoints are available at <code>/mcp/sse/*</code> paths and mirror the functionality of standard REST endpoints:</p> <ul> <li><code>/mcp/sse</code> - Server-Sent Events endpoint for MCP client connections</li> <li><code>/mcp/sse/openapi.json</code> - OpenAPI specification for available MCP tools</li> <li><code>/mcp/sse/device/*</code>, <code>/mcp/sse/devices/*</code>, <code>/mcp/sse/nettools/*</code>, <code>/mcp/sse/events/*</code> - MCP-enabled versions of REST endpoints</li> </ul> <p>MCP endpoints require the same Bearer token authentication as REST endpoints.</p> <p>\ud83d\udcd6 See MCP Server Bridge API for complete documentation, tool specifications, and integration examples.</p> <p>See Testing for example requests and usage.</p>"},{"location":"API/#notes","title":"Notes","text":"<ul> <li>All endpoints enforce Bearer token authentication.</li> <li>Errors return JSON with <code>success: False</code> and an error message.</li> <li>GraphQL is available for advanced queries, while REST endpoints cover structured use cases.</li> <li>Endpoints run on <code>0.0.0.0:&lt;GRAPHQL_PORT&gt;</code> with CORS enabled.</li> <li>Use consistent API tokens and node/plugin names when interacting with <code>/sync</code> to ensure data integrity.</li> </ul>"},{"location":"API_DBQUERY/","title":"Database Query API","text":"<p>The Database Query API provides direct, low-level access to the NetAlertX database. It allows read, write, update, and delete operations against tables, using base64-encoded SQL or structured parameters.</p> <p>Warning</p> <p>This API is primarily used internally to generate and render the application UI. These endpoints are low-level and powerful, and should be used with caution. Wherever possible, prefer the standard API endpoints. Invalid or unsafe queries can corrupt data. If you need data in a specific format that is not already provided, please open an issue or pull request with a clear, broadly useful use case. This helps ensure new endpoints benefit the wider community rather than relying on raw database queries.</p>"},{"location":"API_DBQUERY/#authentication","title":"Authentication","text":"<p>All <code>/dbquery/*</code> endpoints require an API token in the HTTP headers:</p> <pre><code>Authorization: Bearer &lt;API_TOKEN&gt;\n</code></pre> <p>If the token is missing or invalid (HTTP 403):</p> <pre><code>{\n \"success\": false,\n \"message\": \"ERROR: Not authorized\",\n \"error\": \"Forbidden\"\n}\n</code></pre>"},{"location":"API_DBQUERY/#endpoints","title":"Endpoints","text":""},{"location":"API_DBQUERY/#1-post-dbqueryread","title":"1. <code>POST /dbquery/read</code>","text":"<p>Execute a read-only SQL query (e.g., <code>SELECT</code>).</p>"},{"location":"API_DBQUERY/#request-body","title":"Request Body","text":"<pre><code>{\n \"rawSql\": \"U0VMRUNUICogRlJPTSBERVZJQ0VT\" // base64 encoded SQL\n}\n</code></pre> <p>Decoded SQL:</p> <pre><code>SELECT * FROM Devices;\n</code></pre>"},{"location":"API_DBQUERY/#response","title":"Response","text":"<pre><code>{\n \"success\": true,\n \"results\": [\n { \"devMac\": \"AA:BB:CC:DD:EE:FF\", \"devName\": \"Phone\" }\n ]\n}\n</code></pre>"},{"location":"API_DBQUERY/#curl-example","title":"<code>curl</code> Example","text":"<pre><code>curl -X POST \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/dbquery/read\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Accept: application/json\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"rawSql\": \"U0VMRUNUICogRlJPTSBERVZJQ0VT\"\n }'\n</code></pre>"},{"location":"API_DBQUERY/#2-post-dbqueryupdate-safer-than-dbquerywrite","title":"2. <code>POST /dbquery/update</code> (safer than <code>/dbquery/write</code>)","text":"<p>Update rows in a table by <code>columnName</code> + <code>id</code>. <code>/dbquery/update</code> is parameterized to reduce the risk of SQL injection, while <code>/dbquery/write</code> executes raw SQL directly.</p>"},{"location":"API_DBQUERY/#request-body_1","title":"Request Body","text":"<pre><code>{\n \"columnName\": \"devMac\",\n \"id\": [\"AA:BB:CC:DD:EE:FF\"],\n \"dbtable\": \"Devices\",\n \"columns\": [\"devName\", \"devOwner\"],\n \"values\": [\"Laptop\", \"Alice\"]\n}\n</code></pre>"},{"location":"API_DBQUERY/#response_1","title":"Response","text":"<pre><code>{ \"success\": true, \"updated_count\": 1 }\n</code></pre>"},{"location":"API_DBQUERY/#curl-example_1","title":"<code>curl</code> Example","text":"<pre><code>curl -X POST \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/dbquery/update\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Accept: application/json\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"columnName\": \"devMac\",\n \"id\": [\"AA:BB:CC:DD:EE:FF\"],\n \"dbtable\": \"Devices\",\n \"columns\": [\"devName\", \"devOwner\"],\n \"values\": [\"Laptop\", \"Alice\"]\n }'\n</code></pre>"},{"location":"API_DBQUERY/#3-post-dbquerywrite","title":"3. <code>POST /dbquery/write</code>","text":"<p>Execute a write query (<code>INSERT</code>, <code>UPDATE</code>, <code>DELETE</code>).</p>"},{"location":"API_DBQUERY/#request-body_2","title":"Request Body","text":"<pre><code>{\n \"rawSql\": \"SU5TRVJUIElOVE8gRGV2aWNlcyAoZGV2TWFjLCBkZXYgTmFtZSwgZGV2Rmlyc3RDb25uZWN0aW9uLCBkZXZMYXN0Q29ubmVjdGlvbiwgZGV2TGFzdElQKSBWQUxVRVMgKCc2QTpCQjo0Qzo1RDo2RTonLCAnVGVzdERldmljZScsICcyMDI1LTA4LTMwIDEyOjAwOjAwJywgJzIwMjUtMDgtMzAgMTI6MDA6MDAnLCAnMTAuMC4wLjEwJyk=\"\n}\n</code></pre> <p>Decoded SQL:</p> <pre><code>INSERT INTO Devices (devMac, devName, devFirstConnection, devLastConnection, devLastIP)\nVALUES ('6A:BB:4C:5D:6E', 'TestDevice', '2025-08-30 12:00:00', '2025-08-30 12:00:00', '10.0.0.10');\n</code></pre>"},{"location":"API_DBQUERY/#response_2","title":"Response","text":"<pre><code>{ \"success\": true, \"affected_rows\": 1 }\n</code></pre>"},{"location":"API_DBQUERY/#curl-example_2","title":"<code>curl</code> Example","text":"<pre><code>curl -X POST \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/dbquery/write\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Accept: application/json\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"rawSql\": \"SU5TRVJUIElOVE8gRGV2aWNlcyAoZGV2TWFjLCBkZXYgTmFtZSwgZGV2Rmlyc3RDb25uZWN0aW9uLCBkZXZMYXN0Q29ubmVjdGlvbiwgZGV2TGFzdElQKSBWQUxVRVMgKCc2QTpCQjo0Qzo1RDo2RTonLCAnVGVzdERldmljZScsICcyMDI1LTA4LTMwIDEyOjAwOjAwJywgJzIwMjUtMDgtMzAgMTI6MDA6MDAnLCAnMTAuMC4wLjEwJyk=\"\n }'\n</code></pre>"},{"location":"API_DBQUERY/#4-post-dbquerydelete","title":"4. <code>POST /dbquery/delete</code>","text":"<p>Delete rows in a table by <code>columnName</code> + <code>id</code>.</p>"},{"location":"API_DBQUERY/#request-body_3","title":"Request Body","text":"<pre><code>{\n \"columnName\": \"devMac\",\n \"id\": [\"AA:BB:CC:DD:EE:FF\"],\n \"dbtable\": \"Devices\"\n}\n</code></pre>"},{"location":"API_DBQUERY/#response_3","title":"Response","text":"<pre><code>{ \"success\": true, \"deleted_count\": 1 }\n</code></pre>"},{"location":"API_DBQUERY/#curl-example_3","title":"<code>curl</code> Example","text":"<pre><code>curl -X POST \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/dbquery/delete\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Accept: application/json\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"columnName\": \"devMac\",\n \"id\": [\"AA:BB:CC:DD:EE:FF\"],\n \"dbtable\": \"Devices\"\n }'\n</code></pre>"},{"location":"API_DEVICE/","title":"Device API Endpoints","text":"<p>Manage a single device by its MAC address. Operations include retrieval, updates, deletion, resetting properties, and copying data between devices. All endpoints require authorization via Bearer token.</p>"},{"location":"API_DEVICE/#1-retrieve-device-details","title":"1. Retrieve Device Details","text":"<ul> <li> <p>GET <code>/device/&lt;mac&gt;</code> Fetch all details for a single device, including:</p> </li> <li> <p>Computed status (<code>devStatus</code>) \u2192 <code>On-line</code>, <code>Off-line</code>, or <code>Down</code></p> </li> <li>Session and event counts (<code>devSessions</code>, <code>devEvents</code>, <code>devDownAlerts</code>)</li> <li>Presence hours (<code>devPresenceHours</code>)</li> <li>Children devices (<code>devChildrenDynamic</code>) and NIC children (<code>devChildrenNicsDynamic</code>)</li> </ul> <p>Special case: <code>mac=new</code> returns a template for a new device with default values.</p> <p>Response (success):</p> <pre><code>{\n \"devMac\": \"AA:BB:CC:DD:EE:FF\",\n \"devName\": \"Net - Huawei\",\n \"devOwner\": \"Admin\",\n \"devType\": \"Router\",\n \"devVendor\": \"Huawei\",\n \"devStatus\": \"On-line\",\n \"devSessions\": 12,\n \"devEvents\": 5,\n \"devDownAlerts\": 1,\n \"devPresenceHours\": 32,\n \"devChildrenDynamic\": [...],\n \"devChildrenNicsDynamic\": [...],\n ...\n}\n</code></pre> <p>Error Responses:</p> <ul> <li>Device not found \u2192 HTTP 404</li> <li>Unauthorized \u2192 HTTP 403</li> </ul> <p>MCP Integration: Available as <code>get_device_info</code> and <code>set_device_alias</code> tools. See MCP Server Bridge API.</p>"},{"location":"API_DEVICE/#2-update-device-fields","title":"2. Update Device Fields","text":"<ul> <li>POST <code>/device/&lt;mac&gt;</code> Create or update a device record.</li> </ul> <p>Request Body:</p> <pre><code>{\n \"devName\": \"New Device\",\n \"devOwner\": \"Admin\",\n \"createNew\": true\n}\n</code></pre> <p>Behavior:</p> <ul> <li>If <code>createNew=true</code> \u2192 creates a new device</li> <li>Otherwise \u2192 updates existing device fields</li> </ul> <p>Response:</p> <pre><code>{\n \"success\": true\n}\n</code></pre> <p>Error Responses:</p> <ul> <li>Unauthorized \u2192 HTTP 403</li> </ul>"},{"location":"API_DEVICE/#3-delete-a-device","title":"3. Delete a Device","text":"<ul> <li>DELETE <code>/device/&lt;mac&gt;/delete</code> Deletes the device with the given MAC.</li> </ul> <p>Response:</p> <pre><code>{\n \"success\": true\n}\n</code></pre> <p>Error Responses:</p> <ul> <li>Unauthorized \u2192 HTTP 403</li> </ul>"},{"location":"API_DEVICE/#4-delete-all-events-for-a-device","title":"4. Delete All Events for a Device","text":"<ul> <li>DELETE <code>/device/&lt;mac&gt;/events/delete</code> Removes all events associated with a device.</li> </ul> <p>Response:</p> <pre><code>{\n \"success\": true\n}\n</code></pre>"},{"location":"API_DEVICE/#5-reset-device-properties","title":"5. Reset Device Properties","text":"<ul> <li>POST <code>/device/&lt;mac&gt;/reset-props</code> Resets the device's custom properties to default values.</li> </ul> <p>Request Body: Optional JSON for additional parameters.</p> <p>Response:</p> <pre><code>{\n \"success\": true\n}\n</code></pre>"},{"location":"API_DEVICE/#6-copy-device-data","title":"6. Copy Device Data","text":"<ul> <li>POST <code>/device/copy</code> Copy all data from one device to another. If a device exists with <code>macTo</code>, it is replaced.</li> </ul> <p>Request Body:</p> <pre><code>{\n \"macFrom\": \"AA:BB:CC:DD:EE:FF\",\n \"macTo\": \"11:22:33:44:55:66\"\n}\n</code></pre> <p>Response:</p> <pre><code>{\n \"success\": true,\n \"message\": \"Device copied from AA:BB:CC:DD:EE:FF to 11:22:33:44:55:66\"\n}\n</code></pre> <p>Error Responses:</p> <ul> <li>Missing <code>macFrom</code> or <code>macTo</code> \u2192 HTTP 400</li> <li>Unauthorized \u2192 HTTP 403</li> </ul>"},{"location":"API_DEVICE/#7-update-a-single-column","title":"7. Update a Single Column","text":"<ul> <li>POST <code>/device/&lt;mac&gt;/update-column</code> Update one specific column for a device.</li> </ul> <p>Request Body:</p> <pre><code>{\n \"columnName\": \"devName\",\n \"columnValue\": \"Updated Device Name\"\n}\n</code></pre> <p>Response (success):</p> <pre><code>{\n \"success\": true\n}\n</code></pre> <p>Error Responses:</p> <ul> <li>Device not found \u2192 HTTP 404</li> <li>Missing <code>columnName</code> or <code>columnValue</code> \u2192 HTTP 400</li> <li>Unauthorized \u2192 HTTP 403</li> </ul>"},{"location":"API_DEVICE/#example-curl-requests","title":"Example <code>curl</code> Requests","text":"<p>Get Device Details:</p> <pre><code>curl -X GET \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/device/AA:BB:CC:DD:EE:FF\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\"\n</code></pre> <p>Update Device Fields:</p> <pre><code>curl -X POST \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/device/AA:BB:CC:DD:EE:FF\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Content-Type: application/json\" \\\n --data '{\"devName\": \"New Device Name\"}'\n</code></pre> <p>Delete Device:</p> <pre><code>curl -X DELETE \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/device/AA:BB:CC:DD:EE:FF/delete\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\"\n</code></pre> <p>Copy Device Data:</p> <pre><code>curl -X POST \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/device/copy\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Content-Type: application/json\" \\\n --data '{\"macFrom\":\"AA:BB:CC:DD:EE:FF\",\"macTo\":\"11:22:33:44:55:66\"}'\n</code></pre> <p>Update Single Column:</p> <pre><code>curl -X POST \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/device/AA:BB:CC:DD:EE:FF/update-column\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Content-Type: application/json\" \\\n --data '{\"columnName\":\"devName\",\"columnValue\":\"Updated Device\"}'\n</code></pre>"},{"location":"API_DEVICES/","title":"Devices Collection API Endpoints","text":"<p>The Devices Collection API provides operations to retrieve, manage, import/export, and filter devices in bulk. All endpoints require authorization via Bearer token.</p>"},{"location":"API_DEVICES/#endpoints","title":"Endpoints","text":""},{"location":"API_DEVICES/#1-get-all-devices","title":"1. Get All Devices","text":"<ul> <li>GET <code>/devices</code> Retrieves all devices from the database.</li> </ul> <p>Response (success):</p> <pre><code>{\n \"success\": true,\n \"devices\": [\n {\n \"devName\": \"Net - Huawei\",\n \"devMAC\": \"AA:BB:CC:DD:EE:FF\",\n \"devIP\": \"192.168.1.1\",\n \"devType\": \"Router\",\n \"devFavorite\": 0,\n \"devStatus\": \"online\"\n },\n ...\n ]\n}\n</code></pre> <p>Error Responses:</p> <ul> <li>Unauthorized \u2192 HTTP 403</li> </ul>"},{"location":"API_DEVICES/#2-delete-devices-by-mac","title":"2. Delete Devices by MAC","text":"<ul> <li>DELETE <code>/devices</code> Deletes devices by MAC address. Supports exact matches or wildcard <code>*</code>.</li> </ul> <p>Request Body:</p> <pre><code>{\n \"macs\": [\"AA:BB:CC:DD:EE:FF\", \"11:22:33:*\"]\n}\n</code></pre> <p>Behavior:</p> <ul> <li>If <code>macs</code> is omitted or <code>null</code> \u2192 deletes all devices.</li> <li>Wildcards <code>*</code> match multiple devices.</li> </ul> <p>Response:</p> <pre><code>{\n \"success\": true,\n \"deleted_count\": 5\n}\n</code></pre> <p>Error Responses:</p> <ul> <li>Unauthorized \u2192 HTTP 403</li> </ul>"},{"location":"API_DEVICES/#3-delete-devices-with-empty-macs","title":"3. Delete Devices with Empty MACs","text":"<ul> <li>DELETE <code>/devices/empty-macs</code> Removes all devices where MAC address is null or empty.</li> </ul> <p>Response:</p> <pre><code>{\n \"success\": true,\n \"deleted\": 3\n}\n</code></pre>"},{"location":"API_DEVICES/#4-delete-unknown-devices","title":"4. Delete Unknown Devices","text":"<ul> <li>DELETE <code>/devices/unknown</code> Deletes devices with names marked as <code>(unknown)</code> or <code>(name not found)</code>.</li> </ul> <p>Response:</p> <pre><code>{\n \"success\": true,\n \"deleted\": 2\n}\n</code></pre>"},{"location":"API_DEVICES/#5-export-devices","title":"5. Export Devices","text":"<ul> <li>GET <code>/devices/export</code> or <code>/devices/export/&lt;format&gt;</code> Exports all devices in CSV (default) or JSON format.</li> </ul> <p>Query Parameter / URL Parameter:</p> <ul> <li><code>format</code> (optional) \u2192 <code>csv</code> (default) or <code>json</code></li> </ul> <p>CSV Response:</p> <ul> <li>Returns as a downloadable CSV file: <code>Content-Disposition: attachment; filename=devices.csv</code></li> </ul> <p>JSON Response:</p> <pre><code>{\n \"data\": [\n { \"devName\": \"Net - Huawei\", \"devMAC\": \"AA:BB:CC:DD:EE:FF\", ... },\n ...\n ],\n \"columns\": [\"devName\", \"devMAC\", \"devIP\", \"devType\", \"devFavorite\", \"devStatus\"]\n}\n</code></pre> <p>Error Responses:</p> <ul> <li>Unsupported format \u2192 HTTP 400</li> </ul>"},{"location":"API_DEVICES/#6-import-devices-from-csv","title":"6. Import Devices from CSV","text":"<ul> <li>POST <code>/devices/import</code> Imports devices from an uploaded CSV or base64-encoded CSV content.</li> </ul> <p>Request Body (multipart file or JSON with <code>content</code> field):</p> <pre><code>{\n \"content\": \"&lt;base64-encoded CSV content&gt;\"\n}\n</code></pre> <p>Response:</p> <pre><code>{\n \"success\": true,\n \"inserted\": 25,\n \"skipped_lines\": [3, 7]\n}\n</code></pre> <p>Error Responses:</p> <ul> <li>Missing file or content \u2192 HTTP 400 / 404</li> <li>CSV malformed \u2192 HTTP 400</li> </ul>"},{"location":"API_DEVICES/#7-get-device-totals","title":"7. Get Device Totals","text":"<ul> <li>GET <code>/devices/totals</code> Returns counts of devices by various categories.</li> </ul> <p>Response:</p> <pre><code>[\n 120, // Total devices\n 85, // Connected\n 5, // Favorites\n 10, // New\n 8, // Down\n 12 // Archived\n]\n</code></pre> <p>Order: <code>[all, connected, favorites, new, down, archived]</code></p>"},{"location":"API_DEVICES/#8-get-devices-by-status","title":"8. Get Devices by Status","text":"<ul> <li>GET <code>/devices/by-status?status=&lt;status&gt;</code> Returns devices filtered by status.</li> </ul> <p>Query Parameter:</p> <ul> <li><code>status</code> \u2192 Supported values: <code>online</code>, <code>offline</code>, <code>down</code>, <code>archived</code>, <code>favorites</code>, <code>new</code>, <code>my</code></li> <li>If omitted, returns all devices.</li> </ul> <p>Response (success):</p> <pre><code>[\n { \"id\": \"AA:BB:CC:DD:EE:FF\", \"title\": \"Net - Huawei\", \"favorite\": 0 },\n { \"id\": \"11:22:33:44:55:66\", \"title\": \"\u2605 USG Firewall\", \"favorite\": 1 }\n]\n</code></pre> <p>If <code>devFavorite=1</code>, the title is prepended with a star <code>\u2605</code>.</p>"},{"location":"API_DEVICES/#9-search-devices","title":"9. Search Devices","text":"<ul> <li>POST <code>/devices/search</code> Search for devices by MAC, name, or IP address.</li> </ul> <p>Request Body (JSON):</p> <pre><code>{\n \"query\": \".50\"\n}\n</code></pre> <p>Response:</p> <pre><code>{\n \"success\": true,\n \"devices\": [\n {\n \"devName\": \"Test Device\",\n \"devMac\": \"AA:BB:CC:DD:EE:FF\",\n \"devLastIP\": \"192.168.1.50\"\n }\n ]\n}\n</code></pre>"},{"location":"API_DEVICES/#10-get-latest-device","title":"10. Get Latest Device","text":"<ul> <li>GET <code>/devices/latest</code> Get the most recently connected device.</li> </ul> <p>Response:</p> <pre><code>[\n {\n \"devName\": \"Latest Device\",\n \"devMac\": \"AA:BB:CC:DD:EE:FF\",\n \"devLastIP\": \"192.168.1.100\",\n \"devFirstConnection\": \"2025-12-07 10:30:00\"\n }\n]\n</code></pre>"},{"location":"API_DEVICES/#11-get-network-topology","title":"11. Get Network Topology","text":"<ul> <li>GET <code>/devices/network/topology</code> Get network topology showing device relationships.</li> </ul> <p>Response:</p> <pre><code>{\n \"nodes\": [\n {\n \"id\": \"AA:AA:AA:AA:AA:AA\",\n \"name\": \"Router\",\n \"vendor\": \"VendorA\"\n }\n ],\n \"links\": [\n {\n \"source\": \"AA:AA:AA:AA:AA:AA\",\n \"target\": \"BB:BB:BB:BB:BB:BB\",\n \"port\": \"eth1\"\n }\n ]\n}\n</code></pre>"},{"location":"API_DEVICES/#mcp-tools","title":"MCP Tools","text":"<p>These endpoints are also available as MCP Tools for AI assistant integration: - <code>list_devices</code>, <code>search_devices</code>, <code>get_latest_device</code>, <code>get_network_topology</code>, <code>set_device_alias</code></p> <p>\ud83d\udcd6 See MCP Server Bridge API for AI integration details.</p>"},{"location":"API_DEVICES/#example-curl-requests","title":"Example <code>curl</code> Requests","text":"<p>Get All Devices:</p> <pre><code>curl -X GET \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/devices\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\"\n</code></pre> <p>Delete Devices by MAC:</p> <pre><code>curl -X DELETE \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/devices\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Content-Type: application/json\" \\\n --data '{\"macs\":[\"AA:BB:CC:DD:EE:FF\",\"11:22:33:*\"]}'\n</code></pre> <p>Export Devices CSV:</p> <pre><code>curl -X GET \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/devices/export?format=csv\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\"\n</code></pre> <p>Import Devices from CSV:</p> <pre><code>curl -X POST \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/devices/import\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -F \"file=@devices.csv\"\n</code></pre> <p>Get Devices by Status:</p> <pre><code>curl -X GET \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/devices/by-status?status=online\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\"\n</code></pre> <p>Search Devices:</p> <pre><code>curl -X POST \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/devices/search\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Content-Type: application/json\" \\\n --data '{\"query\": \"192.168.1\"}'\n</code></pre> <p>Get Latest Device:</p> <pre><code>curl -X GET \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/devices/latest\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\"\n</code></pre> <p>Get Network Topology:</p> <pre><code>curl -X GET \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/devices/network/topology\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\"\n</code></pre>"},{"location":"API_DEVICE_FIELD_LOCK/","title":"Device Field Lock/Unlock API","text":""},{"location":"API_DEVICE_FIELD_LOCK/#overview","title":"Overview","text":"<p>The Device Field Lock/Unlock feature allows users to lock specific device fields to prevent plugin overwrites. This is part of the authoritative device field update system that ensures data integrity while maintaining flexibility for user customization.</p>"},{"location":"API_DEVICE_FIELD_LOCK/#concepts","title":"Concepts","text":""},{"location":"API_DEVICE_FIELD_LOCK/#tracked-fields","title":"Tracked Fields","text":"<p>Only certain device fields support locking. These are the fields that can be modified by both plugins and users:</p> <ul> <li><code>devName</code> - Device name/hostname</li> <li><code>devVendor</code> - Device vendor/manufacturer</li> <li><code>devFQDN</code> - Fully qualified domain name</li> <li><code>devSSID</code> - Network SSID</li> <li><code>devParentMAC</code> - Parent device MAC address</li> <li><code>devParentPort</code> - Parent device port</li> <li><code>devParentRelType</code> - Parent device relationship type</li> <li><code>devVlan</code> - VLAN identifier</li> </ul>"},{"location":"API_DEVICE_FIELD_LOCK/#field-source-tracking","title":"Field Source Tracking","text":"<p>Every tracked field has an associated <code>*Source</code> field that indicates where the current value originated:</p> <ul> <li><code>NEWDEV</code> - Created via the UI as a new device</li> <li><code>USER</code> - Manually edited by a user</li> <li><code>LOCKED</code> - Field is locked; prevents any plugin overwrites</li> <li>Plugin name (e.g., <code>UNIFIAPI</code>, <code>PIHOLE</code>) - Last updated by this plugin</li> </ul>"},{"location":"API_DEVICE_FIELD_LOCK/#locking-mechanism","title":"Locking Mechanism","text":"<p>When a field is locked, its source is set to <code>LOCKED</code>. This prevents plugin overwrites based on the authorization logic:</p> <ol> <li>Plugin wants to update field</li> <li>Authoritative handler checks field's <code>*Source</code> value</li> <li>If <code>*Source</code> == <code>LOCKED</code>, plugin update is rejected</li> <li>User can still manually unlock the field</li> </ol> <p>When a field is unlocked, its source is set to <code>NEWDEV</code>, allowing plugins to resume updates.</p>"},{"location":"API_DEVICE_FIELD_LOCK/#endpoints","title":"Endpoints","text":""},{"location":"API_DEVICE_FIELD_LOCK/#lock-or-unlock-a-field","title":"Lock or Unlock a Field","text":"<pre><code>POST /device/{mac}/field/lock\nAuthorization: Bearer {API_TOKEN}\nContent-Type: application/json\n\n{\n \"fieldName\": \"devName\",\n \"lock\": true\n}\n</code></pre>"},{"location":"API_DEVICE_FIELD_LOCK/#parameters","title":"Parameters","text":"<ul> <li><code>mac</code> (path, required): Device MAC address (e.g., <code>AA:BB:CC:DD:EE:FF</code>)</li> <li><code>fieldName</code> (body, required): Name of the field to lock/unlock. Must be one of the tracked fields listed above.</li> <li><code>lock</code> (body, required): Boolean. <code>true</code> to lock, <code>false</code> to unlock.</li> </ul>"},{"location":"API_DEVICE_FIELD_LOCK/#responses","title":"Responses","text":"<p>Success (200) <pre><code>{\n \"success\": true,\n \"message\": \"Field devName locked\",\n \"fieldName\": \"devName\",\n \"locked\": true\n}\n</code></pre></p> <p>Bad Request (400) <pre><code>{\n \"success\": false,\n \"error\": \"fieldName is required\"\n}\n</code></pre></p> <pre><code>{\n \"success\": false,\n \"error\": \"Field 'devInvalidField' cannot be locked\"\n}\n</code></pre> <p>Unauthorized (403) <pre><code>{\n \"success\": false,\n \"error\": \"Unauthorized\"\n}\n</code></pre></p> <p>Not Found (404) <pre><code>{\n \"success\": false,\n \"error\": \"Device not found\"\n}\n</code></pre></p>"},{"location":"API_DEVICE_FIELD_LOCK/#examples","title":"Examples","text":""},{"location":"API_DEVICE_FIELD_LOCK/#lock-a-device-name","title":"Lock a Device Name","text":"<p>Prevent the device name from being overwritten by plugins:</p> <pre><code>curl -X POST https://your-netalertx.local/api/device/AA:BB:CC:DD:EE:FF/field/lock \\\n -H \"Authorization: Bearer your-api-token\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"fieldName\": \"devName\",\n \"lock\": true\n }'\n</code></pre>"},{"location":"API_DEVICE_FIELD_LOCK/#unlock-a-field","title":"Unlock a Field","text":"<p>Allow plugins to resume updating a field:</p> <pre><code>curl -X POST https://your-netalertx.local/api/device/AA:BB:CC:DD:EE:FF/field/lock \\\n -H \"Authorization: Bearer your-api-token\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"fieldName\": \"devName\",\n \"lock\": false\n }'\n</code></pre>"},{"location":"API_DEVICE_FIELD_LOCK/#ui-integration","title":"UI Integration","text":"<p>The Device Edit form displays lock/unlock buttons for all tracked fields:</p> <ol> <li>Lock Button (\ud83d\udd12): Click to prevent plugin overwrites</li> <li>Unlock Button (\ud83d\udd13): Click to allow plugin overwrites again</li> <li>Source Indicator: Shows current field source (USER, LOCKED, NEWDEV, or plugin name)</li> </ol>"},{"location":"API_DEVICE_FIELD_LOCK/#authorization-handler","title":"Authorization Handler","text":"<p>The authoritative field update logic prevents plugin overwrites:</p> <ol> <li>Plugin provides new value for field via plugin config <code>SET_ALWAYS</code>/<code>SET_EMPTY</code></li> <li>Authoritative handler (in DeviceInstance) checks <code>{field}Source</code> value</li> <li>If source is <code>LOCKED</code> or <code>USER</code>, plugin update is rejected</li> <li>If source is <code>NEWDEV</code> or plugin name, plugin update is accepted</li> </ol>"},{"location":"API_DEVICE_FIELD_LOCK/#see-also","title":"See Also","text":"<ul> <li>Device locking</li> <li>Device source fields</li> <li>API Device Endpoints Documentation</li> <li>Authoritative Field Updates System</li> <li>Plugin Configuration Reference</li> </ul>"},{"location":"API_EVENTS/","title":"Events API Endpoints","text":"<p>The Events API provides access to device event logs, allowing creation, retrieval, deletion, and summary of events over time.</p>"},{"location":"API_EVENTS/#endpoints","title":"Endpoints","text":""},{"location":"API_EVENTS/#1-create-event","title":"1. Create Event","text":"<ul> <li>POST <code>/events/create/&lt;mac&gt;</code> Create an event for a device identified by its MAC address.</li> </ul> <p>Request Body (JSON):</p> <pre><code>{\n \"ip\": \"192.168.1.10\",\n \"event_type\": \"Device Down\",\n \"additional_info\": \"Optional info about the event\",\n \"pending_alert\": 1,\n \"event_time\": \"2025-08-24T12:00:00Z\"\n}\n</code></pre> <ul> <li> <p>Parameters:</p> </li> <li> <p><code>ip</code> (string, optional): IP address of the device</p> </li> <li><code>event_type</code> (string, optional): Type of event (default <code>\"Device Down\"</code>)</li> <li><code>additional_info</code> (string, optional): Extra information</li> <li><code>pending_alert</code> (int, optional): 1 if alert email is pending (default 1)</li> <li><code>event_time</code> (ISO datetime, optional): Event timestamp; defaults to current time</li> </ul> <p>Response (JSON):</p> <pre><code>{\n \"success\": true,\n \"message\": \"Event created for 00:11:22:33:44:55\"\n}\n</code></pre>"},{"location":"API_EVENTS/#2-get-events","title":"2. Get Events","text":"<ul> <li>GET <code>/events</code> Retrieve all events, optionally filtered by MAC address:</li> </ul> <pre><code>/events?mac=&lt;mac&gt;\n</code></pre> <p>Response:</p> <pre><code>{\n \"success\": true,\n \"events\": [\n {\n \"eve_MAC\": \"00:11:22:33:44:55\",\n \"eve_IP\": \"192.168.1.10\",\n \"eve_DateTime\": \"2025-08-24T12:00:00Z\",\n \"eve_EventType\": \"Device Down\",\n \"eve_AdditionalInfo\": \"\",\n \"eve_PendingAlertEmail\": 1\n }\n ]\n}\n</code></pre>"},{"location":"API_EVENTS/#3-delete-events","title":"3. Delete Events","text":"<ul> <li>DELETE <code>/events/&lt;mac&gt;</code> \u2192 Delete events for a specific MAC</li> <li>DELETE <code>/events</code> \u2192 Delete all events</li> <li>DELETE <code>/events/&lt;days&gt;</code> \u2192 Delete events older than N days</li> </ul> <p>Response:</p> <pre><code>{\n \"success\": true,\n \"message\": \"Deleted events older than &lt;days&gt; days\"\n}\n</code></pre>"},{"location":"API_EVENTS/#4-get-recent-events","title":"4. Get Recent Events","text":"<ul> <li>GET <code>/events/recent</code> \u2192 Get events from the last 24 hours</li> <li>GET <code>/events/&lt;hours&gt;</code> \u2192 Get events from the last N hours</li> </ul> <p>Response (JSON):</p> <pre><code>{\n \"success\": true,\n \"hours\": 24,\n \"count\": 5,\n \"events\": [\n {\n \"eve_DateTime\": \"2025-12-07 12:00:00\",\n \"eve_EventType\": \"New Device\",\n \"eve_MAC\": \"AA:BB:CC:DD:EE:FF\",\n \"eve_IP\": \"192.168.1.100\",\n \"eve_AdditionalInfo\": \"Device detected\"\n }\n ]\n}\n</code></pre>"},{"location":"API_EVENTS/#5-get-latest-events","title":"5. Get Latest Events","text":"<ul> <li>GET <code>/events/last</code> Get the 10 most recent events.</li> </ul> <p>Response (JSON):</p> <pre><code>{\n \"success\": true,\n \"count\": 10,\n \"events\": [\n {\n \"eve_DateTime\": \"2025-12-07 12:00:00\",\n \"eve_EventType\": \"Device Down\",\n \"eve_MAC\": \"AA:BB:CC:DD:EE:FF\"\n }\n ]\n}\n</code></pre>"},{"location":"API_EVENTS/#6-event-totals-over-a-period","title":"6. Event Totals Over a Period","text":"<ul> <li>GET <code>/sessions/totals?period=&lt;period&gt;</code> Return event and session totals over a given period.</li> </ul> <p>Query Parameters:</p> Parameter Description <code>period</code> Time period for totals, e.g., <code>\"7 days\"</code>, <code>\"1 month\"</code>, <code>\"1 year\"</code>, <code>\"100 years\"</code> <p>Sample Response (JSON Array):</p> <pre><code>[120, 85, 5, 10, 3, 7]\n</code></pre> <p>Meaning of Values:</p> <ol> <li>Total events in the period</li> <li>Total sessions</li> <li>Missing sessions</li> <li>Voided events (<code>eve_EventType LIKE 'VOIDED%'</code>)</li> <li>New device events (<code>eve_EventType LIKE 'New Device'</code>)</li> <li>Device down events (<code>eve_EventType LIKE 'Device Down'</code>)</li> </ol>"},{"location":"API_EVENTS/#mcp-tools","title":"MCP Tools","text":"<p>Event endpoints are available as MCP Tools for AI assistant integration: - <code>get_recent_alerts</code>, <code>get_last_events</code></p> <p>\ud83d\udcd6 See MCP Server Bridge API for AI integration details.</p>"},{"location":"API_EVENTS/#notes","title":"Notes","text":"<ul> <li>All endpoints require authorization (Bearer token). Unauthorized requests return HTTP 403:</li> </ul> <pre><code>{\n \"success\": false,\n \"message\": \"ERROR: Not authorized\",\n \"error\": \"Forbidden\"\n}\n</code></pre> <ul> <li> <p>Events are stored in the Events table with the following fields: <code>eve_MAC</code>, <code>eve_IP</code>, <code>eve_DateTime</code>, <code>eve_EventType</code>, <code>eve_AdditionalInfo</code>, <code>eve_PendingAlertEmail</code>.</p> </li> <li> <p>Event creation automatically logs activity for debugging.</p> </li> </ul>"},{"location":"API_EVENTS/#example-curl-requests","title":"Example <code>curl</code> Requests","text":"<p>Create Event:</p> <pre><code>curl -X POST \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/events/create/00:11:22:33:44:55\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Content-Type: application/json\" \\\n --data '{\n \"ip\": \"192.168.1.10\",\n \"event_type\": \"Device Down\",\n \"additional_info\": \"Power outage\",\n \"pending_alert\": 1\n }'\n</code></pre> <p>Get Events for a Device:</p> <pre><code>curl \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/events?mac=00:11:22:33:44:55\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\"\n</code></pre> <p>Delete Events Older Than 30 Days:</p> <pre><code>curl -X DELETE \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/events/30\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\"\n</code></pre> <p>Get Event Totals for 7 Days:</p> <pre><code>curl \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/sessions/totals?period=7 days\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\"\n</code></pre>"},{"location":"API_GRAPHQL/","title":"GraphQL API Endpoint","text":"<p>GraphQL queries are read-optimized for speed. Data may be slightly out of date until the file system cache refreshes. The GraphQL endpoints allow you to access the following objects:</p> <ul> <li>Devices</li> <li>Settings</li> <li>Language Strings (LangStrings)</li> </ul>"},{"location":"API_GRAPHQL/#endpoints","title":"Endpoints","text":"<ul> <li> <p>GET <code>/graphql</code> Returns a simple status message (useful for browser or debugging).</p> </li> <li> <p>POST <code>/graphql</code> Execute GraphQL queries against the <code>devicesSchema</code>.</p> </li> </ul>"},{"location":"API_GRAPHQL/#devices-query","title":"Devices Query","text":""},{"location":"API_GRAPHQL/#sample-query","title":"Sample Query","text":"<pre><code>query GetDevices($options: PageQueryOptionsInput) {\n devices(options: $options) {\n devices {\n rowid\n devMac\n devName\n devOwner\n devType\n devVendor\n devLastConnection\n devStatus\n }\n count\n }\n}\n</code></pre>"},{"location":"API_GRAPHQL/#query-parameters","title":"Query Parameters","text":"Parameter Description <code>page</code> Page number of results to fetch. <code>limit</code> Number of results per page. <code>sort</code> Sorting options (<code>field</code> = field name, <code>order</code> = <code>asc</code> or <code>desc</code>). <code>search</code> Term to filter devices. <code>status</code> Filter devices by status: <code>my_devices</code>, <code>connected</code>, <code>favorites</code>, <code>new</code>, <code>down</code>, <code>archived</code>, <code>offline</code>. <code>filters</code> Additional filters (array of <code>{ filterColumn, filterValue }</code>)."},{"location":"API_GRAPHQL/#curl-example","title":"<code>curl</code> Example","text":"<pre><code>curl 'http://host:GRAPHQL_PORT/graphql' \\\n -X POST \\\n -H 'Authorization: Bearer API_TOKEN' \\\n -H 'Content-Type: application/json' \\\n --data '{\n \"query\": \"query GetDevices($options: PageQueryOptionsInput) { devices(options: $options) { devices { rowid devMac devName devOwner devType devVendor devLastConnection devStatus } count } }\",\n \"variables\": {\n \"options\": {\n \"page\": 1,\n \"limit\": 10,\n \"sort\": [{ \"field\": \"devName\", \"order\": \"asc\" }],\n \"search\": \"\",\n \"status\": \"connected\"\n }\n }\n }'\n</code></pre>"},{"location":"API_GRAPHQL/#sample-response","title":"Sample Response","text":"<pre><code>{\n \"data\": {\n \"devices\": {\n \"devices\": [\n {\n \"rowid\": 1,\n \"devMac\": \"00:11:22:33:44:55\",\n \"devName\": \"Device 1\",\n \"devOwner\": \"Owner 1\",\n \"devType\": \"Type 1\",\n \"devVendor\": \"Vendor 1\",\n \"devLastConnection\": \"2025-01-01T00:00:00Z\",\n \"devStatus\": \"connected\"\n }\n ],\n \"count\": 1\n }\n }\n}\n</code></pre>"},{"location":"API_GRAPHQL/#settings-query","title":"Settings Query","text":"<p>The settings query provides access to NetAlertX configuration stored in the settings table.</p>"},{"location":"API_GRAPHQL/#sample-query_1","title":"Sample Query","text":"<pre><code>query GetSettings {\n settings {\n settings {\n setKey\n setName\n setDescription\n setType\n setOptions\n setGroup\n setValue\n setEvents\n setOverriddenByEnv\n }\n count\n }\n}\n</code></pre>"},{"location":"API_GRAPHQL/#schema-fields","title":"Schema Fields","text":"Field Type Description <code>setKey</code> String Unique key identifier for the setting. <code>setName</code> String Human-readable name. <code>setDescription</code> String Description or documentation of the setting. <code>setType</code> String Data type (<code>string</code>, <code>int</code>, <code>bool</code>, <code>json</code>, etc.). <code>setOptions</code> String Available options (for dropdown/select-type settings). <code>setGroup</code> String Group/category the setting belongs to. <code>setValue</code> String Current value of the setting. <code>setEvents</code> String Events or triggers related to this setting. <code>setOverriddenByEnv</code> Boolean Whether the setting is overridden by an environment variable at runtime."},{"location":"API_GRAPHQL/#curl-example_1","title":"<code>curl</code> Example","text":"<pre><code>curl 'http://host:GRAPHQL_PORT/graphql' \\\n -X POST \\\n -H 'Authorization: Bearer API_TOKEN' \\\n -H 'Content-Type: application/json' \\\n --data '{\n \"query\": \"query GetSettings { settings { settings { setKey setName setDescription setType setOptions setGroup setValue setEvents setOverriddenByEnv } count } }\"\n }'\n</code></pre>"},{"location":"API_GRAPHQL/#sample-response_1","title":"Sample Response","text":"<pre><code>{\n \"data\": {\n \"settings\": {\n \"settings\": [\n {\n \"setKey\": \"UI_MY_DEVICES\",\n \"setName\": \"My Devices Filter\",\n \"setDescription\": \"Defines which statuses to include in the 'My Devices' view.\",\n \"setType\": \"list\",\n \"setOptions\": \"[\\\"online\\\",\\\"new\\\",\\\"down\\\",\\\"offline\\\",\\\"archived\\\"]\",\n \"setGroup\": \"UI\",\n \"setValue\": \"[\\\"online\\\",\\\"new\\\"]\",\n \"setEvents\": null,\n \"setOverriddenByEnv\": false\n },\n {\n \"setKey\": \"NETWORK_DEVICE_TYPES\",\n \"setName\": \"Network Device Types\",\n \"setDescription\": \"Types of devices considered as network infrastructure.\",\n \"setType\": \"list\",\n \"setOptions\": \"[\\\"Router\\\",\\\"Switch\\\",\\\"AP\\\"]\",\n \"setGroup\": \"Network\",\n \"setValue\": \"[\\\"Router\\\",\\\"Switch\\\"]\",\n \"setEvents\": null,\n \"setOverriddenByEnv\": true\n }\n ],\n \"count\": 2\n }\n }\n}\n</code></pre>"},{"location":"API_GRAPHQL/#langstrings-query","title":"LangStrings Query","text":"<p>The LangStrings query provides access to localized strings. Supports filtering by <code>langCode</code> and <code>langStringKey</code>. If the requested string is missing or empty, you can optionally fallback to <code>en_us</code>.</p>"},{"location":"API_GRAPHQL/#sample-query_2","title":"Sample Query","text":"<pre><code>query GetLangStrings {\n langStrings(langCode: \"de_de\", langStringKey: \"settings_other_scanners\") {\n langStrings {\n langCode\n langStringKey\n langStringText\n }\n count\n }\n}\n</code></pre>"},{"location":"API_GRAPHQL/#query-parameters_1","title":"Query Parameters","text":"Parameter Type Description <code>langCode</code> String Optional language code (e.g., <code>en_us</code>, <code>de_de</code>). If omitted, all languages are returned. <code>langStringKey</code> String Optional string key to retrieve a specific entry. <code>fallback_to_en</code> Boolean Optional (default <code>true</code>). If <code>true</code>, empty or missing strings fallback to <code>en_us</code>."},{"location":"API_GRAPHQL/#curl-example_2","title":"<code>curl</code> Example","text":"<pre><code>curl 'http://host:GRAPHQL_PORT/graphql' \\\n -X POST \\\n -H 'Authorization: Bearer API_TOKEN' \\\n -H 'Content-Type: application/json' \\\n --data '{\n \"query\": \"query GetLangStrings { langStrings(langCode: \\\"de_de\\\", langStringKey: \\\"settings_other_scanners\\\") { langStrings { langCode langStringKey langStringText } count } }\"\n }'\n</code></pre>"},{"location":"API_GRAPHQL/#sample-response_2","title":"Sample Response","text":"<pre><code>{\n \"data\": {\n \"langStrings\": {\n \"count\": 1,\n \"langStrings\": [\n {\n \"langCode\": \"de_de\",\n \"langStringKey\": \"settings_other_scanners\",\n \"langStringText\": \"Other, non-device scanner plugins that are currently enabled.\" // falls back to en_us if empty\n }\n ]\n }\n }\n}\n</code></pre>"},{"location":"API_GRAPHQL/#notes","title":"Notes","text":"<ul> <li>Device, settings, and LangStrings queries can be combined in one request since GraphQL supports batching.</li> <li>The <code>fallback_to_en</code> feature ensures UI always has a value even if a translation is missing.</li> <li>Data is cached in memory per JSON file; changes to language or plugin files will only refresh after the cache detects a file modification.</li> <li>The <code>setOverriddenByEnv</code> flag helps identify setting values that are locked at container runtime.</li> <li>The schema is read-only \u2014 updates must be performed through other APIs or configuration management. See the other API endpoints for details. </li> </ul>"},{"location":"API_LOGS/","title":"Logs API Endpoints","text":"<p>Manage or purge application log files stored under <code>/app/log</code> and manage the execution queue. These endpoints are primarily used for maintenance tasks such as clearing accumulated logs or adding system actions without restarting the container.</p> <p>Only specific, pre-approved log files can be purged for security and stability reasons.</p>"},{"location":"API_LOGS/#delete-purge-a-log-file","title":"Delete (Purge) a Log File","text":"<ul> <li>DELETE <code>/logs?file=&lt;log_file&gt;</code> \u2192 Purge the contents of an allowed log file.</li> </ul> <p>Query Parameter:</p> <ul> <li><code>file</code> \u2192 The name of the log file to purge (e.g., <code>app.log</code>, <code>stdout.log</code>)</li> </ul> <p>Allowed Files:</p> <pre><code>app.log\nIP_changes.log\nstdout.log\nstderr.log\napp.php_errors.log\nexecution_queue.log\ndb_is_locked.log\n</code></pre> <p>Authorization: Requires a valid API token in the <code>Authorization</code> header.</p>"},{"location":"API_LOGS/#curl-example-success","title":"<code>curl</code> Example (Success)","text":"<pre><code>curl -X DELETE 'http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/logs?file=app.log' \\\n -H 'Authorization: Bearer &lt;API_TOKEN&gt;' \\\n -H 'Accept: application/json'\n</code></pre> <p>Response:</p> <pre><code>{\n \"success\": true,\n \"message\": \"[clean_log] File app.log purged successfully\"\n}\n</code></pre>"},{"location":"API_LOGS/#curl-example-not-allowed","title":"<code>curl</code> Example (Not Allowed)","text":"<pre><code>curl -X DELETE 'http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/logs?file=not_allowed.log' \\\n -H 'Authorization: Bearer &lt;API_TOKEN&gt;' \\\n -H 'Accept: application/json'\n</code></pre> <p>Response:</p> <pre><code>{\n \"success\": false,\n \"message\": \"[clean_log] File not_allowed.log is not allowed to be purged\"\n}\n</code></pre>"},{"location":"API_LOGS/#curl-example-unauthorized","title":"<code>curl</code> Example (Unauthorized)","text":"<pre><code>curl -X DELETE 'http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/logs?file=app.log' \\\n -H 'Accept: application/json'\n</code></pre> <p>Response:</p> <pre><code>{\n \"error\": \"Forbidden\"\n}\n</code></pre>"},{"location":"API_LOGS/#add-an-action-to-the-execution-queue","title":"Add an Action to the Execution Queue","text":"<ul> <li>POST <code>/logs/add-to-execution-queue</code> \u2192 Add a system action to the execution queue.</li> </ul> <p>Request Body (JSON):</p> <pre><code>{\n \"action\": \"update_api|devices\"\n}\n</code></pre> <p>Authorization: Requires a valid API token in the <code>Authorization</code> header.</p>"},{"location":"API_LOGS/#curl-example-success_1","title":"<code>curl</code> Example (Success)","text":"<p>The below will update the API cache for Devices</p> <pre><code>curl -X POST 'http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/logs/add-to-execution-queue' \\\n -H 'Authorization: Bearer &lt;API_TOKEN&gt;' \\\n -H 'Content-Type: application/json' \\\n --data '{\"action\": \"update_api|devices\"}'\n</code></pre> <p>Response:</p> <pre><code>{\n \"success\": true,\n \"message\": \"[UserEventsQueueInstance] Action \\\"update_api|devices\\\" added to the execution queue.\"\n}\n</code></pre>"},{"location":"API_LOGS/#curl-example-missing-parameter","title":"<code>curl</code> Example (Missing Parameter)","text":"<pre><code>curl -X POST 'http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/logs/add-to-execution-queue' \\\n -H 'Authorization: Bearer &lt;API_TOKEN&gt;' \\\n -H 'Content-Type: application/json' \\\n --data '{}'\n</code></pre> <p>Response:</p> <pre><code>{\n \"success\": false,\n \"message\": \"Missing parameters\",\n \"error\": \"Missing required 'action' field in JSON body\"\n}\n</code></pre>"},{"location":"API_LOGS/#curl-example-unauthorized_1","title":"<code>curl</code> Example (Unauthorized)","text":"<pre><code>curl -X POST 'http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/logs/add-to-execution-queue' \\\n -H 'Content-Type: application/json' \\\n --data '{\"action\": \"update_api|devices\"}'\n</code></pre> <p>Response:</p> <pre><code>{\n \"error\": \"Forbidden\"\n}\n</code></pre>"},{"location":"API_LOGS/#notes","title":"Notes","text":"<ul> <li>Only predefined files in <code>/app/log</code> can be purged \u2014 arbitrary paths are not permitted.</li> <li> <p>When a log file is purged:</p> </li> <li> <p>Its content is replaced with a short marker text: <code>\"File manually purged\"</code>.</p> </li> <li>A backend log entry is created via <code>mylog()</code>.</li> <li>A frontend notification is generated via <code>write_notification()</code>.</li> <li>Execution queue actions are appended to <code>execution_queue.log</code> and can be processed asynchronously by background tasks or workflows.</li> <li>Unauthorized or invalid attempts are safely logged and rejected.</li> <li>For advanced log retrieval, analysis, or structured querying, use the frontend log viewer.</li> <li>Always ensure that sensitive or production logs are handled carefully \u2014 purging cannot be undone.</li> </ul>"},{"location":"API_MCP/","title":"MCP Server Bridge API","text":"<p>The MCP (Model Context Protocol) Server Bridge provides AI assistants with standardized access to NetAlertX functionality through tools and server-sent events. This enables AI systems to interact with your network monitoring data in real-time.</p>"},{"location":"API_MCP/#overview","title":"Overview","text":"<p>The MCP Server Bridge exposes NetAlertX functionality as MCP Tools that AI assistants can call to:</p> <ul> <li>Search and retrieve device information</li> <li>Trigger network scans</li> <li>Get network topology and events</li> <li>Wake devices via Wake-on-LAN</li> <li>Access open port information</li> <li>Set device aliases</li> </ul> <p>All MCP endpoints mirror the functionality of standard REST endpoints but are optimized for AI assistant integration.</p>"},{"location":"API_MCP/#architecture-overview","title":"Architecture Overview","text":""},{"location":"API_MCP/#mcp-connection-flow","title":"MCP Connection Flow","text":"<pre><code>graph TB\n A[AI Assistant&lt;br/&gt;Claude Desktop] --&gt;|SSE Connection| B[NetAlertX MCP Server&lt;br/&gt;:20212/mcp/sse]\n B --&gt;|JSON-RPC Messages| C[MCP Bridge&lt;br/&gt;api_server_start.py]\n C --&gt;|Tool Calls| D[NetAlertX Tools&lt;br/&gt;Device/Network APIs]\n D --&gt;|Response Data| C\n C --&gt;|JSON Response| B\n B --&gt;|Stream Events| A</code></pre>"},{"location":"API_MCP/#mcp-tool-integration","title":"MCP Tool Integration","text":"<pre><code>sequenceDiagram\n participant AI as AI Assistant\n participant MCP as MCP Server (:20212)\n participant API as NetAlertX API (:20211)\n participant DB as SQLite Database\n\n AI-&gt;&gt;MCP: 1. Connect via SSE\n MCP--&gt;&gt;AI: 2. Session established\n AI-&gt;&gt;MCP: 3. tools/list request\n MCP-&gt;&gt;API: 4. GET /mcp/sse/openapi.json\n API--&gt;&gt;MCP: 5. Available tools spec\n MCP--&gt;&gt;AI: 6. Tool definitions\n AI-&gt;&gt;MCP: 7. tools/call: search_devices\n MCP-&gt;&gt;API: 8. POST /devices/search\n API-&gt;&gt;DB: 9. Query devices\n DB--&gt;&gt;API: 10. Device data\n API--&gt;&gt;MCP: 11. JSON response\n MCP--&gt;&gt;AI: 12. Tool result</code></pre>"},{"location":"API_MCP/#component-architecture","title":"Component Architecture","text":"<pre><code>graph LR\n subgraph \"AI Client\"\n A[Claude Desktop]\n B[Custom MCP Client]\n end\n\n subgraph \"NetAlertX MCP Server (:20212)\"\n C[SSE Endpoint&lt;br/&gt;/mcp/sse]\n D[Message Handler&lt;br/&gt;/mcp/messages]\n E[OpenAPI Spec&lt;br/&gt;/mcp/sse/openapi.json]\n end\n\n subgraph \"NetAlertX API Server (:20211)\"\n F[Device APIs&lt;br/&gt;/devices/*]\n G[Network Tools&lt;br/&gt;/nettools/*]\n H[Events API&lt;br/&gt;/events/*]\n end\n\n subgraph \"Backend\"\n I[SQLite Database]\n J[Network Scanners]\n K[Plugin System]\n end\n\n A -.-&gt;|Bearer Auth| C\n B -.-&gt;|Bearer Auth| C\n C --&gt; D\n C --&gt; E\n D --&gt; F\n D --&gt; G\n D --&gt; H\n F --&gt; I\n G --&gt; J\n H --&gt; I</code></pre>"},{"location":"API_MCP/#authentication","title":"Authentication","text":"<p>MCP endpoints use the same Bearer token authentication as REST endpoints:</p> <pre><code>Authorization: Bearer &lt;API_TOKEN&gt;\n</code></pre> <p>Unauthorized requests return HTTP 403:</p> <pre><code>{\n \"success\": false,\n \"message\": \"ERROR: Not authorized\",\n \"error\": \"Forbidden\"\n}\n</code></pre>"},{"location":"API_MCP/#mcp-connection-endpoint","title":"MCP Connection Endpoint","text":""},{"location":"API_MCP/#server-sent-events-sse","title":"Server-Sent Events (SSE)","text":"<ul> <li>GET/POST <code>/mcp/sse</code></li> </ul> <p>Main MCP connection endpoint for AI clients. Establishes a persistent connection using Server-Sent Events for real-time communication between AI assistants and NetAlertX.</p> <p>Connection Example:</p> <pre><code>const eventSource = new EventSource('/mcp/sse', {\n headers: {\n 'Authorization': 'Bearer &lt;API_TOKEN&gt;'\n }\n});\n\neventSource.onmessage = function(event) {\n const response = JSON.parse(event.data);\n console.log('MCP Response:', response);\n};\n</code></pre>"},{"location":"API_MCP/#openapi-specification","title":"OpenAPI Specification","text":""},{"location":"API_MCP/#get-mcp-tools-specification","title":"Get MCP Tools Specification","text":"<ul> <li>GET <code>/mcp/sse/openapi.json</code></li> </ul> <p>Returns the OpenAPI specification for all available MCP tools, describing the parameters and schemas for each tool.</p> <p>Response:</p> <pre><code>{\n \"openapi\": \"3.0.0\",\n \"info\": {\n \"title\": \"NetAlertX Tools\",\n \"version\": \"1.1.0\"\n },\n \"servers\": [{\"url\": \"/\"}],\n \"paths\": {\n \"/devices/by-status\": {\n \"post\": {\"operationId\": \"list_devices\"}\n },\n \"/device/{mac}\": {\n \"post\": {\"operationId\": \"get_device_info\"}\n },\n \"/devices/search\": {\n \"post\": {\"operationId\": \"search_devices\"}\n }\n }\n}\n</code></pre>"},{"location":"API_MCP/#available-mcp-tools","title":"Available MCP Tools","text":""},{"location":"API_MCP/#device-management-tools","title":"Device Management Tools","text":"Tool Endpoint Description <code>list_devices</code> <code>/devices/by-status</code> List devices by online status <code>get_device_info</code> <code>/device/{mac}</code> Get detailed device information <code>search_devices</code> <code>/devices/search</code> Search devices by MAC, name, or IP <code>get_latest_device</code> <code>/devices/latest</code> Get most recently connected device <code>set_device_alias</code> <code>/device/{mac}/set-alias</code> Set device friendly name"},{"location":"API_MCP/#network-tools","title":"Network Tools","text":"Tool Endpoint Description <code>trigger_scan</code> <code>/nettools/trigger-scan</code> Trigger network discovery scan to find new devices. <code>run_nmap_scan</code> <code>/nettools/nmap</code> Perform NMAP scan on a target to identify open ports. <code>get_open_ports</code> <code>/device/open_ports</code> Get stored NMAP open ports. Use <code>run_nmap_scan</code> first if empty. <code>wol_wake_device</code> <code>/nettools/wakeonlan</code> Wake device using Wake-on-LAN <code>get_network_topology</code> <code>/devices/network/topology</code> Get network topology map"},{"location":"API_MCP/#event-monitoring-tools","title":"Event &amp; Monitoring Tools","text":"Tool Endpoint Description <code>get_recent_alerts</code> <code>/events/recent</code> Get events from last 24 hours <code>get_last_events</code> <code>/events/last</code> Get 10 most recent events"},{"location":"API_MCP/#tool-usage-examples","title":"Tool Usage Examples","text":""},{"location":"API_MCP/#search-devices-tool","title":"Search Devices Tool","text":"<p>Tool Call: <pre><code>{\n \"jsonrpc\": \"2.0\",\n \"id\": \"1\",\n \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"search_devices\",\n \"arguments\": {\n \"query\": \"192.168.1\"\n }\n }\n}\n</code></pre></p> <p>Response: <pre><code>{\n \"jsonrpc\": \"2.0\",\n \"id\": \"1\",\n \"result\": {\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"{\\n \\\"success\\\": true,\\n \\\"devices\\\": [\\n {\\n \\\"devName\\\": \\\"Router\\\",\\n \\\"devMac\\\": \\\"AA:BB:CC:DD:EE:FF\\\",\\n \\\"devLastIP\\\": \\\"192.168.1.1\\\"\\n }\\n ]\\n}\"\n }\n ],\n \"isError\": false\n }\n}\n</code></pre></p>"},{"location":"API_MCP/#trigger-network-scan-tool","title":"Trigger Network Scan Tool","text":"<p>Tool Call: <pre><code>{\n \"jsonrpc\": \"2.0\",\n \"id\": \"2\",\n \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"trigger_scan\",\n \"arguments\": {\n \"type\": \"ARPSCAN\"\n }\n }\n}\n</code></pre></p> <p>Response: <pre><code>{\n \"jsonrpc\": \"2.0\",\n \"id\": \"2\",\n \"result\": {\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"{\\n \\\"success\\\": true,\\n \\\"message\\\": \\\"Scan triggered for type: ARPSCAN\\\"\\n}\"\n }\n ],\n \"isError\": false\n }\n}\n</code></pre></p>"},{"location":"API_MCP/#wake-on-lan-tool","title":"Wake-on-LAN Tool","text":"<p>Tool Call: <pre><code>{\n \"jsonrpc\": \"2.0\",\n \"id\": \"3\",\n \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"wol_wake_device\",\n \"arguments\": {\n \"devMac\": \"AA:BB:CC:DD:EE:FF\"\n }\n }\n}\n</code></pre></p>"},{"location":"API_MCP/#integration-with-ai-assistants","title":"Integration with AI Assistants","text":""},{"location":"API_MCP/#claude-desktop-integration","title":"Claude Desktop Integration","text":"<p>Add to your Claude Desktop <code>mcp.json</code> configuration:</p> <pre><code>{\n \"mcp\": {\n \"servers\": {\n \"netalertx\": {\n \"command\": \"node\",\n \"args\": [\"/path/to/mcp-client.js\"],\n \"env\": {\n \"NETALERTX_URL\": \"http://your-server:&lt;GRAPHQL_PORT&gt;\",\n \"NETALERTX_TOKEN\": \"your-api-token\"\n }\n }\n }\n }\n}\n</code></pre>"},{"location":"API_MCP/#generic-mcp-client","title":"Generic MCP Client","text":"<pre><code>import asyncio\nimport json\nfrom mcp import ClientSession, StdioServerParameters\nfrom mcp.client.stdio import stdio_client\n\nasync def main():\n # Connect to NetAlertX MCP server\n server_params = StdioServerParameters(\n command=\"curl\",\n args=[\n \"-N\", \"-H\", \"Authorization: Bearer &lt;API_TOKEN&gt;\",\n \"http://your-server:&lt;GRAPHQL_PORT&gt;/mcp/sse\"\n ]\n )\n\n async with stdio_client(server_params) as (read, write):\n async with ClientSession(read, write) as session:\n # Initialize connection\n await session.initialize()\n\n # List available tools\n tools = await session.list_tools()\n print(f\"Available tools: {[t.name for t in tools.tools]}\")\n\n # Call a tool\n result = await session.call_tool(\"search_devices\", {\"query\": \"router\"})\n print(f\"Search result: {result}\")\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n</code></pre>"},{"location":"API_MCP/#error-handling","title":"Error Handling","text":"<p>MCP tool calls return structured error information:</p> <p>Error Response: <pre><code>{\n \"jsonrpc\": \"2.0\",\n \"id\": \"1\",\n \"result\": {\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"Error calling tool: Device not found\"\n }\n ],\n \"isError\": true\n }\n}\n</code></pre></p> <p>Common Error Types: - <code>401/403</code> - Authentication failure - <code>400</code> - Invalid parameters or missing required fields - <code>404</code> - Resource not found (device, scan results, etc.) - <code>500</code> - Internal server error</p>"},{"location":"API_MCP/#notes","title":"Notes","text":"<ul> <li>MCP endpoints require the same API token authentication as REST endpoints</li> <li>All MCP tools return JSON responses wrapped in MCP protocol format</li> <li>Server-Sent Events maintain persistent connections for real-time updates</li> <li>Tool parameters match their REST endpoint equivalents</li> <li>Error responses include both HTTP status codes and descriptive messages</li> <li>MCP bridge automatically handles request/response serialization</li> </ul>"},{"location":"API_MCP/#related-documentation","title":"Related Documentation","text":"<ul> <li>Main API Overview - Core REST API documentation</li> <li>Device API - Individual device management</li> <li>Devices Collection API - Bulk device operations</li> <li>Network Tools API - Wake-on-LAN, scans, network utilities</li> <li>Events API - Event logging and monitoring</li> </ul>"},{"location":"API_MESSAGING_IN_APP/","title":"In-app Notifications API","text":"<p>Manage in-app notifications for users. Notifications can be written, retrieved, marked as read, or deleted.</p>"},{"location":"API_MESSAGING_IN_APP/#write-notification","title":"Write Notification","text":"<ul> <li>POST <code>/messaging/in-app/write</code> \u2192 Create a new in-app notification.</li> </ul> <p>Request Body:</p> <pre><code>{\n \"content\": \"This is a test notification\",\n \"level\": \"alert\" // optional, [\"interrupt\",\"info\",\"alert\"] default: \"alert\"\n}\n</code></pre> <p>Response:</p> <pre><code>{\n \"success\": true\n}\n</code></pre>"},{"location":"API_MESSAGING_IN_APP/#curl-example","title":"<code>curl</code> Example","text":"<pre><code>curl -X POST \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/messaging/in-app/write\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Accept: application/json\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"content\": \"This is a test notification\",\n \"level\": \"alert\"\n }'\n</code></pre>"},{"location":"API_MESSAGING_IN_APP/#get-unread-notifications","title":"Get Unread Notifications","text":"<ul> <li>GET <code>/messaging/in-app/unread</code> \u2192 Retrieve all unread notifications.</li> </ul> <p>Response:</p> <pre><code>[\n {\n \"timestamp\": \"2025-10-10T12:34:56\",\n \"guid\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\",\n \"read\": 0,\n \"level\": \"alert\",\n \"content\": \"This is a test notification\"\n }\n]\n</code></pre>"},{"location":"API_MESSAGING_IN_APP/#curl-example_1","title":"<code>curl</code> Example","text":"<pre><code>curl -X GET \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/messaging/in-app/unread\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Accept: application/json\"\n</code></pre>"},{"location":"API_MESSAGING_IN_APP/#mark-all-notifications-as-read","title":"Mark All Notifications as Read","text":"<ul> <li>POST <code>/messaging/in-app/read/all</code> \u2192 Mark all notifications as read.</li> </ul> <p>Response:</p> <pre><code>{\n \"success\": true\n}\n</code></pre>"},{"location":"API_MESSAGING_IN_APP/#curl-example_2","title":"<code>curl</code> Example","text":"<pre><code>curl -X POST \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/messaging/in-app/read/all\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Accept: application/json\"\n</code></pre>"},{"location":"API_MESSAGING_IN_APP/#mark-single-notification-as-read","title":"Mark Single Notification as Read","text":"<ul> <li>POST <code>/messaging/in-app/read/&lt;guid&gt;</code> \u2192 Mark a single notification as read using its GUID.</li> </ul> <p>Response (success):</p> <pre><code>{\n \"success\": true\n}\n</code></pre> <p>Response (failure):</p> <pre><code>{\n \"success\": false,\n \"error\": \"Notification not found\"\n}\n</code></pre>"},{"location":"API_MESSAGING_IN_APP/#curl-example_3","title":"<code>curl</code> Example","text":"<pre><code>curl -X POST \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/messaging/in-app/read/f47ac10b-58cc-4372-a567-0e02b2c3d479\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Accept: application/json\"\n</code></pre>"},{"location":"API_MESSAGING_IN_APP/#delete-all-notifications","title":"Delete All Notifications","text":"<ul> <li>DELETE <code>/messaging/in-app/delete</code> \u2192 Remove all notifications from the system.</li> </ul> <p>Response:</p> <pre><code>{\n \"success\": true\n}\n</code></pre>"},{"location":"API_MESSAGING_IN_APP/#curl-example_4","title":"<code>curl</code> Example","text":"<pre><code>curl -X DELETE \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/messaging/in-app/delete\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Accept: application/json\"\n</code></pre>"},{"location":"API_MESSAGING_IN_APP/#delete-single-notification","title":"Delete Single Notification","text":"<ul> <li>DELETE <code>/messaging/in-app/delete/&lt;guid&gt;</code> \u2192 Remove a single notification by its GUID.</li> </ul> <p>Response (success):</p> <pre><code>{\n \"success\": true\n}\n</code></pre> <p>Response (failure):</p> <pre><code>{\n \"success\": false,\n \"error\": \"Notification not found\"\n}\n</code></pre>"},{"location":"API_MESSAGING_IN_APP/#curl-example_5","title":"<code>curl</code> Example","text":"<pre><code>curl -X DELETE \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/messaging/in-app/delete/f47ac10b-58cc-4372-a567-0e02b2c3d479\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Accept: application/json\"\n</code></pre>"},{"location":"API_METRICS/","title":"Metrics API Endpoint","text":"<p>The <code>/metrics</code> endpoint exposes Prometheus-compatible metrics for NetAlertX, including aggregate device counts and per-device status.</p>"},{"location":"API_METRICS/#endpoint-details","title":"Endpoint Details","text":"<ul> <li>GET <code>/metrics</code> \u2192 Returns metrics in plain text.</li> <li>Host: NetAlertX server</li> <li>Port: As configured in <code>GRAPHQL_PORT</code> (default: <code>20212</code>)</li> </ul>"},{"location":"API_METRICS/#example-output","title":"Example Output","text":"<pre><code>netalertx_connected_devices 31\nnetalertx_offline_devices 54\nnetalertx_down_devices 0\nnetalertx_new_devices 0\nnetalertx_archived_devices 31\nnetalertx_favorite_devices 2\nnetalertx_my_devices 54\n\nnetalertx_device_status{device=\"Net - Huawei\", mac=\"Internet\", ip=\"1111.111.111.111\", vendor=\"None\", first_connection=\"2021-01-01 00:00:00\", last_connection=\"2025-08-04 17:57:00\", dev_type=\"Router\", device_status=\"Online\"} 1\nnetalertx_device_status{device=\"Net - USG\", mac=\"74:ac:74:ac:74:ac\", ip=\"192.168.1.1\", vendor=\"Ubiquiti Networks Inc.\", first_connection=\"2022-02-12 22:05:00\", last_connection=\"2025-06-07 08:16:49\", dev_type=\"Firewall\", device_status=\"Archived\"} 1\nnetalertx_device_status{device=\"Raspberry Pi 4 LAN\", mac=\"74:ac:74:ac:74:74\", ip=\"192.168.1.9\", vendor=\"Raspberry Pi Trading Ltd\", first_connection=\"2022-02-12 22:05:00\", last_connection=\"2025-08-04 17:57:00\", dev_type=\"Singleboard Computer (SBC)\", device_status=\"Online\"} 1\n...\n</code></pre>"},{"location":"API_METRICS/#metrics-overview","title":"Metrics Overview","text":""},{"location":"API_METRICS/#1-aggregate-device-counts","title":"1. Aggregate Device Counts","text":"Metric Description <code>netalertx_connected_devices</code> Devices currently connected <code>netalertx_offline_devices</code> Devices currently offline <code>netalertx_down_devices</code> Down/unreachable devices <code>netalertx_new_devices</code> Recently detected devices <code>netalertx_archived_devices</code> Archived devices <code>netalertx_favorite_devices</code> User-marked favorites <code>netalertx_my_devices</code> Devices associated with the current user"},{"location":"API_METRICS/#2-per-device-status","title":"2. Per-Device Status","text":"<p>Metric: <code>netalertx_device_status</code> Each device has labels:</p> <ul> <li><code>device</code>: friendly name</li> <li><code>mac</code>: MAC address (or placeholder)</li> <li><code>ip</code>: last recorded IP</li> <li><code>vendor</code>: manufacturer or \"None\"</li> <li><code>first_connection</code>: timestamp of first detection</li> <li><code>last_connection</code>: most recent contact</li> <li><code>dev_type</code>: device type/category</li> <li><code>device_status</code>: current status (<code>Online</code>, <code>Offline</code>, <code>Archived</code>, <code>Down</code>, \u2026)</li> </ul> <p>Metric value is always <code>1</code> (presence indicator).</p>"},{"location":"API_METRICS/#querying-with-curl","title":"Querying with <code>curl</code>","text":"<pre><code>curl 'http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/metrics' \\\n -H 'Authorization: Bearer &lt;API_TOKEN&gt;' \\\n -H 'Accept: text/plain'\n</code></pre> <p>Replace placeholders:</p> <ul> <li><code>&lt;server_ip&gt;</code> \u2013 NetAlertX host IP/hostname</li> <li><code>&lt;GRAPHQL_PORT&gt;</code> \u2013 configured port (default <code>20212</code>)</li> <li><code>&lt;API_TOKEN&gt;</code> \u2013 your API token</li> </ul>"},{"location":"API_METRICS/#prometheus-scraping-configuration","title":"Prometheus Scraping Configuration","text":"<pre><code>scrape_configs:\n - job_name: 'netalertx'\n metrics_path: /metrics\n scheme: http\n scrape_interval: 60s\n static_configs:\n - targets: ['&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;']\n authorization:\n type: Bearer\n credentials: &lt;API_TOKEN&gt;\n</code></pre>"},{"location":"API_METRICS/#grafana-dashboard-template","title":"Grafana Dashboard Template","text":"<p>Sample template JSON: Download</p>"},{"location":"API_NETTOOLS/","title":"Net Tools API Endpoints","text":"<p>The Net Tools API provides network diagnostic utilities, including Wake-on-LAN, traceroute, speed testing, DNS resolution, nmap scanning, internet connection information, and network interface info.</p> <p>All endpoints require authorization via Bearer token.</p>"},{"location":"API_NETTOOLS/#endpoints","title":"Endpoints","text":""},{"location":"API_NETTOOLS/#1-wake-on-lan","title":"1. Wake-on-LAN","text":"<ul> <li>POST <code>/nettools/wakeonlan</code> Sends a Wake-on-LAN packet to wake a device.</li> </ul> <p>Request Body (JSON):</p> <pre><code>{\n \"devMac\": \"AA:BB:CC:DD:EE:FF\"\n}\n</code></pre> <p>Response (success):</p> <pre><code>{\n \"success\": true,\n \"message\": \"WOL packet sent\",\n \"output\": \"Sent magic packet to AA:BB:CC:DD:EE:FF\"\n}\n</code></pre> <p>Error Responses:</p> <ul> <li>Invalid MAC address \u2192 HTTP 400</li> <li>Command failure \u2192 HTTP 500</li> </ul>"},{"location":"API_NETTOOLS/#2-traceroute","title":"2. Traceroute","text":"<ul> <li>POST <code>/nettools/traceroute</code> Performs a traceroute to a specified IP address.</li> </ul> <p>Request Body:</p> <pre><code>{\n \"devLastIP\": \"192.168.1.1\"\n}\n</code></pre> <p>Response (success):</p> <pre><code>{\n \"success\": true,\n \"output\": \"traceroute output as string\"\n}\n</code></pre> <p>Error Responses:</p> <ul> <li>Invalid IP \u2192 HTTP 400</li> <li>Traceroute command failure \u2192 HTTP 500</li> </ul>"},{"location":"API_NETTOOLS/#3-speedtest","title":"3. Speedtest","text":"<ul> <li>GET <code>/nettools/speedtest</code> Runs an internet speed test using <code>speedtest-cli</code>.</li> </ul> <p>Response (success):</p> <pre><code>{\n \"success\": true,\n \"output\": [\n \"Ping: 15 ms\",\n \"Download: 120.5 Mbit/s\",\n \"Upload: 22.4 Mbit/s\"\n ]\n}\n</code></pre> <p>Error Responses:</p> <ul> <li>Command failure \u2192 HTTP 500</li> </ul>"},{"location":"API_NETTOOLS/#4-dns-lookup-nslookup","title":"4. DNS Lookup (nslookup)","text":"<ul> <li>POST <code>/nettools/nslookup</code> Resolves an IP address or hostname using <code>nslookup</code>.</li> </ul> <p>Request Body:</p> <pre><code>{\n \"devLastIP\": \"8.8.8.8\"\n}\n</code></pre> <p>Response (success):</p> <pre><code>{\n \"success\": true,\n \"output\": [\n \"Server: 8.8.8.8\",\n \"Address: 8.8.8.8#53\",\n \"Name: google-public-dns-a.google.com\"\n ]\n}\n</code></pre> <p>Error Responses:</p> <ul> <li>Missing or invalid <code>devLastIP</code> \u2192 HTTP 400</li> <li>Command failure \u2192 HTTP 500</li> </ul>"},{"location":"API_NETTOOLS/#5-nmap-scan","title":"5. Nmap Scan","text":"<ul> <li>POST <code>/nettools/nmap</code> Runs an nmap scan on a target IP address or range.</li> </ul> <p>Request Body:</p> <pre><code>{\n \"scan\": \"192.168.1.0/24\",\n \"mode\": \"fast\"\n}\n</code></pre> <p>Supported Modes:</p> Mode nmap Arguments <code>fast</code> <code>-F</code> <code>normal</code> default <code>detail</code> <code>-A</code> <code>skipdiscovery</code> <code>-Pn</code> <p>Response (success):</p> <pre><code>{\n \"success\": true,\n \"mode\": \"fast\",\n \"ip\": \"192.168.1.0/24\",\n \"output\": [\n \"Starting Nmap 7.91\",\n \"Host 192.168.1.1 is up\",\n \"... scan results ...\"\n ]\n}\n</code></pre> <p>Error Responses:</p> <ul> <li>Invalid IP \u2192 HTTP 400</li> <li>Invalid mode \u2192 HTTP 400</li> <li>Command failure \u2192 HTTP 500</li> </ul>"},{"location":"API_NETTOOLS/#6-internet-connection-info","title":"6. Internet Connection Info","text":"<ul> <li>GET <code>/nettools/internetinfo</code> Fetches public internet connection information using <code>ipinfo.io</code>.</li> </ul> <p>Response (success):</p> <pre><code>{\n \"success\": true,\n \"output\": \"IP: 203.0.113.5 City: Sydney Country: AU Org: Example ISP\"\n}\n</code></pre> <p>Error Responses:</p> <ul> <li>Failed request or empty response \u2192 HTTP 500</li> </ul>"},{"location":"API_NETTOOLS/#7-network-interfaces","title":"7. Network Interfaces","text":"<ul> <li>GET <code>/nettools/interfaces</code> Fetches the list of network interfaces on the system, including IPv4/IPv6 addresses, MAC, MTU, state (up/down), and RX/TX byte counters.</li> </ul> <p>Response (success):</p> <pre><code>{\n \"success\": true,\n \"interfaces\": {\n \"eth0\": {\n \"name\": \"eth0\",\n \"short\": \"eth0\",\n \"type\": \"ethernet\",\n \"state\": \"up\",\n \"mtu\": 1500,\n \"mac\": \"00:11:32:EF:A5:6B\",\n \"ipv4\": [\"192.168.1.82/24\"],\n \"ipv6\": [\"fe80::211:32ff:feef:a56c/64\"],\n \"rx_bytes\": 18488221,\n \"tx_bytes\": 1443944\n },\n \"lo\": {\n \"name\": \"lo\",\n \"short\": \"lo\",\n \"type\": \"loopback\",\n \"state\": \"up\",\n \"mtu\": 65536,\n \"mac\": null,\n \"ipv4\": [\"127.0.0.1/8\"],\n \"ipv6\": [\"::1/128\"],\n \"rx_bytes\": 123456,\n \"tx_bytes\": 123456\n }\n }\n}\n</code></pre> <p>Error Responses:</p> <ul> <li>Command failure or parsing error \u2192 HTTP 500</li> </ul>"},{"location":"API_NETTOOLS/#example-curl-requests","title":"Example <code>curl</code> Requests","text":"<p>Wake-on-LAN:</p> <pre><code>curl -X POST \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/nettools/wakeonlan\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Content-Type: application/json\" \\\n --data '{\"devMac\":\"AA:BB:CC:DD:EE:FF\"}'\n</code></pre> <p>Traceroute:</p> <pre><code>curl -X POST \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/nettools/traceroute\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Content-Type: application/json\" \\\n --data '{\"devLastIP\":\"192.168.1.1\"}'\n</code></pre> <p>Speedtest:</p> <pre><code>curl \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/nettools/speedtest\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\"\n</code></pre> <p>Nslookup:</p> <pre><code>curl -X POST \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/nettools/nslookup\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Content-Type: application/json\" \\\n --data '{\"devLastIP\":\"8.8.8.8\"}'\n</code></pre> <p>Nmap Scan:</p> <pre><code>curl -X POST \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/nettools/nmap\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Content-Type: application/json\" \\\n --data '{\"scan\":\"192.168.1.0/24\",\"mode\":\"fast\"}'\n</code></pre> <p>Internet Info:</p> <pre><code>curl \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/nettools/internetinfo\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\"\n</code></pre> <p>Network Interfaces:</p> <pre><code>curl \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/nettools/interfaces\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\"\n</code></pre>"},{"location":"API_NETTOOLS/#mcp-tools","title":"MCP Tools","text":"<p>Network tools are available as MCP Tools for AI assistant integration:</p> <ul> <li><code>wol_wake_device</code>, <code>trigger_scan</code>, <code>get_open_ports</code></li> </ul> <p>\ud83d\udcd6 See MCP Server Bridge API for AI integration details.</p>"},{"location":"API_OLD/","title":"[Deprecated] API endpoints","text":"<p>Warning</p> <p>Some of these endpoints will be deprecated soon. Please refere to the new API endpoints docs for details on the new API layer.</p> <p>NetAlertX comes with a couple of API endpoints. All requests need to be authorized (executed in a logged in browser session) or you have to pass the value of the <code>API_TOKEN</code> settings as authorization bearer, for example:</p> <pre><code>curl 'http://host:GRAPHQL_PORT/graphql' \\\n -X POST \\\n -H 'Authorization: Bearer API_TOKEN' \\\n -H 'Content-Type: application/json' \\\n --data '{\n \"query\": \"query GetDevices($options: PageQueryOptionsInput) { devices(options: $options) { devices { rowid devMac devName devOwner devType devVendor devLastConnection devStatus } count } }\",\n \"variables\": {\n \"options\": {\n \"page\": 1,\n \"limit\": 10,\n \"sort\": [{ \"field\": \"devName\", \"order\": \"asc\" }],\n \"search\": \"\",\n \"status\": \"connected\"\n }\n }\n }'\n</code></pre>"},{"location":"API_OLD/#api-endpoint-graphql","title":"API Endpoint: GraphQL","text":"<ul> <li>Endpoint URL: <code>php/server/query_graphql.php</code></li> <li>Host: <code>same as front end (web ui)</code></li> <li>Port: <code>20212</code> or as defined by the <code>GRAPHQL_PORT</code> setting</li> </ul>"},{"location":"API_OLD/#example-query-to-fetch-devices","title":"Example Query to Fetch Devices","text":"<p>First, let's define the GraphQL query to fetch devices with pagination and sorting options.</p> <pre><code>query GetDevices($options: PageQueryOptionsInput) {\n devices(options: $options) {\n devices {\n rowid\n devMac\n devName\n devOwner\n devType\n devVendor\n devLastConnection\n devStatus\n }\n count\n }\n}\n</code></pre> <p>See also: Debugging GraphQL issues</p>"},{"location":"API_OLD/#curl-command","title":"<code>curl</code> Command","text":"<p>You can use the following <code>curl</code> command to execute the query.</p> <pre><code>curl 'http://host:GRAPHQL_PORT/graphql' -X POST -H 'Authorization: Bearer API_TOKEN' -H 'Content-Type: application/json' --data '{\n \"query\": \"query GetDevices($options: PageQueryOptionsInput) { devices(options: $options) { devices { rowid devMac devName devOwner devType devVendor devLastConnection devStatus } count } }\",\n \"variables\": {\n \"options\": {\n \"page\": 1,\n \"limit\": 10,\n \"sort\": [{ \"field\": \"devName\", \"order\": \"asc\" }],\n \"search\": \"\",\n \"status\": \"connected\"\n }\n }\n }'\n</code></pre>"},{"location":"API_OLD/#explanation","title":"Explanation:","text":"<ol> <li>GraphQL Query:</li> <li>The <code>query</code> parameter contains the GraphQL query as a string.</li> <li> <p>The <code>variables</code> parameter contains the input variables for the query.</p> </li> <li> <p>Query Variables:</p> </li> <li><code>page</code>: Specifies the page number of results to fetch.</li> <li><code>limit</code>: Specifies the number of results per page.</li> <li><code>sort</code>: Specifies the sorting options, with <code>field</code> being the field to sort by and <code>order</code> being the sort order (<code>asc</code> for ascending or <code>desc</code> for descending).</li> <li><code>search</code>: A search term to filter the devices.</li> <li> <p><code>status</code>: The status filter to apply (valid values are <code>my_devices</code> (determined by the <code>UI_MY_DEVICES</code> setting), <code>connected</code>, <code>favorites</code>, <code>new</code>, <code>down</code>, <code>archived</code>, <code>offline</code>).</p> </li> <li> <p><code>curl</code> Command:</p> </li> <li>The <code>-X POST</code> option specifies that we are making a POST request.</li> <li>The <code>-H \"Content-Type: application/json\"</code> option sets the content type of the request to JSON.</li> <li>The <code>-d</code> option provides the request payload, which includes the GraphQL query and variables.</li> </ol>"},{"location":"API_OLD/#sample-response","title":"Sample Response","text":"<p>The response will be in JSON format, similar to the following:</p> <pre><code>{\n \"data\": {\n \"devices\": {\n \"devices\": [\n {\n \"rowid\": 1,\n \"devMac\": \"00:11:22:33:44:55\",\n \"devName\": \"Device 1\",\n \"devOwner\": \"Owner 1\",\n \"devType\": \"Type 1\",\n \"devVendor\": \"Vendor 1\",\n \"devLastConnection\": \"2025-01-01T00:00:00Z\",\n \"devStatus\": \"connected\"\n },\n {\n \"rowid\": 2,\n \"devMac\": \"66:77:88:99:AA:BB\",\n \"devName\": \"Device 2\",\n \"devOwner\": \"Owner 2\",\n \"devType\": \"Type 2\",\n \"devVendor\": \"Vendor 2\",\n \"devLastConnection\": \"2025-01-02T00:00:00Z\",\n \"devStatus\": \"connected\"\n }\n ],\n \"count\": 2\n }\n }\n}\n</code></pre>"},{"location":"API_OLD/#api-endpoint-json-files","title":"API Endpoint: JSON files","text":"<p>This API endpoint retrieves static files, that are periodically updated.</p> <ul> <li>Endpoint URL: <code>php/server/query_json.php?file=&lt;file name&gt;</code></li> <li>Host: <code>same as front end (web ui)</code></li> <li>Port: <code>20211</code> or as defined by the $PORT docker environment variable (same as the port for the web ui)</li> </ul>"},{"location":"API_OLD/#when-are-the-endpoints-updated","title":"When are the endpoints updated","text":"<p>The endpoints are updated when objects in the API endpoints are changed.</p>"},{"location":"API_OLD/#location-of-the-endpoints","title":"Location of the endpoints","text":"<p>In the container, these files are located under the API directory (default: <code>/tmp/api/</code>, configurable via <code>NETALERTX_API</code> environment variable). You can access them via the <code>/php/server/query_json.php?file=user_notifications.json</code> endpoint.</p>"},{"location":"API_OLD/#available-endpoints","title":"Available endpoints","text":"<p>You can access the following files:</p> File name Description <code>notification_json_final.json</code> The json version of the last notification (e.g. used for webhooks - sample JSON). <code>table_devices.json</code> All of the available Devices detected by the app. <code>table_plugins_events.json</code> The list of the unprocessed (pending) notification events (plugins_events DB table). <code>table_plugins_history.json</code> The list of notification events history. <code>table_plugins_objects.json</code> The content of the plugins_objects table. Find more info on the Plugin system here <code>language_strings.json</code> The content of the language_strings table, which in turn is loaded from the plugins <code>config.json</code> definitions. <code>table_custom_endpoint.json</code> A custom endpoint generated by the SQL query specified by the <code>API_CUSTOM_SQL</code> setting. <code>table_settings.json</code> The content of the settings table. <code>app_state.json</code> Contains the current application state."},{"location":"API_OLD/#json-data-format","title":"JSON Data format","text":"<p>The endpoints starting with the <code>table_</code> prefix contain most, if not all, data contained in the corresponding database table. The common format for those is:</p> <pre><code>{\n \"data\": [\n {\n \"db_column_name\": \"data\",\n \"db_column_name2\": \"data2\"\n },\n {\n \"db_column_name\": \"data3\",\n \"db_column_name2\": \"data4\"\n }\n ]\n}\n</code></pre> <p>Example JSON of the <code>table_devices.json</code> endpoint with two Devices (database rows):</p> <pre><code>{\n \"data\": [\n {\n \"devMac\": \"Internet\",\n \"devName\": \"Net - Huawei\",\n \"devType\": \"Router\",\n \"devVendor\": null,\n \"devGroup\": \"Always on\",\n \"devFirstConnection\": \"2021-01-01 00:00:00\",\n \"devLastConnection\": \"2021-01-28 22:22:11\",\n \"devLastIP\": \"192.168.1.24\",\n \"devStaticIP\": 0,\n \"devPresentLastScan\": 1,\n \"devLastNotification\": \"2023-01-28 22:22:28.998715\",\n \"devIsNew\": 0,\n \"devParentMAC\": \"\",\n \"devParentPort\": \"\",\n \"devIcon\": \"globe\"\n },\n {\n \"devMac\": \"a4:8f:ff:aa:ba:1f\",\n \"devName\": \"Net - USG\",\n \"devType\": \"Firewall\",\n \"devVendor\": \"Ubiquiti Inc\",\n \"devGroup\": \"\",\n \"devFirstConnection\": \"2021-02-12 22:05:00\",\n \"devLastConnection\": \"2021-07-17 15:40:00\",\n \"devLastIP\": \"192.168.1.1\",\n \"devStaticIP\": 1,\n \"devPresentLastScan\": 1,\n \"devLastNotification\": \"2021-07-17 15:40:10.667717\",\n \"devIsNew\": 0,\n \"devParentMAC\": \"Internet\",\n \"devParentPort\": 1,\n \"devIcon\": \"shield-halved\"\n }\n ]\n}\n</code></pre>"},{"location":"API_OLD/#api-endpoint-prometheus-exporter","title":"API Endpoint: Prometheus Exporter","text":"<ul> <li>Endpoint URL: <code>/metrics</code></li> <li>Host: (where NetAlertX exporter is running)</li> <li>Port: as configured in the <code>GRAPHQL_PORT</code> setting (<code>20212</code> by default)</li> </ul>"},{"location":"API_OLD/#example-output-of-the-metrics-endpoint","title":"Example Output of the <code>/metrics</code> Endpoint","text":"<p>Below is a representative snippet of the metrics you may find when querying the <code>/metrics</code> endpoint for <code>netalertx</code>. It includes both aggregate counters and <code>device_status</code> labels per device.</p> <pre><code>netalertx_connected_devices 31\nnetalertx_offline_devices 54\nnetalertx_down_devices 0\nnetalertx_new_devices 0\nnetalertx_archived_devices 31\nnetalertx_favorite_devices 2\nnetalertx_my_devices 54\n\nnetalertx_device_status{device=\"Net - Huawei\", mac=\"Internet\", ip=\"1111.111.111.111\", vendor=\"None\", first_connection=\"2021-01-01 00:00:00\", last_connection=\"2025-08-04 17:57:00\", dev_type=\"Router\", device_status=\"Online\"} 1\nnetalertx_device_status{device=\"Net - USG\", mac=\"74:ac:74:ac:74:ac\", ip=\"192.168.1.1\", vendor=\"Ubiquiti Networks Inc.\", first_connection=\"2022-02-12 22:05:00\", last_connection=\"2025-06-07 08:16:49\", dev_type=\"Firewall\", device_status=\"Archived\"} 1\nnetalertx_device_status{device=\"Raspberry Pi 4 LAN\", mac=\"74:ac:74:ac:74:74\", ip=\"192.168.1.9\", vendor=\"Raspberry Pi Trading Ltd\", first_connection=\"2022-02-12 22:05:00\", last_connection=\"2025-08-04 17:57:00\", dev_type=\"Singleboard Computer (SBC)\", device_status=\"Online\"} 1\n...\n</code></pre>"},{"location":"API_OLD/#metrics-explanation","title":"Metrics Explanation","text":""},{"location":"API_OLD/#1-aggregate-device-counts","title":"1. Aggregate Device Counts","text":"<p>Metric names prefixed with <code>netalertx_</code> provide aggregated counts by device status:</p> <ul> <li><code>netalertx_connected_devices</code>: number of devices currently connected</li> <li><code>netalertx_offline_devices</code>: devices currently offline</li> <li><code>netalertx_down_devices</code>: down/unreachable devices</li> <li><code>netalertx_new_devices</code>: devices recently detected</li> <li><code>netalertx_archived_devices</code>: archived devices</li> <li><code>netalertx_favorite_devices</code>: user-marked favorite devices</li> <li><code>netalertx_my_devices</code>: devices associated with the current user context</li> </ul> <p>These numeric values give a high-level overview of device distribution.</p>"},{"location":"API_OLD/#2-perdevice-status-with-labels","title":"2. Per\u2011Device Status with Labels","text":"<p>Each individual device is represented by a <code>netalertx_device_status</code> metric, with descriptive labels:</p> <ul> <li><code>device</code>: friendly name of the device</li> <li><code>mac</code>: MAC address (or placeholder)</li> <li><code>ip</code>: last recorded IP address</li> <li><code>vendor</code>: manufacturer or \"None\" if unknown</li> <li><code>first_connection</code>: timestamp when the device was first observed</li> <li><code>last_connection</code>: most recent contact timestamp</li> <li><code>dev_type</code>: device category or type</li> <li><code>device_status</code>: current status (Online / Offline / Archived / Down / ...)</li> </ul> <p>The metric value is always <code>1</code> (indicating presence or active state) and the combination of labels identifies the device.</p>"},{"location":"API_OLD/#how-to-query-with-curl","title":"How to Query with <code>curl</code>","text":"<p>To fetch the metrics from the NetAlertX exporter:</p> <pre><code>curl 'http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/metrics' \\\n -H 'Authorization: Bearer &lt;API_TOKEN&gt;' \\\n -H 'Accept: text/plain'\n</code></pre> <p>Replace:</p> <ul> <li><code>&lt;server_ip&gt;</code>: IP or hostname of the NetAlertX server</li> <li><code>&lt;GRAPHQL_PORT&gt;</code>: port specified in your <code>GRAPHQL_PORT</code> setting (default: <code>20212</code>)</li> <li><code>&lt;API_TOKEN&gt;</code> your Bearer token from the <code>API_TOKEN</code> setting</li> </ul>"},{"location":"API_OLD/#summary","title":"Summary","text":"<ul> <li>Endpoint: <code>/metrics</code> provides both summary counters and per-device status entries.</li> <li>Aggregate metrics help monitor overall device states.</li> <li>Detailed metrics expose each device\u2019s metadata via labels.</li> <li>Use case: feed into Prometheus for scraping, monitoring, alerting, or charting dashboard views.</li> </ul>"},{"location":"API_OLD/#prometheus-scraping-configuration","title":"Prometheus Scraping Configuration","text":"<pre><code>scrape_configs:\n - job_name: 'netalertx'\n metrics_path: /metrics\n scheme: http\n scrape_interval: 60s\n static_configs:\n - targets: ['&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;']\n authorization:\n type: Bearer\n credentials: &lt;API_TOKEN&gt;\n</code></pre>"},{"location":"API_OLD/#grafana-template","title":"Grafana template","text":"<p>Grafana template sample: Download json</p>"},{"location":"API_OLD/#api-endpoint-log-files","title":"API Endpoint: /log files","text":"<p>This API endpoint retrieves files from the <code>/tmp/log</code> folder.</p> <ul> <li>Endpoint URL: <code>php/server/query_logs.php?file=&lt;file name&gt;</code></li> <li>Host: <code>same as front end (web ui)</code></li> <li>Port: <code>20211</code> or as defined by the $PORT docker environment variable (same as the port for the web ui)</li> </ul> File Description <code>IP_changes.log</code> Logs of IP address changes <code>app.log</code> Main application log <code>app.php_errors.log</code> PHP error log <code>app_front.log</code> Frontend application log <code>app_nmap.log</code> Logs of Nmap scan results <code>db_is_locked.log</code> Logs when the database is locked <code>execution_queue.log</code> Logs of execution queue activities <code>plugins/</code> Directory for temporary plugin-related files (not accessible) <code>report_output.html</code> HTML report output <code>report_output.json</code> JSON format report output <code>report_output.txt</code> Text format report output <code>stderr.log</code> Logs of standard error output <code>stdout.log</code> Logs of standard output"},{"location":"API_OLD/#api-endpoint-config-files","title":"API Endpoint: /config files","text":"<p>To retrieve files from the <code>/data/config</code> folder.</p> <ul> <li>Endpoint URL: <code>php/server/query_config.php?file=&lt;file name&gt;</code></li> <li>Host: <code>same as front end (web ui)</code></li> <li>Port: <code>20211</code> or as defined by the $PORT docker environment variable (same as the port for the web ui)</li> </ul> File Description <code>devices.csv</code> Devices csv file <code>app.conf</code> Application config file"},{"location":"API_ONLINEHISTORY/","title":"Online History API Endpoints","text":"<p>Manage the online history records of devices. Currently, the API supports deletion of all history entries. All endpoints require authorization.</p>"},{"location":"API_ONLINEHISTORY/#1-delete-online-history","title":"1. Delete Online History","text":"<ul> <li>DELETE <code>/history</code> Remove all records from the online history table (<code>Online_History</code>). This operation cannot be undone.</li> </ul> <p>Response (success):</p> <pre><code>{\n \"success\": true,\n \"message\": \"Deleted online history\"\n}\n</code></pre> <p>Error Responses:</p> <ul> <li>Unauthorized \u2192 HTTP 403</li> </ul>"},{"location":"API_ONLINEHISTORY/#example-curl-request","title":"Example <code>curl</code> Request","text":"<pre><code>curl -X DELETE \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/history\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\"\n</code></pre>"},{"location":"API_SESSIONS/","title":"Sessions API Endpoints","text":"<p>Track and manage device connection sessions. Sessions record when a device connects or disconnects on the network.</p>"},{"location":"API_SESSIONS/#create-a-session","title":"Create a Session","text":"<ul> <li>POST <code>/sessions/create</code> \u2192 Create a new session for a device</li> </ul> <p>Request Body:</p> <pre><code>{\n \"mac\": \"AA:BB:CC:DD:EE:FF\",\n \"ip\": \"192.168.1.10\",\n \"start_time\": \"2025-08-01T10:00:00\",\n \"end_time\": \"2025-08-01T12:00:00\", // optional\n \"event_type_conn\": \"Connected\", // optional, default \"Connected\"\n \"event_type_disc\": \"Disconnected\" // optional, default \"Disconnected\"\n}\n</code></pre> <p>Response:</p> <pre><code>{\n \"success\": true,\n \"message\": \"Session created for MAC AA:BB:CC:DD:EE:FF\"\n}\n</code></pre>"},{"location":"API_SESSIONS/#curl-example","title":"<code>curl</code> Example","text":"<pre><code>curl -X POST \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/sessions/create\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Accept: application/json\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"mac\": \"AA:BB:CC:DD:EE:FF\",\n \"ip\": \"192.168.1.10\",\n \"start_time\": \"2025-08-01T10:00:00\",\n \"end_time\": \"2025-08-01T12:00:00\",\n \"event_type_conn\": \"Connected\",\n \"event_type_disc\": \"Disconnected\"\n }'\n</code></pre>"},{"location":"API_SESSIONS/#delete-sessions","title":"Delete Sessions","text":"<ul> <li>DELETE <code>/sessions/delete</code> \u2192 Delete all sessions for a given MAC</li> </ul> <p>Request Body:</p> <pre><code>{\n \"mac\": \"AA:BB:CC:DD:EE:FF\"\n}\n</code></pre> <p>Response:</p> <pre><code>{\n \"success\": true,\n \"message\": \"Deleted sessions for MAC AA:BB:CC:DD:EE:FF\"\n}\n</code></pre>"},{"location":"API_SESSIONS/#curl-example_1","title":"<code>curl</code> Example","text":"<pre><code>curl -X DELETE \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/sessions/delete\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Accept: application/json\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"mac\": \"AA:BB:CC:DD:EE:FF\"\n }'\n</code></pre>"},{"location":"API_SESSIONS/#list-sessions","title":"List Sessions","text":"<ul> <li>GET <code>/sessions/list</code> \u2192 Retrieve sessions optionally filtered by device and date range</li> </ul> <p>Query Parameters:</p> <ul> <li><code>mac</code> (optional) \u2192 Filter by device MAC address</li> <li><code>start_date</code> (optional) \u2192 Filter sessions starting from this date (<code>YYYY-MM-DD</code>)</li> <li><code>end_date</code> (optional) \u2192 Filter sessions ending by this date (<code>YYYY-MM-DD</code>)</li> </ul> <p>Example:</p> <pre><code>/sessions/list?mac=AA:BB:CC:DD:EE:FF&amp;start_date=2025-08-01&amp;end_date=2025-08-21\n</code></pre> <p>Response:</p> <pre><code>{\n \"success\": true,\n \"sessions\": [\n {\n \"ses_MAC\": \"AA:BB:CC:DD:EE:FF\",\n \"ses_Connection\": \"2025-08-01 10:00\",\n \"ses_Disconnection\": \"2025-08-01 12:00\",\n \"ses_Duration\": \"2h 0m\",\n \"ses_IP\": \"192.168.1.10\",\n \"ses_Info\": \"\"\n }\n ]\n}\n</code></pre>"},{"location":"API_SESSIONS/#curl-example_2","title":"<code>curl</code> Example","text":"<p>get sessions for mac</p> <pre><code>curl -X GET \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/sessions/list?mac=AA:BB:CC:DD:EE:FF&amp;start_date=2025-08-01&amp;end_date=2025-08-21\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Accept: application/json\"\n</code></pre>"},{"location":"API_SESSIONS/#calendar-view-of-sessions","title":"Calendar View of Sessions","text":"<ul> <li>GET <code>/sessions/calendar</code> \u2192 View sessions in calendar format</li> </ul> <p>Query Parameters:</p> <ul> <li><code>start</code> \u2192 Start date (<code>YYYY-MM-DD</code>)</li> <li><code>end</code> \u2192 End date (<code>YYYY-MM-DD</code>)</li> </ul> <p>Example:</p> <pre><code>/sessions/calendar?start=2025-08-01&amp;end=2025-08-21\n</code></pre> <p>Response:</p> <pre><code>{\n \"success\": true,\n \"sessions\": [\n {\n \"resourceId\": \"AA:BB:CC:DD:EE:FF\",\n \"title\": \"\",\n \"start\": \"2025-08-01T10:00:00\",\n \"end\": \"2025-08-01T12:00:00\",\n \"color\": \"#00a659\",\n \"tooltip\": \"Connection: 2025-08-01 10:00\\nDisconnection: 2025-08-01 12:00\\nIP: 192.168.1.10\",\n \"className\": \"no-border\"\n }\n ]\n}\n</code></pre>"},{"location":"API_SESSIONS/#curl-example_3","title":"<code>curl</code> Example","text":"<pre><code>curl -X GET \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/sessions/calendar?start=2025-08-01&amp;end=2025-08-21\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Accept: application/json\"\n</code></pre>"},{"location":"API_SESSIONS/#device-sessions","title":"Device Sessions","text":"<ul> <li>GET <code>/sessions/&lt;mac&gt;</code> \u2192 Retrieve sessions for a specific device</li> </ul> <p>Query Parameters:</p> <ul> <li><code>period</code> \u2192 Period to retrieve sessions (<code>1 day</code>, <code>7 days</code>, <code>1 month</code>, etc.) Default: <code>1 day</code></li> </ul> <p>Example:</p> <pre><code>/sessions/AA:BB:CC:DD:EE:FF?period=7 days\n</code></pre> <p>Response:</p> <pre><code>{\n \"success\": true,\n \"sessions\": [\n {\n \"ses_MAC\": \"AA:BB:CC:DD:EE:FF\",\n \"ses_Connection\": \"2025-08-01 10:00\",\n \"ses_Disconnection\": \"2025-08-01 12:00\",\n \"ses_Duration\": \"2h 0m\",\n \"ses_IP\": \"192.168.1.10\",\n \"ses_Info\": \"\"\n }\n ]\n}\n</code></pre>"},{"location":"API_SESSIONS/#curl-example_4","title":"<code>curl</code> Example","text":"<pre><code>curl -X GET \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/sessions/AA:BB:CC:DD:EE:FF?period=7%20days\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Accept: application/json\"\n</code></pre>"},{"location":"API_SESSIONS/#session-events-summary","title":"Session Events Summary","text":"<ul> <li>GET <code>/sessions/session-events</code> \u2192 Retrieve a summary of session events</li> </ul> <p>Query Parameters:</p> <ul> <li><code>type</code> \u2192 Event type (<code>all</code>, <code>sessions</code>, <code>missing</code>, <code>voided</code>, <code>new</code>, <code>down</code>) Default: <code>all</code></li> <li><code>period</code> \u2192 Period to retrieve events (<code>7 days</code>, <code>1 month</code>, etc.)</li> </ul> <p>Example:</p> <pre><code>/sessions/session-events?type=all&amp;period=7 days\n</code></pre> <p>Response: Returns a list of events or sessions with formatted connection, disconnection, duration, and IP information.</p>"},{"location":"API_SESSIONS/#curl-example_5","title":"<code>curl</code> Example","text":"<pre><code>curl -X GET \"http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/sessions/session-events?type=all&amp;period=7%20days\" \\\n -H \"Authorization: Bearer &lt;API_TOKEN&gt;\" \\\n -H \"Accept: application/json\"\n</code></pre>"},{"location":"API_SETTINGS/","title":"Settings API Endpoints","text":"<p>Retrieve application settings stored in the configuration system. This endpoint is useful for quickly fetching individual settings such as <code>API_TOKEN</code> or <code>TIMEZONE</code>.</p> <p>For bulk or structured access (all settings, schema details, or filtering), use the GraphQL API Endpoint.</p>"},{"location":"API_SETTINGS/#get-a-setting","title":"Get a Setting","text":"<ul> <li>GET <code>/settings/&lt;key&gt;</code> \u2192 Retrieve the value of a specific setting</li> </ul> <p>Path Parameter:</p> <ul> <li><code>key</code> \u2192 The setting key to retrieve (e.g., <code>API_TOKEN</code>, <code>TIMEZONE</code>)</li> </ul> <p>Authorization: Requires a valid API token in the <code>Authorization</code> header.</p>"},{"location":"API_SETTINGS/#curl-example-success","title":"<code>curl</code> Example (Success)","text":"<pre><code>curl 'http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/settings/API_TOKEN' \\\n -H 'Authorization: Bearer &lt;API_TOKEN&gt;' \\\n -H 'Accept: application/json'\n</code></pre> <p>Response:</p> <pre><code>{\n \"success\": true,\n \"value\": \"my-secret-token\"\n}\n</code></pre>"},{"location":"API_SETTINGS/#curl-example-invalid-key","title":"<code>curl</code> Example (Invalid Key)","text":"<pre><code>curl 'http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/settings/DOES_NOT_EXIST' \\\n -H 'Authorization: Bearer &lt;API_TOKEN&gt;' \\\n -H 'Accept: application/json'\n</code></pre> <p>Response:</p> <pre><code>{\n \"success\": true,\n \"value\": null\n}\n</code></pre>"},{"location":"API_SETTINGS/#curl-example-unauthorized","title":"<code>curl</code> Example (Unauthorized)","text":"<pre><code>curl 'http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/settings/API_TOKEN' \\\n -H 'Accept: application/json'\n</code></pre> <p>Response:</p> <pre><code>{\n \"error\": \"Forbidden\"\n}\n</code></pre>"},{"location":"API_SETTINGS/#notes","title":"Notes","text":"<ul> <li>This endpoint is optimized for direct retrieval of a single setting.</li> <li>For complex retrieval scenarios (listing all settings, retrieving schema metadata like <code>setName</code>, <code>setDescription</code>, <code>setType</code>, or checking if a setting is overridden by environment variables), use the GraphQL Settings Query:</li> </ul> <pre><code>curl 'http://&lt;server_ip&gt;:&lt;GRAPHQL_PORT&gt;/graphql' \\\n -X POST \\\n -H 'Authorization: Bearer &lt;API_TOKEN&gt;' \\\n -H 'Content-Type: application/json' \\\n --data '{\n \"query\": \"query GetSettings { settings { settings { setKey setName setDescription setType setOptions setGroup setValue setEvents setOverriddenByEnv } count } }\"\n }'\n</code></pre> <p>See the GraphQL API Endpoint for more details.</p>"},{"location":"API_SSE/","title":"SSE (Server-Sent Events)","text":"<p>Real-time app state updates via Server-Sent Events. Reduces server load ~95% vs polling.</p>"},{"location":"API_SSE/#endpoints","title":"Endpoints","text":"Endpoint Method Purpose <code>/sse/state</code> GET Stream state updates (requires Bearer token) <code>/sse/stats</code> GET Debug: connected clients, queued events"},{"location":"API_SSE/#usage","title":"Usage","text":""},{"location":"API_SSE/#connect-to-sse-stream","title":"Connect to SSE Stream","text":"<pre><code>curl -H \"Authorization: Bearer YOUR_API_TOKEN\" \\\n http://localhost:5000/sse/state\n</code></pre>"},{"location":"API_SSE/#check-connection-stats","title":"Check Connection Stats","text":"<pre><code>curl -H \"Authorization: Bearer YOUR_API_TOKEN\" \\\n http://localhost:5000/sse/stats\n</code></pre>"},{"location":"API_SSE/#event-types","title":"Event Types","text":"<ul> <li><code>state_update</code> - App state changed (e.g., \"Scanning\", \"Processing\")</li> <li><code>unread_notifications_count_update</code> - Number of unread notifications changed (count: int)</li> </ul>"},{"location":"API_SSE/#backend-integration","title":"Backend Integration","text":"<p>Broadcasts automatically triggered in <code>app_state.py</code> via <code>broadcast_state_update()</code>:</p> <pre><code>from api_server.sse_broadcast import broadcast_state_update\n\n# Called on every state change - no additional code needed\nbroadcast_state_update(current_state=\"Scanning\", settings_imported=time.time())\n</code></pre>"},{"location":"API_SSE/#frontend-integration","title":"Frontend Integration","text":"<p>Auto-enabled via <code>sse_manager.js</code>:</p> <pre><code>// In browser console:\nnetAlertXStateManager.getStats().then(stats =&gt; {\n console.log(\"Connected clients:\", stats.connected_clients);\n});\n</code></pre>"},{"location":"API_SSE/#fallback-behavior","title":"Fallback Behavior","text":"<ul> <li>If SSE fails after 3 attempts, automatically switches to polling</li> <li>Polling starts at 1s, backs off to 30s max</li> <li>No user-visible difference in functionality</li> </ul>"},{"location":"API_SSE/#files","title":"Files","text":"File Purpose <code>server/api_server/sse_endpoint.py</code> SSE endpoints &amp; event queue <code>server/api_server/sse_broadcast.py</code> Broadcast helper functions <code>front/js/sse_manager.js</code> Client-side SSE connection manager"},{"location":"API_SSE/#troubleshooting","title":"Troubleshooting","text":"Issue Solution Connection refused Check backend running, API token correct No events received Verify <code>broadcast_state_update()</code> is called on state changes High memory Events not processed fast enough, check client logs Using polling instead of SSE Normal fallback - check browser console for errors"},{"location":"API_SYNC/","title":"Sync API Endpoint","text":"<p>The <code>/sync</code> endpoint is used by the SYNC plugin to synchronize data between multiple NetAlertX instances (e.g., from a node to a hub). It supports both GET and POST requests.</p>"},{"location":"API_SYNC/#91-get-sync","title":"9.1 GET <code>/sync</code>","text":"<p>Fetches data from a node to the hub. The data is returned as a base64-encoded JSON file.</p> <p>Example Request:</p> <pre><code>curl 'http://&lt;server&gt;:&lt;GRAPHQL_PORT&gt;/sync' \\\n -H 'Authorization: Bearer &lt;API_TOKEN&gt;'\n</code></pre> <p>Response Example:</p> <pre><code>{\n \"node_name\": \"NODE-01\",\n \"status\": 200,\n \"message\": \"OK\",\n \"data_base64\": \"eyJkZXZpY2VzIjogW3siZGV2TWFjIjogIjAwOjExOjIyOjMzOjQ0OjU1IiwiZGV2TmFtZSI6ICJEZXZpY2UgMSJ9XSwgImNvdW50Ijog1fQ==\",\n \"timestamp\": \"2025-08-24T10:15:00+10:00\"\n}\n</code></pre> <p>Notes:</p> <ul> <li><code>data_base64</code> contains the full JSON data encoded in Base64.</li> <li><code>node_name</code> corresponds to the <code>SYNC_node_name</code> setting on the node.</li> <li>Errors (e.g., missing file) return HTTP 500 with an error message.</li> </ul>"},{"location":"API_SYNC/#92-post-sync","title":"9.2 POST <code>/sync</code>","text":"<p>The POST endpoint is used by nodes to send data to the hub. The hub expects the data as form-encoded fields (application/x-www-form-urlencoded or multipart/form-data). The hub then stores the data in the plugin log folder for processing.</p>"},{"location":"API_SYNC/#required-fields","title":"Required Fields","text":"Field Type Description <code>data</code> string The payload from the plugin or devices. Typically plain text, JSON, or encrypted Base64 data. In your Python script, <code>encrypt_data()</code> is applied before sending. <code>node_name</code> string The name of the node sending the data. Matches the node\u2019s <code>SYNC_node_name</code> setting. Used to generate the filename on the hub. <code>plugin</code> string The name of the plugin sending the data. Determines the filename prefix (<code>last_result.&lt;plugin&gt;...</code>). <code>file_path</code> string (optional) Path of the local file being sent. Used only for logging/debugging purposes on the hub; not required for processing."},{"location":"API_SYNC/#how-the-hub-processes-the-post-data","title":"How the Hub Processes the POST Data","text":"<ol> <li>Receives the data and validates the API token.</li> <li>Stores the raw payload in:</li> </ol> <pre><code>INSTALL_PATH/log/plugins/last_result.&lt;plugin&gt;.encoded.&lt;node_name&gt;.&lt;sequence&gt;.log\n</code></pre> <ul> <li><code>&lt;plugin&gt;</code> \u2192 plugin name from the POST request.</li> <li><code>&lt;node_name&gt;</code> \u2192 node name from the POST request.</li> <li> <p><code>&lt;sequence&gt;</code> \u2192 incremented number for each submission.</p> </li> <li> <p>Decodes / decrypts the data if necessary (Base64 or encrypted) before processing.</p> </li> <li> <p>Processes JSON payloads (e.g., device info) to:</p> </li> <li> <p>Avoid duplicates by tracking <code>devMac</code>.</p> </li> <li>Add metadata like <code>devSyncHubNode</code>.</li> <li>Insert new devices into the database.</li> <li>Renames files to indicate they have been processed:</li> </ul> <pre><code>processed_last_result.&lt;plugin&gt;.&lt;node_name&gt;.&lt;sequence&gt;.log\n</code></pre>"},{"location":"API_SYNC/#example-post-payload","title":"Example POST Payload","text":"<p>If a node is sending device data:</p> <pre><code>curl -X POST 'http://&lt;hub&gt;:&lt;PORT&gt;/sync' \\\n -H 'Authorization: Bearer &lt;API_TOKEN&gt;' \\\n -F 'data={\"data\":[{\"devMac\":\"00:11:22:33:44:55\",\"devName\":\"Device 1\",\"devVendor\":\"Vendor A\",\"devLastIP\":\"192.168.1.10\"}]}' \\\n -F 'node_name=NODE-01' \\\n -F 'plugin=SYNC'\n</code></pre> <ul> <li>The <code>data</code> field contains JSON with a <code>data</code> array, where each element is a device object or plugin data object.</li> <li>The <code>plugin</code> and <code>node_name</code> fields allow the hub to organize and store the file correctly.</li> <li>The data is only processed if the relevant plugins are enabled and run on the target server. </li> </ul>"},{"location":"API_SYNC/#key-notes","title":"Key Notes","text":"<ul> <li>Always use the same <code>plugin</code> and <code>node_name</code> values for consistent storage.</li> <li>Encrypted data: The Python script uses <code>encrypt_data()</code> before sending, and the hub decodes it before processing.</li> <li>Sequence numbers: Every submission generates a new sequence, preventing overwriting previous data.</li> <li>Form-encoded: The hub expects <code>multipart/form-data</code> (cURL <code>-F</code>) or <code>application/x-www-form-urlencoded</code>.</li> </ul> <p>Storage Details:</p> <ul> <li>Data is stored under <code>INSTALL_PATH/log/plugins</code> with filenames following the pattern:</li> </ul> <pre><code>last_result.&lt;plugin&gt;.encoded.&lt;node_name&gt;.&lt;sequence&gt;.log\n</code></pre> <ul> <li>Both encoded and decoded files are tracked, and new submissions increment the sequence number.</li> <li>If storing fails, the API returns HTTP 500 with an error message.</li> <li>The data is only processed if the relevant plugins are enabled and run on the target server. </li> </ul>"},{"location":"API_SYNC/#93-notes-and-best-practices","title":"9.3 Notes and Best Practices","text":"<ul> <li>Authorization Required \u2013 Both GET and POST require a valid API token.</li> <li>Data Integrity \u2013 Ensure that <code>node_name</code> and <code>plugin</code> are consistent to avoid overwriting files.</li> <li>Monitoring \u2013 Notifications are generated whenever data is sent or received (<code>write_notification</code>), which can be used for alerting or auditing.</li> <li>Use Case \u2013 Typically used in multi-node deployments to consolidate device and event data on a central hub.</li> </ul>"},{"location":"API_TESTS/","title":"Tests","text":""},{"location":"API_TESTS/#unit-tests","title":"Unit Tests","text":"<p>Warning</p> <p>Please note these test modify data in the database.</p> <ol> <li>See the <code>/test</code> directory for available test cases. These are not exhaustive but cover the main API endpoints. </li> <li>To run a test case, SSH into the container: <code>sudo docker exec -it netalertx /bin/bash</code> </li> <li>Inside the container, install pytest (if not already installed): <code>pip install pytest</code> </li> <li>Run a specific test case: <code>pytest /app/test/TESTFILE.py</code></li> </ol>"},{"location":"AUTHELIA/","title":"Authelia","text":""},{"location":"AUTHELIA/#authelia-support","title":"Authelia support","text":"<p>Note</p> <p>This is community-contributed. Due to environment, setup, or networking differences, results may vary. Please open a PR to improve it instead of creating an issue, as the maintainer is not actively maintaining it.</p> <pre><code>theme: dark\n\ndefault_2fa_method: \"totp\"\n\nserver:\n address: 0.0.0.0:9091\n endpoints:\n enable_expvars: false\n enable_pprof: false\n authz:\n forward-auth:\n implementation: 'ForwardAuth'\n authn_strategies:\n - name: 'HeaderAuthorization'\n schemes:\n - 'Basic'\n - name: 'CookieSession'\n ext-authz:\n implementation: 'ExtAuthz'\n authn_strategies:\n - name: 'HeaderAuthorization'\n schemes:\n - 'Basic'\n - name: 'CookieSession'\n auth-request:\n implementation: 'AuthRequest'\n authn_strategies:\n - name: 'HeaderAuthRequestProxyAuthorization'\n schemes:\n - 'Basic'\n - name: 'CookieSession'\n legacy:\n implementation: 'Legacy'\n authn_strategies:\n - name: 'HeaderLegacy'\n - name: 'CookieSession'\n disable_healthcheck: false\n tls:\n key: \"\"\n certificate: \"\"\n client_certificates: []\n headers:\n csp_template: \"\"\n\nlog:\n ## Level of verbosity for logs: info, debug, trace.\n level: info\n\n###############################################################\n# The most important section\n###############################################################\naccess_control:\n ## Default policy can either be 'bypass', 'one_factor', 'two_factor' or 'deny'.\n default_policy: deny\n networks:\n - name: internal\n networks:\n - '192.168.0.0/18'\n - '10.10.10.0/8' # Zerotier\n - name: private\n networks:\n - '172.16.0.0/12'\n rules:\n - networks:\n - private\n domain:\n - '*'\n policy: bypass\n - networks:\n - internal\n domain:\n - '*'\n policy: bypass\n - domain:\n # exclude itself from auth, should not happen as we use Traefik middleware on a case-by-case screnario\n - 'auth.MYDOMAIN1.TLD'\n - 'authelia.MYDOMAIN1.TLD'\n - 'auth.MYDOMAIN2.TLD'\n - 'authelia.MYDOMAIN2.TLD'\n policy: bypass\n - domain:\n #All subdomains match\n - 'MYDOMAIN1.TLD'\n - '*.MYDOMAIN1.TLD'\n policy: two_factor\n - domain:\n # This will not work yet as Authelio does not support multi-domain authentication\n - 'MYDOMAIN2.TLD'\n - '*.MYDOMAIN2.TLD'\n policy: two_factor\n\n\n############################################################\nidentity_validation:\n reset_password:\n jwt_secret: \"[REDACTED]\"\n\nidentity_providers:\n oidc:\n enable_client_debug_messages: true\n enforce_pkce: public_clients_only\n hmac_secret: [REDACTED]\n lifespans:\n authorize_code: 1m\n id_token: 1h\n refresh_token: 90m\n access_token: 1h\n cors:\n endpoints:\n - authorization\n - token\n - revocation\n - introspection\n - userinfo\n allowed_origins:\n - \"*\"\n allowed_origins_from_client_redirect_uris: false\n jwks:\n - key: [REDACTED]\n certificate_chain:\n clients:\n - client_id: portainer\n client_name: Portainer\n # generate secret with \"authelia crypto hash generate pbkdf2 --random --random.length 32 --random.charset alphanumeric\"\n # Random Password: [REDACTED]\n # Digest: [REDACTED]\n client_secret: [REDACTED]\n token_endpoint_auth_method: 'client_secret_post'\n public: false\n authorization_policy: two_factor\n consent_mode: pre-configured #explicit\n pre_configured_consent_duration: '6M' #Must be re-authorised every 6 Months\n scopes:\n - openid\n #- groups #Currently not supported in Authelia V\n - email\n - profile\n redirect_uris:\n - https://portainer.MYDOMAIN1.LTD\n userinfo_signed_response_alg: none\n\n - client_id: openproject\n client_name: OpenProject\n # generate secret with \"authelia crypto hash generate pbkdf2 --random --random.length 32 --random.charset alphanumeric\"\n # Random Password: [REDACTED]\n # Digest: [REDACTED]\n client_secret: [REDACTED]\n token_endpoint_auth_method: 'client_secret_basic'\n public: false\n authorization_policy: two_factor\n consent_mode: pre-configured #explicit\n pre_configured_consent_duration: '6M' #Must be re-authorised every 6 Months\n scopes:\n - openid\n #- groups #Currently not supported in Authelia V\n - email\n - profile\n redirect_uris:\n - https://op.MYDOMAIN.TLD\n #grant_types:\n # - refresh_token\n # - authorization_code\n #response_types:\n # - code\n #response_modes:\n # - form_post\n # - query\n # - fragment\n userinfo_signed_response_alg: none\n##################################################################\n\n\ntelemetry:\n metrics:\n enabled: false\n address: tcp://0.0.0.0:9959\n\ntotp:\n disable: false\n issuer: authelia.com\n algorithm: sha1\n digits: 6\n period: 30 ## The period in seconds a one-time password is valid for.\n skew: 1\n secret_size: 32\n\nwebauthn:\n disable: false\n timeout: 60s ## Adjust the interaction timeout for Webauthn dialogues.\n display_name: Authelia\n attestation_conveyance_preference: indirect\n user_verification: preferred\n\nntp:\n address: \"pool.ntp.org\"\n version: 4\n max_desync: 5s\n disable_startup_check: false\n disable_failure: false\n\nauthentication_backend:\n password_reset:\n disable: false\n custom_url: \"\"\n refresh_interval: 5m\n file:\n path: /config/users_database.yml\n watch: true\n password:\n algorithm: argon2\n argon2:\n variant: argon2id\n iterations: 3\n memory: 65536\n parallelism: 4\n key_length: 32\n salt_length: 16\n\npassword_policy:\n standard:\n enabled: false\n min_length: 8\n max_length: 0\n require_uppercase: true\n require_lowercase: true\n require_number: true\n require_special: true\n ## zxcvbn is a well known and used password strength algorithm. It does not have tunable settings.\n zxcvbn:\n enabled: false\n min_score: 3\n\nregulation:\n max_retries: 3\n find_time: 2m\n ban_time: 5m\n\nsession:\n name: authelia_session\n secret: [REDACTED]\n expiration: 60m\n inactivity: 15m\n cookies:\n - domain: 'MYDOMAIN1.LTD'\n authelia_url: 'https://auth.MYDOMAIN1.LTD'\n name: 'authelia_session'\n default_redirection_url: 'https://MYDOMAIN1.LTD'\n - domain: 'MYDOMAIN2.LTD'\n authelia_url: 'https://auth.MYDOMAIN2.LTD'\n name: 'authelia_session_other'\n default_redirection_url: 'https://MYDOMAIN2.LTD'\n\nstorage:\n encryption_key: [REDACTED]\n local:\n path: /config/db.sqlite3\n\nnotifier:\n disable_startup_check: true\n smtp:\n address: MYOTHERDOMAIN.LTD:465\n timeout: 5s\n username: \"USER@DOMAIN\"\n password: \"[REDACTED]\"\n sender: \"Authelia &lt;postmaster@MYOTHERDOMAIN.LTD&gt;\"\n identifier: NAME@MYOTHERDOMAIN.LTD\n subject: \"[Authelia] {title}\"\n startup_check_address: postmaster@MYOTHERDOMAIN.LTD\n</code></pre>"},{"location":"BACKUPS/","title":"Backing Things Up","text":"<p>Note</p> <p>To back up 99% of your configuration, back up at least the <code>/data/config</code> folder. Database definitions can change between releases, so the safest method is to restore backups using the same app version they were taken from, then upgrade incrementally by following the Migration documentation.</p>"},{"location":"BACKUPS/#what-to-back-up","title":"What to Back Up","text":"<p>There are four key artifacts you can use to back up your NetAlertX configuration:</p> File Description Limitations <code>/db/app.db</code> The application database Might be in an uncommitted state or corrupted <code>/config/app.conf</code> Configuration file Can be overridden using the <code>APP_CONF_OVERRIDE</code> variable <code>/config/devices.csv</code> CSV file containing device data Does not include historical data <code>/config/workflows.json</code> JSON file containing your workflows N/A"},{"location":"BACKUPS/#where-the-data-lives","title":"Where the Data Lives","text":"<p>Understanding where your data is stored helps you plan your backup strategy.</p>"},{"location":"BACKUPS/#core-configuration","title":"Core Configuration","text":"<p>Stored in <code>/data/config/app.conf</code>. This includes settings for:</p> <ul> <li>Notifications</li> <li>Scanning</li> <li>Scheduled maintenance</li> <li>UI preferences</li> </ul> <p>(See Settings System for details.)</p>"},{"location":"BACKUPS/#device-data","title":"Device Data","text":"<p>Stored in <code>/data/config/devices_&lt;timestamp&gt;.csv</code> or <code>/data/config/devices.csv</code>, created by the CSV Backup <code>CSVBCKP</code> Plugin. Contains:</p> <ul> <li>Device names, icons, and categories</li> <li>Network configuration</li> <li>Custom properties</li> </ul>"},{"location":"BACKUPS/#historical-data","title":"Historical Data","text":"<p>Stored in <code>/data/db/app.db</code> (see Database Overview). Contains:</p> <ul> <li>Plugin data and historical entries</li> <li>Event and notification history</li> <li>Device presence history</li> </ul>"},{"location":"BACKUPS/#backup-strategies","title":"Backup Strategies","text":"<p>The safest approach is to back up both the <code>/db</code> and <code>/config</code> folders regularly. Tools like Kopia make this simple and efficient.</p> <p>If you can only keep a few files, prioritize:</p> <ol> <li>The latest <code>devices_&lt;timestamp&gt;.csv</code> or <code>devices.csv</code></li> <li><code>app.conf</code></li> <li><code>workflows.json</code></li> </ol> <p>You can also download the <code>app.conf</code> and <code>devices.csv</code> files from the Maintenance section:</p> <p></p>"},{"location":"BACKUPS/#scenario-1-full-backup-and-restore","title":"Scenario 1: Full Backup and Restore","text":"<p>Goal: Full recovery of your configuration and data.</p>"},{"location":"BACKUPS/#what-to-back-up_1","title":"\ud83d\udcbe What to Back Up","text":"<ul> <li><code>/data/db/app.db</code> (uncorrupted)</li> <li><code>/data/config/app.conf</code></li> <li><code>/data/config/workflows.json</code></li> </ul>"},{"location":"BACKUPS/#how-to-restore","title":"\ud83d\udce5 How to Restore","text":"<p>Map these files into your container as described in the Setup documentation.</p>"},{"location":"BACKUPS/#scenario-2-corrupted-database","title":"Scenario 2: Corrupted Database","text":"<p>Goal: Recover configuration and device data when the database is lost or corrupted.</p>"},{"location":"BACKUPS/#what-to-back-up_2","title":"\ud83d\udcbe What to Back Up","text":"<ul> <li><code>/data/config/app.conf</code></li> <li><code>/data/config/workflows.json</code></li> <li><code>/data/config/devices_&lt;timestamp&gt;.csv</code> (rename to <code>devices.csv</code> during restore)</li> </ul>"},{"location":"BACKUPS/#how-to-restore_1","title":"\ud83d\udce5 How to Restore","text":"<ol> <li>Copy <code>app.conf</code> and <code>workflows.json</code> into <code>/data/config/</code></li> <li>Rename and place <code>devices_&lt;timestamp&gt;.csv</code> \u2192 <code>/data/config/devices.csv</code></li> <li>Restore via the Maintenance section under Devices \u2192 Bulk Editing</li> </ol> <p>This recovers nearly all configuration, workflows, and device metadata.</p>"},{"location":"BACKUPS/#docker-based-backup-and-restore","title":"Docker-Based Backup and Restore","text":"<p>For users running NetAlertX via Docker, you can back up or restore directly from your host system \u2014 a convenient and scriptable option.</p>"},{"location":"BACKUPS/#full-backup-file-level","title":"Full Backup (File-Level)","text":"<ol> <li>Stop the container:</li> </ol> <pre><code>docker stop netalertx\n</code></pre> <ol> <li>Create a compressed archive of your configuration and database volumes:</li> </ol> <pre><code>docker run --rm -v local_path/config:/config -v local_path/db:/db alpine tar -cz /config /db &gt; netalertx-backup.tar.gz\n</code></pre> <ol> <li>Restart the container:</li> </ol> <pre><code>docker start netalertx\n</code></pre>"},{"location":"BACKUPS/#restore-from-backup","title":"Restore from Backup","text":"<ol> <li>Stop the container:</li> </ol> <pre><code>docker stop netalertx\n</code></pre> <ol> <li>Restore from your backup file:</li> </ol> <pre><code>docker run --rm -i -v local_path/config:/config -v local_path/db:/db alpine tar -C / -xz &lt; netalertx-backup.tar.gz\n</code></pre> <ol> <li>Restart the container:</li> </ol> <pre><code>docker start netalertx\n</code></pre> <p>This approach uses a temporary, minimal <code>alpine</code> container to access Docker-managed volumes. The <code>tar</code> command creates or extracts an archive directly from your host\u2019s filesystem, making it fast, clean, and reliable for both automation and manual recovery.</p>"},{"location":"BACKUPS/#summary","title":"Summary","text":"<ul> <li>Back up <code>/data/config</code> for configuration and devices; <code>/data/db</code> for history</li> <li>Keep regular backups, especially before upgrades</li> <li>For Docker setups, use the lightweight <code>alpine</code>-based backup method for consistency and portability</li> </ul>"},{"location":"BUILDS/","title":"NetAlertX Builds: Choose Your Path","text":"<p>NetAlertX provides different installation methods for different needs. This guide helps you choose the right path for security, experimentation, or development.</p>"},{"location":"BUILDS/#1-hardened-appliance-default-production","title":"1. Hardened Appliance (Default Production)","text":"<p>Note</p> <p>Use this image if: You want to use NetAlertX securely.</p>"},{"location":"BUILDS/#who-is-this-for","title":"Who is this for?","text":"<p>All users who want a stable, secure, \"set-it-and-forget-it\" appliance.</p>"},{"location":"BUILDS/#methodology","title":"Methodology","text":"<ul> <li>Multi-stage Alpine build</li> <li>Aggressively \"amputated\"</li> <li>Locked down for max security</li> </ul>"},{"location":"BUILDS/#source","title":"Source","text":"<p><code>Dockerfile (hardened target)</code></p>"},{"location":"BUILDS/#2-tinkerers-image-insecure-vm-style","title":"2. \"Tinkerer's\" Image (Insecure VM-Style)","text":"<p>Note</p> <p>Use this image if: You want to experiment with NetAlertX.</p>"},{"location":"BUILDS/#who-is-this-for_1","title":"Who is this for?","text":"<p>Power users, developers, and \"tinkerers\" wanting a familiar \"VM-like\" experience.</p>"},{"location":"BUILDS/#methodology_1","title":"Methodology","text":"<ul> <li>Traditional Debian build</li> <li>Includes full un-hardened OS</li> <li>Contains <code>apt</code>, <code>sudo</code>, <code>git</code></li> </ul>"},{"location":"BUILDS/#source_1","title":"Source","text":"<p><code>Dockerfile.debian</code></p>"},{"location":"BUILDS/#3-contributors-devcontainer-project-developers","title":"3. Contributor's Devcontainer (Project Developers)","text":"<p>Note</p> <p>Use this image if: You want to develop NetAlertX itself.</p>"},{"location":"BUILDS/#who-is-this-for_2","title":"Who is this for?","text":"<p>Project contributors who are actively writing and debugging code for NetAlertX.</p>"},{"location":"BUILDS/#methodology_2","title":"Methodology","text":"<ul> <li>Builds <code>FROM runner</code> stage</li> <li>Loaded by VS Code</li> <li>Full debug tools: <code>xdebug</code>, <code>pytest</code></li> </ul>"},{"location":"BUILDS/#source_2","title":"Source","text":"<p><code>Dockerfile (devcontainer target)</code></p>"},{"location":"BUILDS/#visualizing-the-trade-offs","title":"Visualizing the Trade-Offs","text":"<p>This chart compares the three builds across key attributes. A higher score means \"more of\" that attribute. Notice the clear trade-offs between security and development features.</p> <p></p>"},{"location":"BUILDS/#build-process-origins","title":"Build Process &amp; Origins","text":"<p>The final images originate from two different files and build paths. The main <code>Dockerfile</code> uses stages to create both the hardened and development container images.</p>"},{"location":"BUILDS/#official-build-path","title":"Official Build Path","text":"<p>Dockerfile -&gt; builder (Stage 1) -&gt; runner (Stage 2) -&gt; hardened (Final Stage) (Production Image) + devcontainer (Final Stage) (Developer Image)</p>"},{"location":"BUILDS/#legacy-build-path","title":"Legacy Build Path","text":"<p>Dockerfile.debian -&gt; \"Tinkerer's\" Image (Insecure VM-Style Image)</p>"},{"location":"COMMON_ISSUES/","title":"Troubleshooting Common Issues","text":"<p>Tip</p> <p>Before troubleshooting, ensure you have set the correct Debugging and LOG_LEVEL.</p>"},{"location":"COMMON_ISSUES/#docker-container-doesnt-start","title":"Docker Container Doesn't Start","text":"<p>Initial setup issues are often caused by missing permissions or incorrectly mapped volumes. Always double-check your <code>docker run</code> or <code>docker-compose.yml</code> against the official setup guide before proceeding.</p>"},{"location":"COMMON_ISSUES/#permissions","title":"Permissions","text":"<p>Make sure your file permissions are correctly set:</p> <ul> <li>If you encounter AJAX errors, cannot write to the database, or see an empty screen, check that permissions are correct and review the logs under <code>/tmp/log</code>.</li> <li>To fix permission issues with the database, update the owner and group of <code>app.db</code> as described in the File Permissions guide.</li> </ul>"},{"location":"COMMON_ISSUES/#container-restarts-crashes","title":"Container Restarts / Crashes","text":"<ul> <li>Check the logs for details. Often, required settings are missing.</li> <li>For more detailed troubleshooting, see Debug and Troubleshooting Tips.</li> <li>To observe errors directly, run the container in the foreground instead of <code>-d</code>:</li> </ul> <pre><code>docker run --rm -it &lt;your_image&gt;\n</code></pre>"},{"location":"COMMON_ISSUES/#docker-container-starts-but-the-application-misbehaves","title":"Docker Container Starts, But the Application Misbehaves","text":"<p>If the container starts but the app shows unexpected behavior, the cause is often data corruption, incorrect configuration, or unexpected input data.</p>"},{"location":"COMMON_ISSUES/#continuous-loading-screen","title":"Continuous \"Loading...\" Screen","text":"<p>A misconfigured application may display a persistent <code>Loading...</code> dialog. This is usually caused by the backend failing to start.</p> <p>Steps to troubleshoot:</p> <ol> <li>Check Maintenance \u2192 Logs for exceptions.</li> <li>If no exception is visible, check the Portainer logs.</li> <li>Start the container in the foreground to observe exceptions.</li> <li>Enable <code>trace</code> or <code>debug</code> logging for detailed output (see Debug Tips).</li> <li>Verify that <code>GRAPHQL_PORT</code> is correctly configured.</li> <li> <p>Check browser logs (press <code>F12</code>):</p> </li> <li> <p>Console tab \u2192 refresh the page</p> </li> <li>Network tab \u2192 refresh the page</li> </ol> <p>If you are unsure how to resolve errors, provide screenshots or log excerpts in your issue report or Discord discussion.</p>"},{"location":"COMMON_ISSUES/#common-configuration-issues","title":"Common Configuration Issues","text":""},{"location":"COMMON_ISSUES/#incorrect-scan_subnets","title":"Incorrect <code>SCAN_SUBNETS</code>","text":"<p>If <code>SCAN_SUBNETS</code> is misconfigured, you may see only a few devices in your device list after a scan. See the Subnets Documentation for proper configuration.</p>"},{"location":"COMMON_ISSUES/#duplicate-devices-and-notifications","title":"Duplicate Devices and Notifications","text":"<ul> <li>Devices are identified by their MAC address.</li> <li>If a device's MAC changes, it will be treated as a new device, triggering notifications.</li> <li>Prevent this by adjusting your device configuration for Android, iOS, or Windows. See the Random MACs Guide.</li> </ul>"},{"location":"COMMON_ISSUES/#unable-to-resolve-host","title":"Unable to Resolve Host","text":"<ul> <li>Ensure <code>SCAN_SUBNETS</code> uses the correct mask and <code>--interface</code>.</li> <li>Refer to the Subnets Documentation for detailed guidance.</li> </ul>"},{"location":"COMMON_ISSUES/#invalid-json-errors","title":"Invalid JSON Errors","text":"<ul> <li>Follow the steps in Invalid JSON Errors Debug Help.</li> </ul>"},{"location":"COMMON_ISSUES/#sudo-execution-fails-eg-on-arpscan-on-raspberry-pi-4","title":"Sudo Execution Fails (e.g., on arpscan on Raspberry Pi 4)","text":"<p>Error:</p> <pre><code>sudo: unexpected child termination condition: 0\n</code></pre> <p>Resolution:</p> <pre><code>wget ftp.us.debian.org/debian/pool/main/libs/libseccomp/libseccomp2_2.5.3-2_armhf.deb\nsudo dpkg -i libseccomp2_2.5.3-2_armhf.deb\n</code></pre> <p>\u26a0\ufe0f The link may break over time. Check Debian Packages for the latest version.</p>"},{"location":"COMMON_ISSUES/#only-router-and-own-device-show-up","title":"Only Router and Own Device Show Up","text":"<ul> <li>Verify the subnet and interface in <code>SCAN_SUBNETS</code>.</li> <li>On devices with multiple Ethernet ports, you may need to change <code>eth0</code> to the correct interface.</li> </ul>"},{"location":"COMMON_ISSUES/#losing-settings-or-devices-after-update","title":"Losing Settings or Devices After Update","text":"<ul> <li>Ensure <code>/data/db</code> and <code>/data/config</code> are mapped to persistent storage.</li> <li>Without persistent volumes, these folders are recreated on every update.</li> <li>See Docker Volumes Setup for proper configuration.</li> </ul>"},{"location":"COMMON_ISSUES/#application-performance-issues","title":"Application Performance Issues","text":"<p>Slowness can be caused by:</p> <ul> <li>Incorrect settings (causing app restarts) \u2192 check <code>app.log</code>.</li> <li>Too many background processes \u2192 disable unnecessary scanners.</li> <li>Long scans \u2192 limit the number of scanned devices.</li> <li>Excessive disk operations or failing maintenance plugins.</li> </ul> <p>See Performance Tips for detailed optimization steps.</p>"},{"location":"COMMON_ISSUES/#ip-flipping","title":"IP flipping","text":"<p>With <code>ARPSCAN</code> scans some devices might flip IP addresses after each scan triggering false notifications. This is because some devices respond to broadcast calls and thus different IPs after scans are logged.</p> <p>See how to prevent IP flipping in the ARPSCAN plugin guide.</p> <p>Alternatively adjust your notification settings to prevent false positives by filtering out events or devices.</p>"},{"location":"COMMON_ISSUES/#multiple-nics-on-same-host-reporting-same-ip","title":"Multiple NICs on Same Host Reporting Same IP","text":"<p>On systems with multiple NICs (like a Proxmox server), each NIC has its own MAC address. Sometimes NetAlertX can incorrectly assign the same IP to all NICs, causing false device mappings. This is due to the way ARP responses are handled by the OS and cannot be overridden directly in NetAlertX.</p> <p>Resolution (Linux-based systems, e.g., Proxmox):</p> <p>Run the following commands on the host to fix ARP behavior:</p> <pre><code>sudo sysctl -w net.ipv4.conf.all.arp_ignore=1\nsudo sysctl -w net.ipv4.conf.all.arp_announce=2\n</code></pre> <p>This ensures each NIC responds correctly to ARP requests and prevents NetAlertX from misassigning IPs.</p> <p>For setups with multiple interfaces on the same switch, consider workflows, device exclusions, or dummy devices as additional workarounds. See Feature Requests for reporting edge cases.</p>"},{"location":"COMMUNITY_GUIDES/","title":"Community Guides","text":"<p>Note</p> <p>This is community-contributed. Due to environment, setup, or networking differences, results may vary. Please open a PR to improve it instead of creating an issue, as the maintainer is not actively maintaining it.</p> <p>Use the official installation guides at first and use community content as supplementary material. (Ordered by last update time)</p> <ul> <li>\u25b6 Discover &amp; Monitor Your Network with This Self-Hosted Open Source Tool - Lawrence Systems (June 2025)</li> <li>\u25b6 Home Lab Network Monitoring - Scotti-BYTE Enterprise Consulting Services (July 2024)</li> <li>\ud83d\udcc4 How to Install NetAlertX on Your Synology NAS - Marius hosting (Updated frequently)</li> <li>\ud83d\udcc4 Using the PiAlert Network Security Scanner on a Raspberry Pi - PiMyLifeUp</li> <li>\u25b6 How to Setup Pi.Alert on Your Synology NAS - Digital Aloha</li> <li>\ud83d\udcc4 \u9632\u8e6d\u7f51\u795e\u5668\uff0c\u7f51\u7edc\u5b89\u5168\u52a9\u624b | \u6781\u7a7a\u95f4\u90e8\u7f72\u7f51\u7edc\u626b\u63cf\u548c\u901a\u77e5\u7cfb\u7edf\u300eNetAlertX\u300f</li> <li>\ud83d\udcc4 \uc2dc\ub180/\ud5e4\ub180\uc5d0\uc11c \ub124\ud2b8\uc6cc\ud06c \uc2a4\uce90\ub108 Pi.Alert Docker\ub85c \uc124\uce58 \ubc0f \uc0ac\uc6a9\ud558\uae30 (July 2023)</li> <li>\ud83d\udcc4 \u7f51\u7edc\u5165\u4fb5\u63a2\u6d4b\u5668Pi.Alert (Chinese) (May 2023)</li> <li>\u25b6 Pi.Alert auf Synology &amp; Docker by - J\u00fcrgen Barth (March 2023)</li> <li>\u25b6 Top Docker Container for Home Server Security - VirtualizationHowto (March 2023)</li> <li>\u25b6 Pi.Alert or WatchYourLAN can alert you to unknown devices appearing on your WiFi or LAN network - Danie van der Merwe (November 2022)</li> </ul>"},{"location":"CUSTOM_PROPERTIES/","title":"Custom Properties for Devices","text":""},{"location":"CUSTOM_PROPERTIES/#overview","title":"Overview","text":"<p>This functionality allows you to define custom properties for devices, which can store and display additional information on the device listing page. By marking properties as \"Show\", you can enhance the user interface with quick actions, notes, or external links.</p>"},{"location":"CUSTOM_PROPERTIES/#key-features","title":"Key Features:","text":"<ul> <li>Customizable Properties: Define specific properties for each device.</li> <li>Visibility Control: Choose which properties are displayed on the device listing page.</li> <li>Interactive Elements: Include actions like links, modals, and device management directly in the interface.</li> </ul>"},{"location":"CUSTOM_PROPERTIES/#usage-on-the-device-listing-page","title":"Usage on the Device Listing Page","text":"<p>Visible properties (<code>CUSTPROP_show: true</code>) are displayed as interactive icons in the device listing. Each icon can perform one of the following actions based on the <code>CUSTPROP_type</code>:</p> <ol> <li>Modals (e.g., Show Notes):</li> <li>Displays detailed information in a popup modal.</li> <li> <p>Example: Firmware version details.</p> </li> <li> <p>Links:</p> </li> <li>Redirect to an external or internal URL.</li> <li> <p>Example: Open a device's documentation or external site.</p> </li> <li> <p>Device Actions:</p> </li> <li>Manage devices with actions like delete.</li> <li> <p>Example: Quickly remove a device from the network.</p> </li> <li> <p>Plugins:</p> </li> <li>Future placeholder for running custom plugin scripts.</li> <li>Note: Not implemented yet.</li> </ol>"},{"location":"CUSTOM_PROPERTIES/#example-use-cases","title":"Example Use Cases","text":"<ol> <li>Device Documentation Link:</li> <li> <p>Add a custom property with <code>CUSTPROP_type</code> set to <code>link</code> or <code>link_new_tab</code> to allow quick navigation to the external documentation of the device.</p> </li> <li> <p>Firmware Details:</p> </li> <li> <p>Use <code>CUSTPROP_type: show_notes</code> to display firmware versions or upgrade instructions in a modal.</p> </li> <li> <p>Device Removal:</p> </li> <li>Enable device removal functionality using <code>CUSTPROP_type: delete_dev</code>.</li> </ol>"},{"location":"CUSTOM_PROPERTIES/#defining-custom-properties","title":"Defining Custom Properties","text":"<p>Custom properties are structured as a list of objects, where each property includes the following fields:</p> Field Description <code>CUSTPROP_icon</code> The icon (Base64-encoded HTML) displayed for the property. <code>CUSTPROP_type</code> The action type (e.g., <code>show_notes</code>, <code>link</code>, <code>delete_dev</code>). <code>CUSTPROP_name</code> A short name or title for the property. <code>CUSTPROP_args</code> Arguments for the action (e.g., URL or modal text). <code>CUSTPROP_notes</code> Additional notes or details displayed when applicable. <code>CUSTPROP_show</code> A boolean to control visibility (<code>true</code> to show on the listing page)."},{"location":"CUSTOM_PROPERTIES/#available-action-types","title":"Available Action Types","text":"<ul> <li>Show Notes: Displays a modal with a title and additional notes.</li> <li>Example: Show firmware details or custom messages.</li> <li>Link: Redirects to a specified URL in the current browser tab. (Arguments Needs to contain the full URL.)</li> <li>Link (New Tab): Opens a specified URL in a new browser tab. (Arguments Needs to contain the full URL.)</li> <li>Delete Device: Deletes the device using its MAC address.</li> <li>Run Plugin: Placeholder for executing custom plugins (not implemented yet).</li> </ul>"},{"location":"CUSTOM_PROPERTIES/#notes","title":"Notes","text":"<ul> <li>Plugin Functionality: The <code>run_plugin</code> action type is currently not implemented and will show an alert if used.</li> <li>Custom Icons (Experimental \ud83e\uddea): Use Base64-encoded HTML to provide custom icons for each property. You can add your icons in Setttings via the <code>CUSTPROP_icon</code> settings</li> <li>Visibility Control: Only properties with <code>CUSTPROP_show: true</code> will appear on the listing page.</li> </ul> <p>This feature provides a flexible way to enhance device management and display with interactive elements tailored to your needs.</p>"},{"location":"DATABASE/","title":"A high-level description of the database structure","text":"<p>An overview of the most important database tables as well as an detailed overview of the Devices table. The MAC address is used as a foreign key in most cases.</p>"},{"location":"DATABASE/#devices-database-table","title":"Devices database table","text":"Field Name Description Sample Value <code>devMac</code> MAC address of the device. <code>00:1A:2B:3C:4D:5E</code> <code>devName</code> Name of the device. <code>iPhone 12</code> <code>devOwner</code> Owner of the device. <code>John Doe</code> <code>devType</code> Type of the device (e.g., phone, laptop, etc.). If set to a network type (e.g., switch), it will become selectable as a Network Parent Node. <code>Laptop</code> <code>devVendor</code> Vendor/manufacturer of the device. <code>Apple</code> <code>devFavorite</code> Whether the device is marked as a favorite. <code>1</code> <code>devGroup</code> Group the device belongs to. <code>Home Devices</code> <code>devComments</code> User comments or notes about the device. <code>Used for work purposes</code> <code>devFirstConnection</code> Timestamp of the device's first connection. <code>2025-03-22 12:07:26+11:00</code> <code>devLastConnection</code> Timestamp of the device's last connection. <code>2025-03-22 12:07:26+11:00</code> <code>devLastIP</code> Last known IP address of the device. <code>192.168.1.5</code> <code>devStaticIP</code> Whether the device has a static IP address. <code>0</code> <code>devScan</code> Whether the device should be scanned. <code>1</code> <code>devLogEvents</code> Whether events related to the device should be logged. <code>0</code> <code>devAlertEvents</code> Whether alerts should be generated for events. <code>1</code> <code>devAlertDown</code> Whether an alert should be sent when the device goes down. <code>0</code> <code>devCanSleep</code> Whether the device can enter a sleep window. When <code>1</code>, offline periods within the <code>NTFPRCS_sleep_time</code> window are shown as Sleeping instead of Down and no down alert is fired. <code>0</code> <code>devSkipRepeated</code> Whether to skip repeated alerts for this device. <code>1</code> <code>devLastNotification</code> Timestamp of the last notification sent for this device. <code>2025-03-22 12:07:26+11:00</code> <code>devPresentLastScan</code> Whether the device was present during the last scan. <code>1</code> <code>devIsNew</code> Whether the device is marked as new. <code>0</code> <code>devLocation</code> Physical or logical location of the device. <code>Living Room</code> <code>devIsArchived</code> Whether the device is archived. <code>0</code> <code>devParentMAC</code> MAC address of the parent device (if applicable) to build the Network Tree. <code>00:1A:2B:3C:4D:5F</code> <code>devParentPort</code> Port of the parent device to which this device is connected. <code>Port 3</code> <code>devIcon</code> Icon representing the device. The value is a base64-encoded SVG or Font Awesome HTML tag. <code>PHN2ZyB...</code> <code>devGUID</code> Unique identifier for the device. <code>a2f4b5d6-7a8c-9d10-11e1-f12345678901</code> <code>devSite</code> Site or location where the device is registered. <code>Office</code> <code>devSSID</code> SSID of the Wi-Fi network the device is connected to. <code>HomeNetwork</code> <code>devSyncHubNode</code> The NetAlertX node ID used for synchronization between NetAlertX instances. <code>node_1</code> <code>devSourcePlugin</code> Source plugin that discovered the device. <code>ARPSCAN</code> <code>devCustomProps</code> Custom properties related to the device. The value is a base64-encoded JSON object. <code>PHN2ZyB...</code> <code>devFQDN</code> Fully qualified domain name. <code>raspberrypi.local</code> <code>devParentRelType</code> The type of relationship between the current device and it's parent node. By default, selecting <code>nic</code> will hide it from lists. <code>nic</code> <code>devReqNicsOnline</code> If all NICs are required to be online to mark teh current device online. <code>0</code> <p>Note</p> <p><code>DevicesView</code> extends the <code>Devices</code> table with two computed fields that are never persisted: - <code>devIsSleeping</code> (<code>1</code> when <code>devCanSleep=1</code>, device is offline, and <code>devLastConnection</code> is within the <code>NTFPRCS_sleep_time</code> window). - <code>devFlapping</code> (<code>1</code> when the device has changed state more than the flap threshold times in the trailing window). - <code>devStatus</code> \u2014 derived string: <code>On-line</code>, <code>Sleeping</code>, <code>Down</code>, or <code>Off-line</code>.</p> <p>To understand how values of these fields influuence application behavior, such as Notifications or Network topology, see also:</p> <ul> <li>Device Management</li> <li>Network Tree Topology Setup</li> <li>Notifications</li> </ul>"},{"location":"DATABASE/#other-tables-overview","title":"Other Tables overview","text":"Table name Description Sample data CurrentScan Result of the current scan Devices The main devices database that also contains the Network tree mappings. If <code>ScanCycle</code> is set to <code>0</code> device is not scanned. Events Used to collect connection/disconnection events. Online_History Used to display the <code>Device presence</code> chart Parameters Used to pass values between the frontend and backend. Plugins_Events For capturing events exposed by a plugin via the <code>last_result.log</code> file. If unique then saved into the <code>Plugins_Objects</code> table. Entries are deleted once processed and stored in the <code>Plugins_History</code> and/or <code>Plugins_Objects</code> tables. Plugins_History History of all entries from the <code>Plugins_Events</code> table Plugins_Language_Strings Language strings collected from the plugin <code>config.json</code> files used for string resolution in the frontend. Plugins_Objects Unique objects detected by individual plugins. Sessions Used to display sessions in the charts Settings Database representation of the sum of all settings from <code>app.conf</code> and plugins coming from <code>config.json</code> files."},{"location":"DEBUG_API_SERVER/","title":"Debugging GraphQL server issues","text":"<p>The GraphQL server is an API middle layer, running on it's own port specified by <code>GRAPHQL_PORT</code>, to retrieve and show the data in the UI. It can also be used to retrieve data for custom third party integarions. Check the API documentation for details.</p> <p>The most common issue is that the GraphQL server doesn't start properly, usually due to a port conflict. If you are running multiple NetAlertX instances, make sure to use unique ports by changing the <code>GRAPHQL_PORT</code> setting. The default is <code>20212</code>.</p>"},{"location":"DEBUG_API_SERVER/#how-to-update-the-graphql_port-in-case-of-issues","title":"How to update the <code>GRAPHQL_PORT</code> in case of issues","text":"<p>As a first troubleshooting step try changing the default <code>GRAPHQL_PORT</code> setting. Please remember NetAlertX is running on the host so any application uising the same port will cause issues.</p>"},{"location":"DEBUG_API_SERVER/#updating-the-setting-via-the-settings-ui","title":"Updating the setting via the Settings UI","text":"<p>Ideally use the Settings UI to update the setting under General -&gt; Core -&gt; GraphQL port:</p> <p></p> <p>You might need to temporarily stop other applications or NetAlertX instances causing conflicts to update the setting. The <code>API_TOKEN</code> is used to authenticate any API calls, including GraphQL requests.</p>"},{"location":"DEBUG_API_SERVER/#updating-the-appconf-file","title":"Updating the <code>app.conf</code> file","text":"<p>If the UI is not accessible, you can directly edit the <code>app.conf</code> file in your <code>/config</code> folder:</p> <p></p>"},{"location":"DEBUG_API_SERVER/#using-a-docker-variable","title":"Using a docker variable","text":"<p>All application settings can also be initialized via the <code>APP_CONF_OVERRIDE</code> docker env variable.</p> <pre><code>...\n environment:\n - PORT=20213\n - APP_CONF_OVERRIDE={\"GRAPHQL_PORT\":\"20214\"}\n...\n</code></pre>"},{"location":"DEBUG_API_SERVER/#how-to-check-the-graphql-server-is-running","title":"How to check the GraphQL server is running?","text":"<p>There are several ways to check if the GraphQL server is running.</p>"},{"location":"DEBUG_API_SERVER/#flask-debug-mode-environment","title":"Flask debug mode (environment)","text":"<p>You can control whether the Flask development debugger is enabled by setting the environment variable <code>FLASK_DEBUG</code> (default: <code>False</code>). Enabling debug mode will turn on the interactive debugger which may expose a remote code execution (RCE) vector if the server is reachable; only enable this for local development and never in production. Valid truthy values are: <code>1</code>, <code>true</code>, <code>yes</code>, <code>on</code> (case-insensitive).</p> <p>In the running container you can set this variable via Docker Compose or your environment, for example:</p> <pre><code>environment:\n - FLASK_DEBUG=1\n</code></pre> <p>When enabled, the GraphQL server startup logs will indicate the debug setting.</p>"},{"location":"DEBUG_API_SERVER/#init-check","title":"Init Check","text":"<p>You can navigate to System Info -&gt; Init Check to see if <code>isGraphQLServerRunning</code> is ticked:</p> <p></p>"},{"location":"DEBUG_API_SERVER/#checking-the-logs","title":"Checking the Logs","text":"<p>You can navigate to Maintenance -&gt; Logs and search for <code>graphql</code> to see if it started correctly and serving requests:</p> <p></p>"},{"location":"DEBUG_API_SERVER/#inspecting-the-browser-console","title":"Inspecting the Browser console","text":"<p>In your browser open the dev console (usually F12) and navigate to the Network tab where you can filter GraphQL requests (e.g., reload the Devices page).</p> <p></p> <p>You can then inspect any of the POST requests by opening them in a new tab.</p> <p></p>"},{"location":"DEBUG_INVALID_JSON/","title":"How to debug the Invalid JSON response error","text":"<p>Check the the HTTP response of the failing backend call by following these steps:</p> <ul> <li>Open developer console in your browser (usually, e. g. for Chrome, key F12 on the keyboard).</li> <li>Follow the steps in this screenshot:</li> </ul> <p></p> <ul> <li>Copy the URL causing the error and enter it in the address bar of your browser directly and hit enter. The copied URLs could look something like this (notice the query strings at the end):</li> <li> <p><code>http://&lt;server&gt;:20211/api/table_devices.json?nocache=1704141103121</code></p> </li> <li> <p>Post the error response in the existing issue thread on GitHub or create a new issue and include the redacted response of the failing query.</p> </li> </ul> <p>For reference, the above queries should return results in the following format:</p>"},{"location":"DEBUG_INVALID_JSON/#first-url","title":"First URL:","text":"<ul> <li>Should yield a valid JSON file</li> </ul>"},{"location":"DEBUG_INVALID_JSON/#second-url","title":"Second URL:","text":""},{"location":"DEBUG_INVALID_JSON/#third-url","title":"Third URL:","text":"<p>You can copy and paste any JSON result (result of the First and Third query) into an online JSON checker, such as this one to check if it's valid.</p>"},{"location":"DEBUG_PHP/","title":"Debugging backend PHP issues","text":""},{"location":"DEBUG_PHP/#logs-in-ui","title":"Logs in UI","text":"<p>You can view recent backend PHP errors directly in the Maintenance &gt; Logs section of the UI. This provides quick access to logs without needing terminal access.</p>"},{"location":"DEBUG_PHP/#accessing-logs-directly","title":"Accessing logs directly","text":"<p>Sometimes, the UI might not be accessible. In that case, you can access the logs directly inside the container.</p>"},{"location":"DEBUG_PHP/#step-by-step","title":"Step-by-step:","text":"<ol> <li>Open a shell into the container:</li> </ol> <pre><code>docker exec -it netalertx /bin/sh\n</code></pre> <ol> <li>Check the NGINX error log:</li> </ol> <pre><code>cat /var/log/nginx/error.log\n</code></pre> <ol> <li>Check the PHP application error log:</li> </ol> <pre><code>cat /tmp/log/app.php_errors.log\n</code></pre> <p>These logs will help identify syntax issues, fatal errors, or startup problems when the UI fails to load properly.</p>"},{"location":"DEBUG_PLUGINS/","title":"Troubleshooting plugins","text":"<p>Tip</p> <p>Before troubleshooting, please ensure you have the right Debugging and LOG_LEVEL set in Settings.</p>"},{"location":"DEBUG_PLUGINS/#high-level-overview","title":"High-level overview","text":"<p>If a Plugin supplies data to the main app it's done either vie a SQL query or via a script that updates the <code>last_result.log</code> file in the plugin log folder (<code>app/log/plugins/</code>).</p> <p>For a more in-depth overview on how plugins work check the Plugins development docs.</p>"},{"location":"DEBUG_PLUGINS/#prerequisites","title":"Prerequisites","text":"<ul> <li>Make sure you read and followed the specific plugin setup instructions.</li> <li>Ensure you have debug enabled (see More Logging)</li> </ul>"},{"location":"DEBUG_PLUGINS/#potential-issues","title":"Potential issues","text":"<ul> <li>Bugs</li> <li>Unexpected input (e.g. special characters in names)</li> <li>Dependencies changed how data is output</li> </ul>"},{"location":"DEBUG_PLUGINS/#incorrect-input-data","title":"Incorrect input data","text":"<p>Input data from the plugin might cause mapping issues in specific edge cases. Look for a corresponding section in the <code>app.log</code> file, and search for <code>[Scheduler] run for PLUGINNAME: YES</code>, so for ICMP you would look for <code>[Scheduler] run for ICMP: YES</code>. You can find examples of useful logs below. If your issue is related to a plugin, and you don't include a log section with this data, we can't help you to resolve your issue.</p>"},{"location":"DEBUG_PLUGINS/#icmp-log-example","title":"ICMP log example","text":"<pre><code>20:39:04 [Scheduler] run for ICMP: YES\n20:39:04 [ICMP] fping skipping 192.168.1.124 : [2], timed out (NaN avg, 100% loss)\n20:39:04 [ICMP] adding 192.168.1.123 from 192.168.1.123 : [2], 64 bytes, 20.1 ms (8.22 avg, 0% loss)\n20:39:04 [ICMP] fping skipping 192.168.1.157 : [1], timed out (NaN avg, 100% loss)\n20:39:04 [ICMP] adding 192.168.1.79 from 192.168.1.79 : [2], 64 bytes, 48.3 ms (60.9 avg, 0% loss)\n20:39:04 [ICMP] fping skipping 192.168.1.128 : [2], timed out (NaN avg, 100% loss)\n20:39:04 [ICMP] fping skipping 192.168.1.129 : [2], timed out (NaN avg, 100% loss)\n</code></pre>"},{"location":"DEBUG_PLUGINS/#pihole-log-example","title":"PIHOLE log example","text":"<pre><code>17:31:05 [Scheduler] run for PIHOLE: YES\n17:31:05 [Plugin utils] ---------------------------------------------\n17:31:05 [Plugin utils] display_name: PiHole (Device sync)\n17:31:05 [Plugins] CMD: SELECT n.hwaddr AS Object_PrimaryID, {s-quote}null{s-quote} AS Object_SecondaryID, datetime() AS DateTime, na.ip AS Watched_Value1, n.lastQuery AS Watched_Value2, na.name AS Watched_Value3, n.macVendor AS Watched_Value4, {s-quote}null{s-quote} AS Extra, n.hwaddr AS ForeignKey FROM EXTERNAL_PIHOLE.Network AS n LEFT JOIN EXTERNAL_PIHOLE.Network_Addresses AS na ON na.network_id = n.id WHERE n.hwaddr NOT LIKE {s-quote}ip-%{s-quote} AND n.hwaddr is not {s-quote}00:00:00:00:00:00{s-quote} AND na.ip is not null\n17:31:05 [Plugins] setTyp: subnets\n17:31:05 [Plugin utils] Flattening the below array\n17:31:05 ['192.168.1.0/24 --interface=eth1']\n17:31:05 [Plugin utils] isinstance(arr, list) : False | isinstance(arr, str) : True\n17:31:05 [Plugins] Resolved value: 192.168.1.0/24 --interface=eth1\n17:31:05 [Plugins] Convert to Base64: True\n17:31:05 [Plugins] base64 value: b'MTkyLjE2OC4xLjAvMjQgLS1pbnRlcmZhY2U9ZXRoMQ=='\n17:31:05 [Plugins] Timeout: 10\n17:31:05 [Plugins] Executing: SELECT n.hwaddr AS Object_PrimaryID, 'null' AS Object_SecondaryID, datetime() AS DateTime, na.ip AS Watched_Value1, n.lastQuery AS Watched_Value2, na.name AS Watched_Value3, n.macVendor AS Watched_Value4, 'null' AS Extra, n.hwaddr AS ForeignKey FROM EXTERNAL_PIHOLE.Network AS n LEFT JOIN EXTERNAL_PIHOLE.Network_Addresses AS na ON na.network_id = n.id WHERE n.hwaddr NOT LIKE 'ip-%' AND n.hwaddr is not '00:00:00:00:00:00' AND na.ip is not null\n\ud83d\udd3b\n17:31:05 [Plugins] SUCCESS, received 2 entries\n17:31:05 [Plugins] sqlParam entries: [(0, 'PIHOLE', '01:01:01:01:01:01', 'null', 'null', '2023-12-25 06:31:05', '172.30.0.1', 0, 'aaaa', 'vvvvvvvvv', 'not-processed', 'null', 'null', '01:01:01:01:01:01'), (0, 'PIHOLE', '02:42:ac:1e:00:02', 'null', 'null', '2023-12-25 06:31:05', '172.30.0.2', 0, 'dddd', 'vvvvv2222', 'not-processed', 'null', 'null', '02:42:ac:1e:00:02')]\n17:31:05 [Plugins] Processing : PIHOLE\n17:31:05 [Plugins] Existing objects from Plugins_Objects: 4\n17:31:05 [Plugins] Logged events from the plugin run : 2\n17:31:05 [Plugins] pluginEvents count: 2\n17:31:05 [Plugins] pluginObjects count: 4\n17:31:05 [Plugins] events_to_insert count: 0\n17:31:05 [Plugins] history_to_insert count: 4\n17:31:05 [Plugins] objects_to_insert count: 0\n17:31:05 [Plugins] objects_to_update count: 4\n17:31:05 [Plugin utils] In pluginEvents there are 2 events with the status \"watched-not-changed\"\n17:31:05 [Plugin utils] In pluginObjects there are 2 events with the status \"missing-in-last-scan\"\n17:31:05 [Plugin utils] In pluginObjects there are 2 events with the status \"watched-not-changed\"\n17:31:05 [Plugins] Mapping objects to database table: CurrentScan\n17:31:05 [Plugins] SQL query for mapping: INSERT into CurrentScan ( \"scanMac\", \"scanLastIP\", \"scanLastQuery\", \"scanName\", \"scanVendor\", \"scanSourcePlugin\") VALUES ( ?, ?, ?, ?, ?, ?)\n17:31:05 [Plugins] SQL sqlParams for mapping: [('01:01:01:01:01:01', '172.30.0.1', 0, 'aaaa', 'vvvvvvvvv', 'PIHOLE'), ('02:42:ac:1e:00:02', '172.30.0.2', 0, 'dddd', 'vvvvv2222', 'PIHOLE')]\n\ud83d\udd3a\n17:31:05 [API] Update API starting\n17:31:06 [API] Updating table_plugins_history.json file in /api\n</code></pre> <p>Note</p> <p>The debug output between the \ud83d\udd3bred arrows\ud83d\udd3a is important for debugging (arrows added only to highlight the section on this page, they are not available in the actual debug log)</p> <p>In the above output notice the section logging how many events are produced by the plugin:</p> <pre><code>17:31:05 [Plugins] Existing objects from Plugins_Objects: 4\n17:31:05 [Plugins] Logged events from the plugin run : 2\n17:31:05 [Plugins] pluginEvents count: 2\n17:31:05 [Plugins] pluginObjects count: 4\n17:31:05 [Plugins] events_to_insert count: 0\n17:31:05 [Plugins] history_to_insert count: 4\n17:31:05 [Plugins] objects_to_insert count: 0\n17:31:05 [Plugins] objects_to_update count: 4\n</code></pre> <p>These values, if formatted correctly, will also show up in the UI:</p> <p></p>"},{"location":"DEBUG_PLUGINS/#sharing-application-state","title":"Sharing application state","text":"<p>Sometimes specific log sections are needed to debug issues. The Devices and CurrentScan table data is sometimes needed to figure out what's wrong.</p> <ol> <li>Please set <code>LOG_LEVEL</code> to <code>trace</code> in the Settings (Disable it once you have the info as this produces big log files).</li> <li>Wait for the issue to occur.</li> <li>Search for <code>================ DEVICES table content ================</code> in your logs.</li> <li>Search for <code>================ CurrentScan table content ================</code> in your logs.</li> <li>Open a new issue and post (redacted) output into the issue description (or send to the netalertx@gmail.com email if sensitive data present).</li> <li>Please set <code>LOG_LEVEL</code> to <code>debug</code> or lower.</li> </ol>"},{"location":"DEBUG_TIPS/","title":"Debugging and troubleshooting","text":"<p>Please follow tips 1 - 4 to get a more detailed error.</p>"},{"location":"DEBUG_TIPS/#1-more-logging","title":"1. More Logging","text":"<p>When debugging an issue always set the highest log level in Settings -&gt; Core:</p> <p><code>LOG_LEVEL='trace'</code></p>"},{"location":"DEBUG_TIPS/#2-surfacing-errors-when-container-restarts","title":"2. Surfacing errors when container restarts","text":"<p>Start the container via the terminal with a command similar to this one:</p> <pre><code>docker run \\\n --network=host \\\n --restart unless-stopped \\\n -v /local_data_dir:/data \\\n -v /etc/localtime:/etc/localtime:ro \\\n --tmpfs /tmp:uid=20211,gid=20211,mode=1700 \\\n -e PORT=20211 \\\n -e APP_CONF_OVERRIDE='{\"GRAPHQL_PORT\":\"20214\"}' \\\n ghcr.io/netalertx/netalertx:latest\n</code></pre> <p>Note: Your <code>/local_data_dir</code> should contain a <code>config</code> and <code>db</code> folder.</p> <p>Note</p> <p>\u26a0 The most important part is NOT to use the <code>-d</code> parameter so you see the error when the container crashes. Use this error in your issue description.</p>"},{"location":"DEBUG_TIPS/#3-check-the-_dev-image-and-open-issues","title":"3. Check the _dev image and open issues","text":"<p>If possible, check if your issue got fixed in the <code>_dev</code> image before opening a new issue. The container is:</p> <p><code>ghcr.io/netalertx/netalertx-dev:latest</code></p> <p>\u26a0 Please backup your DB and config beforehand!</p> <p>Please also search open issues.</p>"},{"location":"DEBUG_TIPS/#4-disable-restart-behavior","title":"4. Disable restart behavior","text":"<p>To prevent a Docker container from automatically restarting in a Docker Compose file, specify the restart policy as <code>no</code>:</p> <pre><code>version: '3'\n\nservices:\n your-service:\n image: your-image:tag\n restart: no\n # Other service configurations...\n</code></pre>"},{"location":"DEBUG_TIPS/#5-tmp-mount-directories-to-rule-host-out-permission-issues","title":"5. TMP mount directories to rule host out permission issues","text":"<p>Try starting the container with all data to be in non-persistent volumes. If this works, the issue might be related to the permissions of your persistent data mount locations on your server. See teh Permissions guide for details.</p>"},{"location":"DEBUG_TIPS/#6-sharing-application-state","title":"6. Sharing application state","text":"<p>Sometimes specific log sections are needed to debug issues. The Devices and CurrentScan table data is sometimes needed to figure out what's wrong.</p> <ol> <li>Please set <code>LOG_LEVEL</code> to <code>trace</code> (Disable it once you have the info as this produces big log files).</li> <li>Wait for the issue to occur.</li> <li>Search for <code>================ DEVICES table content ================</code> in your logs.</li> <li>Search for <code>================ CurrentScan table content ================</code> in your logs.</li> <li>Open a new issue and post (redacted) output into the issue description (or send to the netalertx@gmail.com email if sensitive data present).</li> <li>Please set <code>LOG_LEVEL</code> to <code>debug</code> or lower.</li> </ol>"},{"location":"DEBUG_TIPS/#common-issues","title":"Common issues","text":"<p>See Common issues for additional troubleshooting tips.</p>"},{"location":"DEVICES_BULK_EDITING/","title":"Editing multiple devices at once","text":"<p>NetAlertX allows you to mass-edit devices via a CSV export and import feature, or directly in the UI.</p>"},{"location":"DEVICES_BULK_EDITING/#ui-multi-edit","title":"UI multi edit","text":"<p>Note</p> <p>Make sure you have your backups saved and restorable before doing any mass edits. Check Backup strategies.</p> <p>You can select devices in the Devices view by selecting devices to edit and then clicking the Multi-edit button or via the Maintenance &gt; Multi-Edit section.</p> <p></p>"},{"location":"DEVICES_BULK_EDITING/#csv-bulk-edit","title":"CSV bulk edit","text":"<p>The database and device structure may change with new releases. When using the CSV import functionality, ensure the format matches what the application expects. To avoid issues, you can first export the devices and review the column formats before importing any custom data.</p> <p>Note</p> <p>As always, backup everything, just in case.</p> <ol> <li>In Maintenance &gt; Backup / Restore click the CSV Export button.</li> <li>A <code>devices.csv</code> is generated in the <code>/config</code> folder</li> <li>Edit the <code>devices.csv</code> file however you like.</li> </ol> <p></p> <p>Note</p> <p>The file containing a list of Devices including the Network relationships between Network Nodes and connected devices. You can also trigger this with the <code>CSV Backup</code> plugin. (\ud83d\udca1 You can schedule this)</p> <p></p>"},{"location":"DEVICES_BULK_EDITING/#file-encoding-format","title":"File encoding format","text":"<p>Note</p> <p>Keep Linux line endings (suggested editors: Nano, Notepad++)</p> <p></p>"},{"location":"DEVICE_DISPLAY_SETTINGS/","title":"Device Display Settings","text":"<p>This set of settings allows you to group Devices under different views. The Archived toggle allows you to exclude a Device from most listings and notifications.</p> <p></p>"},{"location":"DEVICE_DISPLAY_SETTINGS/#status-colors","title":"Status Colors","text":"Icon Status Image Description Online (Green) A device that is no longer marked as a \"New Device\". New (Green) A newly discovered device that is online and is still marked as a \"New Device\". Online (Orange) The device is online, but unstable and flapping (3 status changes in the last hour). New (Grey) Same as \"New (Green)\" but the device is now offline. New (Grey) Same as \"New (Green)\" but the device is now offline and archived. Offline (Grey) A device that was not detected online in the last scan. Archived (Grey) A device that was not detected online in the last scan. Sleeping (Aqua) A device with Can Sleep enabled that has gone offline within the <code>NTFPRCS_sleep_time</code> window. No down alert is fired while the device is in this state. See Notifications. Down (Red) A device marked as \"Alert Down\" and offline for the duration set in <code>NTFPRCS_alert_down_time</code>. <p>See also Notification guide.</p>"},{"location":"DEVICE_FIELD_LOCK/","title":"Quick Reference Guide - Device Field Lock/Unlock System","text":""},{"location":"DEVICE_FIELD_LOCK/#overview","title":"Overview","text":"<p>The device field lock/unlock system allows you to protect specific device fields from being automatically overwritten by scanning plugins. When you lock a field, NetAlertX remembers your choice and prevents plugins from changing that value until you unlock it.</p> <p>Use case: You've manually corrected a device name or port number and want to keep it that way, even when plugins discover different values.</p>"},{"location":"DEVICE_FIELD_LOCK/#tracked-fields","title":"Tracked Fields","text":"<p>These are the ONLY fields that can be locked:</p> <ul> <li><code>devName</code> - Device hostname/alias</li> <li><code>devVendor</code> - Device manufacturer</li> <li><code>devSSID</code> - WiFi network name</li> <li><code>devParentMAC</code> - Parent/gateway MAC</li> <li><code>devParentPort</code> - Parent device port</li> <li><code>devParentRelType</code> - Relationship type (e.g., \"gateway\")</li> <li><code>devVlan</code> - VLAN identifier</li> </ul> <p>Additional fields that are tracked (and their source is dispalyed in the UI if available):</p> <ul> <li><code>devMac</code></li> <li><code>devLastIP</code></li> <li><code>devFQDN</code></li> </ul>"},{"location":"DEVICE_FIELD_LOCK/#source-values-explained","title":"Source Values Explained","text":"<p>Each locked field has a \"source\" indicator that shows you why the value is protected:</p> Indicator Meaning Can It Change? \ud83d\udd12 LOCKED You locked this field No, until you unlock it \u270f\ufe0f USER You edited this field No, plugins can't overwrite \ud83d\udce1 NEWDEV Default/unset value Yes, plugins can update \ud83d\udce1 Plugin name Last updated by a plugin (e.g., UNIFIAPI) Yes, plugins can update if field in SET_ALWAYS <p>Overwrite rules are</p> <p>Tip</p> <p>You can bulk-unlock devices in the Multi-edit dialog. This removes all <code>USER</code> and <code>LOCKED</code> values from all <code>*Source</code> fields of selected devices.</p>"},{"location":"DEVICE_FIELD_LOCK/#usage-examples","title":"Usage Examples","text":""},{"location":"DEVICE_FIELD_LOCK/#lock-a-field-prevent-plugin-changes","title":"Lock a Field (Prevent Plugin Changes)","text":"<ol> <li>Navigate to Device Details for the device</li> <li>Find the field you want to protect (e.g., device name)</li> <li>Click the lock button (\ud83d\udd12) next to the field</li> <li>The button changes to unlock (\ud83d\udd13)</li> <li>That field is now protected</li> </ol>"},{"location":"DEVICE_FIELD_LOCK/#unlock-a-field-allow-plugin-updates","title":"Unlock a Field (Allow Plugin Updates)","text":"<ol> <li>Go to Device Details</li> <li>Find the locked field (shows \ud83d\udd13)</li> <li>Click the unlock button (\ud83d\udd13)</li> <li>The button changes back to lock (\ud83d\udd12)</li> <li>Plugins can now update that field again</li> </ol>"},{"location":"DEVICE_FIELD_LOCK/#common-scenarios","title":"Common Scenarios","text":""},{"location":"DEVICE_FIELD_LOCK/#scenario-1-youve-named-your-device-and-want-to-keep-the-name","title":"Scenario 1: You've Named Your Device and Want to Keep the Name","text":"<ol> <li>You manually edit device name to \"Living Room Smart TV\"</li> <li>A scanning plugin later discovers it as \"Unknown Device\" or \"DEVICE-ABC123\"</li> <li>Solution: Lock the device name field</li> <li>Your custom name is preserved even after future scans</li> </ol>"},{"location":"DEVICE_FIELD_LOCK/#scenario-2-you-lock-a-field-but-it-still-changes","title":"Scenario 2: You Lock a Field, But It Still Changes","text":"<p>This means the field source is USER or LOCKED (protected). Check: - Is it showing the lock icon? (If yes, it's protected) - Wait a moment\u2014sometimes changes take a few seconds to display - Try refreshing the page</p>"},{"location":"DEVICE_FIELD_LOCK/#scenario-3-you-want-to-let-plugins-update-again","title":"Scenario 3: You Want to Let Plugins Update Again","text":"<ol> <li>Find the device with locked fields</li> <li>Click the unlock button (\ud83d\udd13) next to each field</li> <li>Refresh the page</li> <li>Next time a plugin runs, it can update that field</li> </ol>"},{"location":"DEVICE_FIELD_LOCK/#what-happens-when-you-lock-a-field","title":"What Happens When You Lock a Field","text":"<ul> <li>\u2705 Your custom value is kept</li> <li>\u2705 Future plugin scans won't overwrite it</li> <li>\u2705 You can still manually edit it anytime after unlocking</li> <li>\u2705 Lock persists across plugin runs</li> <li>\u2705 Other users can see it's locked</li> </ul>"},{"location":"DEVICE_FIELD_LOCK/#what-happens-when-you-unlock-a-field","title":"What Happens When You Unlock a Field","text":"<ul> <li>\u2705 Plugins can update it again on next scan</li> <li>\u2705 If a plugin has a new value, it will be applied</li> <li>\u2705 You can lock it again anytime</li> <li>\u2705 Your manual edits are still saved in the database</li> </ul>"},{"location":"DEVICE_FIELD_LOCK/#error-messages-solutions","title":"Error Messages &amp; Solutions","text":"Message What It Means What to Do \"Field cannot be locked\" You tried to lock a field that doesn't support locking Only lock the fields listed above \"Device not found\" The device MAC address doesn't exist Verify the device hasn't been deleted Lock button doesn't work Network or permission issue Refresh the page and try again Unexpected field changed Field might have been unlocked Check if field shows unlock icon (\ud83d\udd13)"},{"location":"DEVICE_FIELD_LOCK/#quick-tips","title":"Quick Tips","text":"<ul> <li>Lock names you manually corrected to keep them stable</li> <li>Leave discovery fields (vendor, FQDN) unlocked for automatic updates</li> <li>Use locks sparingly\u2014they prevent automatic data enrichment</li> <li>Check the source indicator (colored badge) to understand field origin</li> <li>Lock buttons only appear for devices that are saved (not for new devices being created)</li> </ul>"},{"location":"DEVICE_FIELD_LOCK/#when-to-lock-vs-when-not-to-lock","title":"When to Lock vs. When NOT to Lock","text":""},{"location":"DEVICE_FIELD_LOCK/#good-reasons-to-lock","title":"\u2705 Good reasons to lock:","text":"<ul> <li>You've customized the device name and it's correct</li> <li>You've set a static IP and it shouldn't change</li> <li>You've configured VLAN information</li> <li>You know the parent device and don't want it auto-corrected</li> </ul>"},{"location":"DEVICE_FIELD_LOCK/#bad-reasons-to-lock","title":"\u274c Bad reasons to lock:","text":"<ul> <li>The value seems wrong\u2014edit it first, then lock</li> <li>You want to prevent data from another source\u2014use field lock, not to hide problems</li> <li>You're trying to force a value the system disagrees with</li> </ul>"},{"location":"DEVICE_FIELD_LOCK/#troubleshooting","title":"Troubleshooting","text":"<p>Lock button not appearing:</p> <ul> <li>Confirm the field is one of the tracked fields (see list above)</li> <li>Confirm the device is already saved (new devices don't show lock buttons)</li> <li>Refresh the page</li> </ul> <p>Lock button is there but click doesn't work:</p> <ul> <li>Check your internet connection</li> <li>Check you have permission to edit devices</li> <li>Look at browser console (F12 &gt; Console tab) for error messages</li> <li>Try again in a few seconds</li> </ul> <p>Field still changes after locking:</p> <ul> <li>Double-check the lock icon shows</li> <li>Reload the page\u2014the change might be a display issue</li> <li>Check if you accidentally unlocked it</li> <li>Open an issue if it persists</li> </ul>"},{"location":"DEVICE_FIELD_LOCK/#see-also","title":"See also","text":"<ul> <li>Device locking</li> <li>Device source fields</li> <li>API Device Endpoints Documentation</li> <li>Authoritative Field Updates System</li> <li>Plugin Configuration Reference</li> <li>Device locking APIs</li> <li>Device management</li> </ul>"},{"location":"DEVICE_HEURISTICS/","title":"Device Heuristics: Icon and Type Guessing","text":"<p>This module is responsible for inferring the most likely device type and icon based on minimal identifying data like MAC address, vendor, IP, or device name.</p> <p>It does this using a set of heuristics defined in an external JSON rules file, which it evaluates in priority order.</p> <p>Note</p> <p>You can find the full source code of the heuristics module in the <code>device_heuristics.py</code> file.</p>"},{"location":"DEVICE_HEURISTICS/#json-rule-format","title":"JSON Rule Format","text":"<p>Rules are defined in a file called <code>device_heuristics_rules.json</code> (located under <code>/back</code>), structured like:</p> <pre><code>[\n {\n \"dev_type\": \"Phone\",\n \"icon_html\": \"&lt;i class=\\\"fa-brands fa-apple\\\"&gt;&lt;/i&gt;\",\n \"matching_pattern\": [\n { \"mac_prefix\": \"001A79\", \"vendor\": \"Apple\" }\n ],\n \"name_pattern\": [\"iphone\", \"pixel\"]\n }\n]\n</code></pre> <p>Note</p> <p>Feel free to raise a PR in case you'd like to add any rules into the <code>device_heuristics_rules.json</code> file. Please place new rules into the correct position and consider the priority of already available rules.</p>"},{"location":"DEVICE_HEURISTICS/#supported-fields","title":"Supported fields:","text":"Field Type Description <code>dev_type</code> <code>string</code> Type to assign if rule matches (e.g. <code>\"Gateway\"</code>, <code>\"Phone\"</code>) <code>icon_html</code> <code>string</code> Icon (HTML string) to assign if rule matches. Encoded to base64 at load time. <code>matching_pattern</code> <code>array</code> List of <code>{ mac_prefix, vendor }</code> objects for first strict and then loose matching <code>name_pattern</code> <code>array</code> (optional) List of lowercase substrings (used with regex) <code>ip_pattern</code> <code>array</code> (optional) Regex patterns to match IPs <p>Order in this array defines priority \u2014 rules are checked top-down and short-circuit on first match.</p>"},{"location":"DEVICE_HEURISTICS/#matching-flow-in-priority-order","title":"Matching Flow (in Priority Order)","text":"<p>The function <code>guess_device_attributes(...)</code> runs a series of matching functions in strict order:</p> <ol> <li>MAC + Vendor \u2192 <code>match_mac_and_vendor()</code></li> <li>Vendor only \u2192 <code>match_vendor()</code></li> <li>Name pattern \u2192 <code>match_name()</code></li> <li>IP pattern \u2192 <code>match_ip()</code></li> <li>Final fallback \u2192 defaults defined in the <code>NEWDEV_devIcon</code> and <code>NEWDEV_devType</code> settings.</li> </ol> <p>Note</p> <p>The app will try guessing the device type or icon if <code>devType</code> or <code>devIcon</code> are <code>\"\"</code> or <code>\"null\"</code>.</p>"},{"location":"DEVICE_HEURISTICS/#use-of-default-values","title":"Use of default values","text":"<p>The guessing process runs for every device as long as the current type or icon still matches the default values. Even if earlier heuristics return a match, the system continues evaluating additional clues \u2014 like name or IP \u2014 to try and replace placeholders.</p> <pre><code># Still considered a match attempt if current values are defaults\nif (not type_ or type_ == default_type) or (not icon or icon == default_icon):\n type_, icon = match_ip(ip, default_type, default_icon)\n</code></pre> <p>In other words: if the type or icon is still <code>\"unknown\"</code> (or matches the default), the system assumes the match isn\u2019t final \u2014 and keeps looking. It stops only when both values are non-default (defaults are defined in the <code>NEWDEV_devIcon</code> and <code>NEWDEV_devType</code> settings).</p>"},{"location":"DEVICE_HEURISTICS/#match-behavior-per-function","title":"Match Behavior (per function)","text":"<p>These functions are executed in the following order:</p>"},{"location":"DEVICE_HEURISTICS/#match_mac_and_vendormac_clean-vendor","title":"<code>match_mac_and_vendor(mac_clean, vendor, ...)</code>","text":"<ul> <li>Looks for MAC prefix and vendor substring match</li> <li>Most precise</li> <li>Stops as soon as a match is found</li> </ul>"},{"location":"DEVICE_HEURISTICS/#match_vendorvendor","title":"<code>match_vendor(vendor, ...)</code>","text":"<ul> <li>Falls back to substring match on vendor only</li> <li>Ignores rules where <code>mac_prefix</code> is present (ensures this is really a fallback)</li> </ul>"},{"location":"DEVICE_HEURISTICS/#match_namename","title":"<code>match_name(name, ...)</code>","text":"<ul> <li>Lowercase name is compared against all <code>name_pattern</code> values using regex</li> <li>Good for user-assigned labels (e.g. \"AP Office\", \"iPhone\")</li> </ul>"},{"location":"DEVICE_HEURISTICS/#match_ipip","title":"<code>match_ip(ip, ...)</code>","text":"<ul> <li>If IP is present and matches regex patterns under any rule, it returns that type/icon</li> <li>Usually used for gateways or local IP ranges</li> </ul>"},{"location":"DEVICE_HEURISTICS/#icons","title":"Icons","text":"<ul> <li>Each rule can define an <code>icon_html</code>, which is converted to a <code>icon_base64</code> on load</li> <li>If missing, it falls back to the passed-in <code>default_icon</code> (<code>NEWDEV_devIcon</code> setting)</li> <li>If a match is found but icon is still blank, default is used</li> </ul> <p>TL;DR: Type and icon must both be matched. If only one is matched, the other falls back to the default.</p>"},{"location":"DEVICE_HEURISTICS/#priority-mechanics","title":"Priority Mechanics","text":"<ul> <li>JSON rules are evaluated top-to-bottom</li> <li>Matching is first-hit wins \u2014 no scoring, no weights</li> <li>Rules that are more specific (e.g. exact MAC prefixes) should be listed earlier</li> </ul>"},{"location":"DEVICE_MANAGEMENT/","title":"Device Management","text":"<p>The Main Info section is where most of the device identifiable information is stored and edited. Some of the information is autodetected via various plugins. Initial values for most of the fields can be specified in the <code>NEWDEV</code> plugin.</p> <p>Note</p> <p>You can multi-edit devices by selecting them in the main Devices view, from the Mainetence section, or via the CSV Export functionality under Maintenance. More info can be found in the Devices Bulk-editing docs.</p> <p></p>"},{"location":"DEVICE_MANAGEMENT/#main-info","title":"Main Info","text":"<ul> <li>MAC: MAC addres of the device. Not editable, unless creating a new dummy device.</li> <li>Last IP: IP addres of the device. Not editable, unless creating a new dummy device.</li> <li>Name: Friendly device name. Autodetected via various \ud83c\udd8e Name discovery plugins. The app attaches <code>(IP match)</code> if the name is discovered via an IP match and not MAC match which could mean the name could be incorrect as IPs might change.</li> <li>Icon: Partially autodetected. Select an existing or add a custom icon. You can also auto-apply the same icon on all devices of the same type.</li> <li>Owner: Device owner (The list is self-populated with existing owners and you can add custom values).</li> <li>Type: Select a device type from the dropdown list (<code>Smartphone</code>, <code>Tablet</code>, <code>Laptop</code>, <code>TV</code>, <code>router</code>, etc.) or add a new device type. If you want the device to act as a Network device (and be able to be a network node in the Network view), select a type under Network Devices or add a new Network Device type in Settings. More information can be found in the Network Setup docs.</li> <li>Vendor: The manufacturing vendor. Automatically updated by NetAlertX when empty or unknown, can be edited.</li> <li>Group: Select a group (<code>Always on</code>, <code>Personal</code>, <code>Friends</code>, etc.) or type your own Group name.</li> <li>Location: Select the location, usually a room, where the device is located (<code>Kitchen</code>, <code>Attic</code>, <code>Living room</code>, etc.) or add a custom Location.</li> <li>Comments: Add any comments for the device, such as a serial number, or maintenance information.</li> </ul> <p>Note</p> <p>Please note the above usage of the fields are only suggestions. You can use most of these fields for other purposes, such as storing the network interface, company owning a device, or similar.</p>"},{"location":"DEVICE_MANAGEMENT/#dummy-devices","title":"Dummy devices","text":"<p>You can create dummy devices from the Devices listing screen.</p> <p></p> <p>The MAC field and the Last IP field will then become editable.</p> <p></p>"},{"location":"DEVICE_MANAGEMENT/#dummy-or-manually-created-device-status","title":"Dummy or Manually Created Device Status","text":"<p>You can control a dummy device\u2019s status either via <code>ICMP</code> (automatic) or the <code>Force Status</code> field (manual). Choose based on whether the device is real and how important data hygiene is.</p>"},{"location":"DEVICE_MANAGEMENT/#icmp-real-devices","title":"<code>ICMP</code> (Real Devices)","text":"<p>Use a real IP that responds to ping so status is updated automatically.</p>"},{"location":"DEVICE_MANAGEMENT/#force-status-best-for-data-hygiene","title":"<code>Force Status</code> (Best for Data Hygiene)","text":"<p>Manually set the status when the device is not reachable or is purely logical. This keeps your data clean and avoids fake IPs.</p>"},{"location":"DEVICE_MANAGEMENT/#loopback-ip-127001-0000","title":"Loopback IP (<code>127.0.0.1</code>, <code>0.0.0.0</code>)","text":"<p>Use when you want the device to always appear online via <code>ICMP</code>. Note this simulates reachability and introduces artificial data. This approach might be preferred, if you want to filter and distinguish dummy devices based on IP when filtering your asset lists.</p>"},{"location":"DEVICE_MANAGEMENT/#copying-data-from-an-existing-device","title":"Copying data from an existing device.","text":"<p>To speed up device population you can also copy data from an existing device. This can be done from the Tools tab on the Device details.</p>"},{"location":"DEVICE_MANAGEMENT/#field-locking-preventing-plugin-overwrites","title":"Field Locking (Preventing Plugin Overwrites)","text":"<p>NetAlertX allows you to \"lock\" specific device fields to prevent plugins from automatically overwriting your custom values. This is useful when you've manually corrected information that might be discovered differently by discovery plugins.</p>"},{"location":"DEVICE_MANAGEMENT/#quick-start","title":"Quick Start","text":"<ol> <li>Open a device for editing</li> <li>Click the lock button (\ud83d\udd12) next to any tracked field</li> <li>The field is now protected\u2014plugins cannot change it until you unlock it</li> </ol>"},{"location":"DEVICE_MANAGEMENT/#see-also","title":"See Also","text":"<ul> <li>For Users: Quick Reference - Device Field Lock/Unlock - How to use field locking</li> <li>For Developers: API Device Field Lock Documentation - Technical API reference</li> <li>For Plugin Developers: Plugin Field Configuration (SET_ALWAYS/SET_EMPTY) - Configure which fields plugins can update</li> </ul>"},{"location":"DEVICE_SOURCE_FIELDS/","title":"Understanding Device Source Fields and Field Updates","text":"<p>When the system scans a network, it finds various details about devices (like names, IP addresses, and manufacturers). To ensure the data remains accurate without accidentally overwriting manual changes, the system uses a set of \"Source Rules.\"</p> <p></p>"},{"location":"DEVICE_SOURCE_FIELDS/#the-protection-levels","title":"The \"Protection\" Levels","text":"<p>Every piece of information for a device has a Source. This source determines whether a new scan is allowed to change that value.</p> Source Status Description Can a Scan Overwrite it? USER You manually entered this value. Never LOCKED This value is pinned and protected. Never NEWDEV This value was initialized from <code>NEWDEV</code> plugin settings. Always (Plugin Name) The value was found by a specific scanner (e.g., <code>NBTSCAN</code>). Only if specific rules are met"},{"location":"DEVICE_SOURCE_FIELDS/#how-scans-update-information","title":"How Scans Update Information","text":"<p>If a field is not protected by a <code>USER</code> or <code>LOCKED</code> status, the system follows these rules to decide if it should update the info:</p>"},{"location":"DEVICE_SOURCE_FIELDS/#1-the-empty-field-rule-default","title":"1. The \"Empty Field\" Rule (Default)","text":"<p>By default, the system is cautious. It will only fill in a piece of information if the current field is empty (showing as \"unknown,\" \"0.0.0.0,\" or blank). It won't change for example an existing name unless you tell it to.</p>"},{"location":"DEVICE_SOURCE_FIELDS/#2-set_always","title":"2. SET_ALWAYS","text":"<p>Some plugins are configured to be \"authoritative.\" If a field is in the SET_ALWAYS setting of a plugin:</p> <ul> <li>The scanner will always overwrite the current value with the new one.</li> <li>Note: It will still never overwrite a <code>USER</code> or <code>LOCKED</code> field.</li> </ul>"},{"location":"DEVICE_SOURCE_FIELDS/#3-set_empty","title":"3. SET_EMPTY","text":"<p>If a field is in the SET_EMPTY list:</p> <ul> <li>The scanner will only provide a value if the current field is currently empty.</li> <li>This is used for fields where we want to \"fill in the blanks\" but never change a value once it has been established by any source.</li> </ul>"},{"location":"DEVICE_SOURCE_FIELDS/#4-automatic-overrides-live-tracking","title":"4. Automatic Overrides (Live Tracking)","text":"<p>Some fields, like IP Addresses (<code>devLastIP</code>) and Full Domain Names (<code>devFQDN</code>), are set to automatically update whenever they change. This ensures that if a device moves to a new IP on your network, the system reflects that change immediately without you having to do anything.</p>"},{"location":"DEVICE_SOURCE_FIELDS/#summary-of-field-logic","title":"Summary of Field Logic","text":"If the current value is... And the Scan finds... Does it update? USER / LOCKED Anything No Empty A new value Yes A \"Plugin\" value A different value No (Unless <code>SET_ALWAYS</code> is on) An IP Address A different IP Yes (Updates automatically)"},{"location":"DEVICE_SOURCE_FIELDS/#see-also","title":"See also:","text":"<ul> <li>Device locking</li> <li>Device source fields</li> <li>API Device Endpoints Documentation</li> <li>Authoritative Field Updates System</li> <li>Plugin Configuration Reference</li> <li>Device locking APIs</li> <li>Device management</li> </ul>"},{"location":"DEV_DEVCONTAINER/","title":"Devcontainer for NetAlertX Guide","text":"<p>This devcontainer is designed to mirror the production container environment as closely as possible, while providing a rich set of tools for development.</p>"},{"location":"DEV_DEVCONTAINER/#how-to-get-started","title":"How to Get Started","text":"<ol> <li> <p>Prerequisites:</p> <ul> <li>A working Docker installation that can be managed by your user. This can be Docker Desktop or Docker Engine installed via other methods (like the official get-docker script).</li> <li>Visual Studio Code installed.</li> <li>The VS Code Dev Containers extension installed.</li> </ul> </li> <li> <p>Launch the Devcontainer:</p> <ul> <li>Clone this repository.</li> <li>Open the repository folder in VS Code.</li> <li>A notification will pop up in the bottom-right corner asking to \"Reopen in Container\". Click it.</li> <li>VS Code will now build the Docker image and connect your editor to the container. Your terminal, debugger, and all tools will now be running inside this isolated environment.</li> </ul> </li> </ol>"},{"location":"DEV_DEVCONTAINER/#key-workflows-features","title":"Key Workflows &amp; Features","text":"<p>Once you're inside the container, everything is set up for you.</p>"},{"location":"DEV_DEVCONTAINER/#1-services-frontend-backend","title":"1. Services (Frontend &amp; Backend)","text":"<p>The container's startup script (<code>.devcontainer/scripts/setup.sh</code>) automatically starts the Nginx/PHP frontend and the Python backend. You can restart them at any time using the built-in tasks.</p>"},{"location":"DEV_DEVCONTAINER/#2-integrated-debugging-just-press-f5","title":"2. Integrated Debugging (Just Press F5!)","text":"<p>Debugging for both the Python backend and PHP frontend is pre-configured and ready to go.</p> <ul> <li>Python Backend (debugpy): The backend automatically starts with a debugger attached on port <code>5678</code>. Simply open a Python file (e.g., <code>server/__main__.py</code>), set a breakpoint, and press F5 (or select \"Python Backend Debug: Attach\") to connect the debugger.</li> <li>PHP Frontend (Xdebug): Xdebug listens on port <code>9003</code>. In VS Code, start listening for Xdebug connections and use a browser extension (like \"Xdebug helper\") to start a debugging session for the web UI.</li> </ul>"},{"location":"DEV_DEVCONTAINER/#3-common-tasks-f1-run-task","title":"3. Common Tasks (F1 -&gt; Run Task)","text":"<p>We've created several VS Code Tasks to simplify common operations. Access them by pressing <code>F1</code> and typing \"Tasks: Run Task\".</p> <ul> <li><code>Generate Dockerfile</code>: This is important. The actual <code>.devcontainer/Dockerfile</code> is auto-generated. If you need to change the container environment, edit <code>.devcontainer/resources/devcontainer-Dockerfile</code> and then run this task.</li> <li><code>Re-Run Startup Script</code>: Manually re-runs the <code>.devcontainer/scripts/setup.sh</code> script to re-link files and restart services.</li> <li><code>Start Backend (Python)</code> / <code>Start Frontend (nginx and PHP-FPM)</code>: Manually restart the services if needed.</li> </ul>"},{"location":"DEV_DEVCONTAINER/#4-running-tests","title":"4. Running Tests","text":"<p>The environment includes <code>pytest</code>. You can run tests directly from the VS Code Test Explorer UI or by running <code>pytest -q</code> in the integrated terminal. The necessary <code>PYTHONPATH</code> is already configured so that tests can correctly import the server modules.</p>"},{"location":"DEV_DEVCONTAINER/#how-to-maintain-this-devcontainer","title":"How to Maintain This Devcontainer","text":"<p>The setup is designed to be easy to manage. Here are the core principles:</p> <ul> <li>Don't Edit <code>Dockerfile</code> Directly: The main <code>.devcontainer/Dockerfile</code> is a combination of the project's root <code>Dockerfile</code> and a special dev-only stage. To add new tools or dependencies, edit <code>.devcontainer/resources/devcontainer-Dockerfile</code> and then run the <code>Generate Dockerfile</code> task.</li> <li>Build-Time vs. Run-Time Setup:<ul> <li>For changes that can be baked into the image (like installing a new package with <code>apk add</code>), add them to the resource Dockerfile.</li> <li>For changes that must happen when the container starts (like creating symlinks, setting permissions, or starting services), use <code>.devcontainer/scripts/setup.sh</code>.</li> </ul> </li> <li>Project Conventions: The <code>.github/copilot-instructions.md</code> file is an excellent resource to help AI and humans understand the project's architecture, conventions, and how to use existing helper functions instead of hardcoding values.</li> </ul> <p>This setup provides a powerful and consistent foundation for all current and future contributors to NetAlertX.</p>"},{"location":"DEV_ENV_SETUP/","title":"Development Environment Setup","text":"<p>I truly appreciate all contributions! To help keep this project maintainable, this guide provides an overview of project priorities, key design considerations, and overall philosophy. It also includes instructions for setting up your environment so you can start contributing right away.</p>"},{"location":"DEV_ENV_SETUP/#development-guidelines","title":"Development Guidelines","text":"<p>Before starting development, please review the following guidelines.</p>"},{"location":"DEV_ENV_SETUP/#priority-order-highest-to-lowest","title":"Priority Order (Highest to Lowest)","text":"<ol> <li>\ud83d\udd3c Fixing core bugs that lack workarounds</li> <li>\ud83d\udd35 Adding core functionality that unlocks other features (e.g., plugins)</li> <li>\ud83d\udd35 Refactoring to enable faster development</li> <li>\ud83d\udd3d UI improvements (PRs welcome, but low priority)</li> </ol>"},{"location":"DEV_ENV_SETUP/#design-philosophy","title":"Design Philosophy","text":"<p>The application architecture is designed for extensibility and maintainability. It relies heavily on configuration manifests via plugins and settings to dynamically build the UI and populate the application with data from various sources.</p> <p>For details, see: - Plugins Development (includes video) - Settings System</p> <p>Focus on core functionality and integrate with existing tools rather than reinventing the wheel.</p> <p>Examples: - Using Apprise for notifications instead of implementing multiple separate gateways - Implementing regex-based validation instead of one-off validation for each setting</p> <p>Note</p> <p>UI changes have lower priority. PRs are welcome, but please keep them small and focused.</p>"},{"location":"DEV_ENV_SETUP/#development-environment-set-up","title":"Development Environment Set Up","text":"<p>Tip</p> <p>There is also a ready to use devcontainer available.</p> <p>The following steps will guide you to set up your environment for local development and to run a custom docker build on your system. For most changes the container doesn't need to be rebuild which speeds up the development significantly.</p> <p>Note</p> <p>Replace <code>/development</code> with the path where your code files will be stored. The default container name is <code>netalertx</code> so there might be a conflict with your running containers.</p>"},{"location":"DEV_ENV_SETUP/#1-download-the-code","title":"1. Download the code:","text":"<ul> <li><code>mkdir /development</code></li> <li><code>cd /development &amp;&amp; git clone https://github.com/netalertx/NetAlertX.git</code></li> </ul>"},{"location":"DEV_ENV_SETUP/#2-create-a-dev-env_dev-file","title":"2. Create a DEV .env_dev file","text":"<p><code>touch /development/.env_dev &amp;&amp; sudo nano /development/.env_dev</code></p> <p>The file content should be following, with your custom values.</p> <pre><code>#--------------------------------\n#NETALERTX\n#--------------------------------\nPORT=22222 # make sure this port is unique on your whole network\nDEV_LOCATION=/development/NetAlertX\nAPP_DATA_LOCATION=/volume/docker_appdata\n# Make sure your GRAPHQL_PORT setting has a port that is unique on your whole host network\nAPP_CONF_OVERRIDE={\"GRAPHQL_PORT\":\"22223\"}\n# ALWAYS_FRESH_INSTALL=true # uncommenting this will always delete the content of /config and /db dirs on boot to simulate a fresh install\n</code></pre>"},{"location":"DEV_ENV_SETUP/#3-create-db-and-config-dirs","title":"3. Create /db and /config dirs","text":"<p>Create a folder <code>netalertx</code> in the <code>APP_DATA_LOCATION</code> (in this example in <code>/volume/docker_appdata</code>) with 2 subfolders <code>db</code> and <code>config</code>.</p> <ul> <li><code>mkdir /volume/docker_appdata/netalertx</code></li> <li><code>mkdir /volume/docker_appdata/netalertx/db</code></li> <li><code>mkdir /volume/docker_appdata/netalertx/config</code></li> </ul>"},{"location":"DEV_ENV_SETUP/#4-run-the-container","title":"4. Run the container","text":"<ul> <li><code>cd /development/NetAlertX &amp;&amp; sudo docker-compose --env-file ../.env_dev</code></li> </ul> <p>You can then modify the python script without restarting/rebuilding the container every time. Additionally, you can trigger a plugin run via the UI:</p> <p></p>"},{"location":"DEV_ENV_SETUP/#tips","title":"Tips","text":"<p>A quick cheat sheet of useful commands.</p>"},{"location":"DEV_ENV_SETUP/#removing-the-container-and-image","title":"Removing the container and image","text":"<p>A command to stop, remove the container and the image (replace <code>netalertx</code> and <code>netalertx-netalertx</code> with the appropriate values)</p> <ul> <li><code>sudo docker container stop netalertx ; sudo docker container rm netalertx ; sudo docker image rm netalertx-netalertx</code></li> </ul>"},{"location":"DEV_ENV_SETUP/#restart-the-server-backend","title":"Restart the server backend","text":"<p>Most code changes can be tested without rebuilding the container. When working on the python server backend, you only need to restart the server.</p> <ol> <li>You can usually restart the backend via Maintenance &gt; Logs &gt; Restart server</li> </ol> <p></p> <ol> <li> <p>If above doesn't work, SSH into the container and kill &amp; restart the main script loop</p> </li> <li> <p><code>sudo docker exec -it netalertx /bin/bash</code></p> </li> <li> <p><code>pkill -f \"python /app/server\" &amp;&amp; python /app/server &amp;</code></p> </li> <li> <p>If none of the above work, restart the docker container.</p> </li> <li> <p>This is usually the last resort as sometimes the Docker engine becomes unresponsive and the whole engine needs to be restarted.</p> </li> </ol>"},{"location":"DEV_ENV_SETUP/#contributing-pull-requests","title":"Contributing &amp; Pull Requests","text":""},{"location":"DEV_ENV_SETUP/#before-submitting-a-pr-please-ensure","title":"Before submitting a PR, please ensure:","text":"<p>\u2714 Changes are backward-compatible with existing installs. \u2714 No unnecessary changes are made. \u2714 New features are reusable, not narrowly scoped. \u2714 Features are implemented via plugins if possible.</p>"},{"location":"DEV_ENV_SETUP/#mandatory-test-cases","title":"Mandatory Test Cases","text":"<ul> <li>Fresh install (no DB/config).</li> <li>Existing DB/config compatibility.</li> <li> <p>Notification testing:</p> <ul> <li>Email</li> <li>Apprise (e.g., Telegram)</li> <li>Webhook (e.g., Discord)</li> <li>MQTT (e.g., Home Assistant)</li> </ul> </li> <li> <p>Updating Settings and their persistence.</p> </li> <li>Updating a Device</li> <li>Plugin functionality.</li> <li>Error log inspection.</li> </ul> <p>Note</p> <p>Always run all available tests as per the Testing documentation.</p>"},{"location":"DEV_PORTS_HOST_MODE/","title":"Dev Ports in Host Network Mode","text":"<p>When using <code>\"--network=host\"</code> in the devcontainer, VS Code's normal port forwarding model doesn't apply. All container ports are already on the host network namespace, so:</p> <ul> <li>Listing ports in <code>forwardPorts</code> can cause VS Code to pre-bind or reserve them (conflicts with startup scripts waiting for a free port).</li> <li>The PORTS panel will not auto-detect services reliably, because forwarding isn't occurring.</li> <li>Debugger ports (e.g. Xdebug <code>9003</code>, Python debugpy <code>5678</code>) can still be listed safely.</li> </ul>"},{"location":"DEV_PORTS_HOST_MODE/#recommended-pattern","title":"Recommended Pattern","text":"<ol> <li>Only include debugger ports in <code>forwardPorts</code>: <pre><code>\"forwardPorts\": [5678, 9003]\n</code></pre></li> <li>Do NOT list application service ports (e.g. 20211, 20212) there when in host mode.</li> <li>Use the helper task to enumerate current bindings:</li> <li>Run task: <code>&gt; Tasks: Run Task</code> \u2192 <code>[Dev Container] List NetAlertX Ports</code></li> </ol>"},{"location":"DEV_PORTS_HOST_MODE/#port-enumeration-script","title":"Port Enumeration Script","text":"<p>Script: <code>scripts/list-ports.sh</code> Outputs binding address, PID (if resolvable) and process name for key ports.</p> <p>You can edit the PORTS variable inside that script to add/remove watched ports.</p>"},{"location":"DEV_PORTS_HOST_MODE/#xdebug-notes","title":"Xdebug Notes","text":"<p>Set in <code>99-xdebug.ini</code>: <pre><code>xdebug.client_host=127.0.0.1\nxdebug.client_port=9003\nxdebug.discover_client_host=1\n</code></pre> Ensure your IDE is listening on 9003.</p>"},{"location":"DEV_PORTS_HOST_MODE/#troubleshooting","title":"Troubleshooting","text":"Symptom Cause Fix <code>Waiting for port 20211 to free...</code> repeats VS Code pre-bound the port via <code>forwardPorts</code> Remove the port from <code>forwardPorts</code>, rebuild, retry PHP request hangs at start Xdebug trying to connect to unresolved host (<code>host.docker.internal</code>) Use <code>127.0.0.1</code> or rely on discovery PORTS panel empty Expected in host mode Use the port enumeration task"},{"location":"DEV_PORTS_HOST_MODE/#future-improvements","title":"Future Improvements","text":"<ul> <li>Optional: add a small web status endpoint summarizing runtime ports.</li> <li>Optional: detect host mode in <code>setup.sh</code> and skip the wait loop if the PID using port is the intended process.</li> </ul>"},{"location":"DOCKER_COMPOSE/","title":"NetAlertX and Docker Compose","text":"<p>Warning</p> <p>\u26a0\ufe0f Important: The docker-compose has recently changed. Carefully read the Migration guide for detailed instructions.</p> <p>Great care is taken to ensure NetAlertX meets the needs of everyone while being flexible enough for anyone. This document outlines how you can configure your docker-compose. There are many settings, so we recommend using the Baseline Docker Compose as-is, or modifying it for your system.Good care is taken to ensure NetAlertX meets the needs of everyone while being flexible enough for anyone. This document outlines how you can configure your docker-compose. There are many settings, so we recommend using the Baseline Docker Compose as-is, or modifying it for your system.</p> <p>Note</p> <p>The container needs to run in <code>network_mode:\"host\"</code> to access Layer 2 networking such as arp, nmap and others. Due to lack of support for this feature, Windows host is not a supported operating system.</p>"},{"location":"DOCKER_COMPOSE/#baseline-docker-compose","title":"Baseline Docker Compose","text":"<p>There is one baseline for NetAlertX. That's the default security-enabled official distribution.</p> <pre><code>services:\n netalertx:\n #use an environmental variable to set host networking mode if needed\n container_name: netalertx # The name when you docker contiainer ls\n image: ghcr.io/netalertx/netalertx:latest\n network_mode: ${NETALERTX_NETWORK_MODE:-host} # Use host networking for ARP scanning and other services\n\n read_only: true # Make the container filesystem read-only\n cap_drop: # Drop all capabilities for enhanced security\n - ALL\n cap_add: # Add only the necessary capabilities\n - NET_ADMIN # Required for ARP scanning\n - NET_RAW # Required for raw socket operations\n - NET_BIND_SERVICE # Required to bind to privileged ports (nbtscan)\n - CHOWN # Required for root-entrypoint to chown /data + /tmp before dropping privileges\n - SETUID # Required for root-entrypoint to switch to non-root user\n - SETGID # Required for root-entrypoint to switch to non-root group\n\n volumes:\n - type: volume # Persistent Docker-managed named volume for config + database\n source: netalertx_data\n target: /data # `/data/config` and `/data/db` live inside this mount\n read_only: false\n\n # Example custom local folder called /home/user/netalertx_data\n # - type: bind\n # source: /home/user/netalertx_data\n # target: /data\n # read_only: false\n # ... or use the alternative format\n # - /home/user/netalertx_data:/data:rw\n\n - type: bind # Bind mount for timezone consistency\n source: /etc/localtime\n target: /etc/localtime\n read_only: true\n\n # Mount your DHCP server file into NetAlertX for a plugin to access\n # - path/on/host/to/dhcp.file:/resources/dhcp.file\n\n # tmpfs mount consolidates writable state for a read-only container and improves performance\n # uid/gid default to the service user (NETALERTX_UID/GID, default 20211)\n # mode=1700 grants rwx------ permissions to the runtime user only\n tmpfs:\n # Comment out to retain logs between container restarts - this has a server performance impact.\n - \"/tmp:uid=${NETALERTX_UID:-20211},gid=${NETALERTX_GID:-20211},mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime\"\n\n # Retain logs - comment out tmpfs /tmp if you want to retain logs between container restarts\n # Please note if you remove the /tmp mount, you must create and maintain sub-folder mounts.\n # - /path/on/host/log:/tmp/log\n # - \"/tmp/api:uid=${NETALERTX_UID:-20211},gid=${NETALERTX_GID:-20211},mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime\"\n # - \"/tmp/nginx:uid=${NETALERTX_UID:-20211},gid=${NETALERTX_GID:-20211},mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime\"\n # - \"/tmp/run:uid=${NETALERTX_UID:-20211},gid=${NETALERTX_GID:-20211},mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime\"\n\n environment:\n LISTEN_ADDR: ${LISTEN_ADDR:-0.0.0.0} # Listen for connections on all interfaces\n PORT: ${PORT:-20211} # Application port\n GRAPHQL_PORT: ${GRAPHQL_PORT:-20212} # GraphQL API port (passed into APP_CONF_OVERRIDE at runtime)\n # NETALERTX_DEBUG: ${NETALERTX_DEBUG:-0} # 0=kill all services and restart if any dies. 1 keeps running dead services.\n # PUID: 20211 # Runtime PUID override, set to 0 to run as root\n # PGID: 20211 # Runtime PGID override\n\n # Resource limits to prevent resource exhaustion\n mem_limit: 2048m # Maximum memory usage\n mem_reservation: 1024m # Soft memory limit\n cpu_shares: 512 # Relative CPU weight for CPU contention scenarios\n pids_limit: 512 # Limit the number of processes/threads to prevent fork bombs\n logging:\n options:\n max-size: \"10m\" # Rotate log files after they reach 10MB\n max-file: \"3\" # Keep a maximum of 3 log files\n\n # Always restart the container unless explicitly stopped\n restart: unless-stopped\n\nvolumes: # Persistent volume for configuration and database storage\n netalertx_data:\n</code></pre> <p>Run or re-run it:</p> <pre><code>docker compose up --force-recreate\n</code></pre> <p>Tip</p> <p>Runtime UID/GID: The image ships with a service user <code>netalertx</code> (UID/GID 20211) and a readonly lock owner also at 20211 for 004/005 immutability. If you override the runtime user (compose <code>user:</code> or <code>NETALERTX_UID/GID</code> vars), ensure your <code>/data</code> volume and tmpfs mounts use matching <code>uid/gid</code> so startup checks and writable paths succeed.</p>"},{"location":"DOCKER_COMPOSE/#customize-with-environmental-variables","title":"Customize with Environmental Variables","text":"<p>You can override the default settings by passing environmental variables to the <code>docker compose up</code> command.</p> <p>Example using a single variable:</p> <p>This command runs NetAlertX on port 8080 instead of the default 20211.</p> <pre><code>PORT=8080 docker compose up\n</code></pre> <p>Example using all available variables:</p> <p>This command demonstrates overriding all primary environmental variables: running with host networking, on port 20211, GraphQL on 20212, and listening on all IPs.</p> <pre><code>NETALERTX_NETWORK_MODE=host \\\nLISTEN_ADDR=0.0.0.0 \\\nPORT=20211 \\\nGRAPHQL_PORT=20212 \\\nNETALERTX_DEBUG=0 \\\ndocker compose up\n</code></pre>"},{"location":"DOCKER_COMPOSE/#docker-composeyaml-modifications","title":"<code>docker-compose.yaml</code> Modifications","text":""},{"location":"DOCKER_COMPOSE/#modification-1-use-a-local-folder-bind-mount","title":"Modification 1: Use a Local Folder (Bind Mount)","text":"<p>By default, the baseline compose file uses a single named volume (netalertx_data) mounted at <code>/data</code>. This single-volume layout is preferred because NetAlertX manages both configuration and the database under <code>/data</code> (for example, <code>/data/config</code> and <code>/data/db</code>) via its web UI. Using one named volume simplifies permissions and portability: Docker manages the storage and NetAlertX manages the files inside <code>/data</code>.</p> <p>A two-volume layout that mounts <code>/data/config</code> and <code>/data/db</code> separately (for example, <code>netalertx_config</code> and <code>netalertx_db</code>) is supported for backward compatibility and some advanced workflows, but it is an abnormal/legacy layout and not recommended for new deployments.</p> <p>However, if you prefer to have direct, file-level access to your configuration for manual editing, a \"bind mount\" is a simple alternative. This tells Docker to use a specific folder from your computer (the \"host\") inside the container.</p> <p>How to make the change:</p> <ol> <li> <p>Choose a location on your computer. For example, <code>/local_data_dir</code>.</p> </li> <li> <p>Create the subfolders: <code>mkdir -p /local_data_dir/config</code> and <code>mkdir -p /local_data_dir/db</code>.</p> </li> <li> <p>Edit your <code>docker-compose.yml</code> and find the <code>volumes:</code> section (the one inside the <code>netalertx:</code> service).</p> </li> <li> <p>Comment out (add a <code>#</code> in front) or delete the <code>type: volume</code> blocks for <code>netalertx_config</code> and <code>netalertx_db</code>.</p> </li> <li> <p>Add new lines pointing to your local folders.</p> </li> </ol> <p>Before (Using Named Volumes - Preferred):</p> <pre><code>...\n volumes:\n - netalertx_config:/data/config:rw #short-form volume (no /path is a short volume)\n - netalertx_db:/data/db:rw\n...\n</code></pre> <p>After (Using a Local Folder / Bind Mount): Make sure to replace <code>/local_data_dir</code> with your actual path. The format is <code>&lt;path_on_your_computer&gt;:&lt;path_inside_container&gt;:&lt;options&gt;</code>.</p> <pre><code>...\n volumes:\n# - netalertx_config:/data/config:rw\n# - netalertx_db:/data/db:rw\n - /local_data_dir/config:/data/config:rw\n - /local_data_dir/db:/data/db:rw\n...\n</code></pre> <p>Now, any files created by NetAlertX in <code>/data/config</code> will appear in your <code>/local_data_dir/config</code> folder.</p> <p>This same method works for mounting other things, like custom plugins or enterprise NGINX files, as shown in the commented-out examples in the baseline file.</p>"},{"location":"DOCKER_COMPOSE/#example-2-external-env-file-for-paths","title":"Example 2: External <code>.env</code> File for Paths","text":"<p>This method is useful for keeping your paths and other settings separate from your main compose file, making it more portable.</p> <p><code>docker-compose.yml</code> changes:</p> <pre><code>...\nservices:\n netalertx:\n environment:\n - PORT=${PORT}\n - GRAPHQL_PORT=${GRAPHQL_PORT}\n\n...\n</code></pre> <p><code>.env</code> file contents:</p> <pre><code>PORT=20211\nNETALERTX_NETWORK_MODE=host\nLISTEN_ADDR=0.0.0.0\nGRAPHQL_PORT=20212\n</code></pre> <p>Run with: <code>sudo docker-compose --env-file /path/to/.env up</code></p>"},{"location":"DOCKER_COMPOSE/#example-3-docker-swarm","title":"Example 3: Docker Swarm","text":"<p>This is for deploying on a Docker Swarm cluster. The key differences from the baseline are the removal of <code>network_mode:</code> from the service, and the addition of <code>deploy:</code> and <code>networks:</code> blocks at both the service and top-level.</p> <p>Here are the only changes you need to make to the baseline compose file to make it Swarm-compatible.</p> <pre><code>services:\n netalertx:\n ...\n # network_mode: ${NETALERTX_NETWORK_MODE:-host} # &lt;-- DELETE THIS LINE\n ...\n\n # 2. ADD a 'networks:' block INSIDE the service to connect to the external host network.\n networks:\n - outside\n # 3. ADD a 'deploy:' block to manage the service as a swarm replica.\n deploy:\n mode: replicated\n replicas: 1\n restart_policy:\n condition: on-failure\n\n\n# 4. ADD a new top-level 'networks:' block at the end of the file to define 'outside' as the external 'host' network.\nnetworks:\n outside:\n external:\n name: \"host\"\n</code></pre>"},{"location":"DOCKER_INSTALLATION/","title":"Docker Guide","text":""},{"location":"DOCKER_INSTALLATION/#netalertx-network-visibility-asset-intelligence-framework","title":"NetAlertX - Network Visibility &amp; Asset Intelligence Framework","text":""},{"location":"DOCKER_INSTALLATION/#docker-guide-releases-docs-plugins-website","title":"|| Docker guide || Releases || Docs || Plugins || Website","text":"<p>Head to https://netalertx.com/ for more gifs and screenshots \ud83d\udcf7.</p> <p>Note</p> <p>There is also an experimental \ud83e\uddea bare-metal install method available.</p>"},{"location":"DOCKER_INSTALLATION/#basic-usage","title":"\ud83d\udcd5 Basic Usage","text":"<p>Warning</p> <p>You will have to run the container on the <code>host</code> network and specify <code>SCAN_SUBNETS</code> unless you use other plugin scanners. The initial scan can take a few minutes, so please wait 5-10 minutes for the initial discovery to finish.</p> <pre><code>docker run -d --rm --network=host \\\n -v /local_data_dir:/data \\\n -v /etc/localtime:/etc/localtime \\\n --tmpfs /tmp:uid=${NETALERTX_UID:-20211},gid=${NETALERTX_GID:-20211},mode=1700 \\\n -e PORT=20211 \\\n -e APP_CONF_OVERRIDE={\"GRAPHQL_PORT\":\"20214\"} \\\n ghcr.io/netalertx/netalertx:latest\n</code></pre> <p>Runtime UID/GID: The image defaults to a service user <code>netalertx</code> (UID/GID 20211). A separate readonly lock owner also uses UID/GID 20211 for 004/005 immutability. You can override the runtime UID/GID at build (ARG) or run (<code>--user</code> / compose <code>user:</code>) but must align writable mounts (<code>/data</code>, <code>/tmp*</code>) and tmpfs <code>uid/gid</code> to that choice.</p> <p>See alternative docked-compose examples.</p>"},{"location":"DOCKER_INSTALLATION/#default-ports","title":"Default ports","text":"Default Description How to override <code>20211</code> Port of the web interface <code>-e PORT=20222</code> <code>20212</code> Port of the backend API server <code>-e APP_CONF_OVERRIDE={\"GRAPHQL_PORT\":\"20214\"}</code> or via the <code>GRAPHQL_PORT</code> Setting"},{"location":"DOCKER_INSTALLATION/#docker-environment-variables","title":"Docker environment variables","text":"Variable Description Example/Default Value <code>PUID</code> Runtime UID override, set to <code>0</code> to run as root. <code>20211</code> <code>PGID</code> Runtime GID override <code>20211</code> <code>PORT</code> Port of the web interface <code>20211</code> <code>LISTEN_ADDR</code> Set the specific IP Address for the listener address for the nginx webserver (web interface). This could be useful when using multiple subnets to hide the web interface from all untrusted networks. <code>0.0.0.0</code> <code>LOADED_PLUGINS</code> Default plugins to load. Plugins cannot be loaded with <code>APP_CONF_OVERRIDE</code>, you need to use this variable instead and then specify the plugins settings with <code>APP_CONF_OVERRIDE</code>. <code>[\"PIHOLE\",\"ASUSWRT\"]</code> <code>APP_CONF_OVERRIDE</code> JSON override for settings (except <code>LOADED_PLUGINS</code>). <code>{\"SCAN_SUBNETS\":\"['192.168.1.0/24 --interface=eth1']\",\"GRAPHQL_PORT\":\"20212\"}</code> <code>ALWAYS_FRESH_INSTALL</code> \u26a0 If <code>true</code> will delete the content of the <code>/db</code> &amp; <code>/config</code> folders. For testing purposes. Can be coupled with watchtower to have an always freshly installed <code>netalertx</code>/<code>netalertx-dev</code> image. <code>true</code> <p>You can override the default GraphQL port setting <code>GRAPHQL_PORT</code> (set to <code>20212</code>) by using the <code>APP_CONF_OVERRIDE</code> env variable. <code>LOADED_PLUGINS</code> and settings in <code>APP_CONF_OVERRIDE</code> can be specified via the UI as well.</p>"},{"location":"DOCKER_INSTALLATION/#docker-paths","title":"Docker paths","text":"<p>Note</p> <p>See also Backup strategies.</p> Required Path Description \u2705 <code>:/data</code> Folder which needs to contain a <code>/db</code> and <code>/config</code> sub-folders. \u2705 <code>/etc/localtime:/etc/localtime:ro</code> Ensuring the timezone is the same as on the server. <code>:/tmp/log</code> Logs folder useful for debugging if you have issues setting up the container <code>:/tmp/api</code> The API endpoint containing static (but regularly updated) json and other files. Path configurable via <code>NETALERTX_API</code> environment variable. <code>:/app/front/plugins/&lt;plugin&gt;/ignore_plugin</code> Map a file <code>ignore_plugin</code> to ignore a plugin. Plugins can be soft-disabled via settings. More in the Plugin docs. <code>:/etc/resolv.conf</code> Use a custom <code>resolv.conf</code> file for better name resolution."},{"location":"DOCKER_INSTALLATION/#folder-structure","title":"Folder structure","text":"<p>Use separate <code>db</code> and <code>config</code> directories, do not nest them:</p> <pre><code>data\n\u251c\u2500\u2500 config\n\u2514\u2500\u2500 db\n</code></pre>"},{"location":"DOCKER_INSTALLATION/#permissions","title":"Permissions","text":"<p>If you are facing permissions issues run the following commands on your server. This will change the owner and assure sufficient access to the database and config files that are stored in the <code>/local_data_dir/db</code> and <code>/local_data_dir/config</code> folders (replace <code>local_data_dir</code> with the location where your <code>/db</code> and <code>/config</code> folders are located).</p> <pre><code># Use the runtime UID/GID you intend to run with (default 20211:20211)\nsudo chown -R ${NETALERTX_UID:-20211}:${NETALERTX_GID:-20211} /local_data_dir\nsudo chmod -R a+rwx /local_data_dir\n</code></pre>"},{"location":"DOCKER_INSTALLATION/#initial-setup","title":"Initial setup","text":"<ul> <li>If unavailable, the app generates a default <code>app.conf</code> and <code>app.db</code> file on the first run.</li> <li>The preferred way is to manage the configuration via the Settings section in the UI, if UI is inaccessible you can modify app.conf in the <code>/data/config/</code> folder directly</li> </ul>"},{"location":"DOCKER_INSTALLATION/#setting-up-scanners","title":"Setting up scanners","text":"<p>You have to specify which network(s) should be scanned. This is done by entering subnets that are accessible from the host. If you use the default <code>ARPSCAN</code> plugin, you have to specify at least one valid subnet and interface in the <code>SCAN_SUBNETS</code> setting. See the documentation on How to set up multiple SUBNETS, VLANs and what are limitations for troubleshooting and more advanced scenarios.</p> <p>If you are running PiHole you can synchronize devices directly. Check the PiHole configuration guide for details.</p> <p>Note</p> <p>You can bulk-import devices via the CSV import method.</p>"},{"location":"DOCKER_INSTALLATION/#community-guides","title":"Community guides","text":"<p>You can read or watch several community configuration guides in Chinese, Korean, German, or French.</p> <p>Please note these might be outdated. Rely on official documentation first.</p>"},{"location":"DOCKER_INSTALLATION/#common-issues","title":"Common issues","text":"<ul> <li>Before creating a new issue, please check if a similar issue was already resolved.</li> <li>Check also common issues and debugging tips.</li> </ul>"},{"location":"DOCKER_INSTALLATION/#support-me","title":"\ud83d\udc99 Support me","text":"<ul> <li>Bitcoin: <code>1N8tupjeCK12qRVU2XrV17WvKK7LCawyZM</code></li> <li>Ethereum: <code>0x6e2749Cb42F4411bc98501406BdcD82244e3f9C7</code></li> </ul> <p>\ud83d\udce7 Email me at netalertx@gmail.com if you want to get in touch or if I should add other sponsorship platforms.</p>"},{"location":"DOCKER_MAINTENANCE/","title":"The NetAlertX Container Operator's Guide","text":"<p>Warning</p> <p>\u26a0\ufe0f Important: The docker-compose has recently changed. Carefully read the Migration guide for detailed instructions.</p> <p>This guide assumes you are starting with the official <code>docker-compose.yml</code> file provided with the project. We strongly recommend you start with or migrate to this file as your baseline and modify it to suit your specific needs (e.g., changing file paths). While there are many ways to configure NetAlertX, the default file is designed to meet the mandatory security baseline with layer-2 networking capabilities while operating securely and without startup warnings.</p> <p>This guide provides direct, concise solutions for common NetAlertX administrative tasks. It is structured to help you identify a problem, implement the solution, and understand the details.</p>"},{"location":"DOCKER_MAINTENANCE/#guide-contents","title":"Guide Contents","text":"<ul> <li>Using a Local Folder for Configuration</li> <li>Migrating from a Local Folder to a Docker Volume</li> <li>Applying a Custom Nginx Configuration</li> <li>Mounting Additional Files for Plugins</li> </ul> <p>Note</p> <p>Other relevant resources - Fixing Permission Issues - Handling Backups - Accessing Application Logs</p>"},{"location":"DOCKER_MAINTENANCE/#task-using-a-local-folder-for-configuration","title":"Task: Using a Local Folder for Configuration","text":""},{"location":"DOCKER_MAINTENANCE/#problem","title":"Problem","text":"<p>You want to edit your <code>app.conf</code> and other configuration files directly from your host machine, instead of using a Docker-managed volume.</p>"},{"location":"DOCKER_MAINTENANCE/#solution","title":"Solution","text":"<ol> <li>Stop the container:</li> </ol> <p><pre><code>docker-compose down\n</code></pre> 2. (Optional but Recommended) Back up your data using the method in Part 1. 3. Create a local folder on your host machine (e.g., <code>/data/netalertx_config</code>). 4. Edit <code>docker-compose.yml</code>:</p> <ul> <li>Comment out the <code>netalertx_config</code> volume entry.</li> <li>Uncomment and set the path for the \"Example custom local folder\" bind mount entry.</li> </ul> <p><pre><code>...\n volumes:\n # - type: volume\n # source: netalertx_config\n # target: /data/config\n # read_only: false\n...\n # Example custom local folder called /data/netalertx_config\n - type: bind\n source: /data/netalertx_config\n target: /data/config\n read_only: false\n...\n</code></pre> 5. (Optional) Restore your backup. 6. Restart the container:</p> <pre><code>docker-compose up -d\n</code></pre>"},{"location":"DOCKER_MAINTENANCE/#about-this-method","title":"About This Method","text":"<p>This replaces the Docker-managed volume with a \"bind mount.\" This is a direct mapping between a folder on your host computer (<code>/data/netalertx_config</code>) and a folder inside the container (<code>/data/config</code>), allowing you to edit the files directly.</p>"},{"location":"DOCKER_MAINTENANCE/#task-migrating-from-a-local-folder-to-a-docker-volume","title":"Task: Migrating from a Local Folder to a Docker Volume","text":""},{"location":"DOCKER_MAINTENANCE/#problem_1","title":"Problem","text":"<p>You are currently using a local folder (bind mount) for your configuration (e.g., <code>/data/netalertx_config</code>) and want to switch to the recommended Docker-managed volume (<code>netalertx_config</code>).</p>"},{"location":"DOCKER_MAINTENANCE/#solution_1","title":"Solution","text":"<ol> <li>Stop the container:</li> </ol> <p><pre><code>docker-compose down\n</code></pre> 2. Edit <code>docker-compose.yml</code>:</p> <ul> <li>Comment out the bind mount entry for your local folder.</li> <li>Uncomment the <code>netalertx_config</code> volume entry.</li> </ul> <p><pre><code>...\n volumes:\n - type: volume\n source: netalertx_config\n target: /data/config\n read_only: false\n...\n # Example custom local folder called /data/netalertx_config\n # - type: bind\n # source: /data/netalertx_config\n # target: /data/config\n # read_only: false\n...\n</code></pre> 3. (Optional) Initialize the volume:</p> <p><pre><code>docker-compose up -d &amp;&amp; docker-compose down\n</code></pre> 4. Run the migration command (replace <code>/data/netalertx_config</code> with your actual path):</p> <p><pre><code>docker run --rm -v netalertx_config:/config -v /data/netalertx_config:/local-config alpine \\\n sh -c \"tar -C /local-config -c . | tar -C /config -x\"\n</code></pre> 5. Restart the container:</p> <pre><code>docker-compose up -d\n</code></pre>"},{"location":"DOCKER_MAINTENANCE/#about-this-method_1","title":"About This Method","text":"<p>This uses a temporary <code>alpine</code> container that mounts both your source folder (<code>/local-config</code>) and destination volume (<code>/config</code>). The <code>tar ... | tar ...</code> command safely copies all files, including hidden ones, preserving structure.</p>"},{"location":"DOCKER_MAINTENANCE/#task-applying-a-custom-nginx-configuration","title":"Task: Applying a Custom Nginx Configuration","text":""},{"location":"DOCKER_MAINTENANCE/#problem_2","title":"Problem","text":"<p>You need to override the default Nginx configuration to add features like LDAP, SSO, or custom SSL settings.</p>"},{"location":"DOCKER_MAINTENANCE/#solution_2","title":"Solution","text":"<ol> <li>Stop the container:</li> </ol> <p><pre><code>docker-compose down\n</code></pre> 2. Create your custom config file on your host (e.g., <code>/data/my-netalertx.conf</code>). 3. Edit <code>docker-compose.yml</code>:</p> <p><pre><code>...\n # Use a custom Enterprise-configured nginx config for ldap or other settings\n - /data/my-netalertx.conf:/tmp/nginx/active-config/netalertx.conf:ro\n...\n</code></pre> 4. Restart the container:</p> <pre><code>docker-compose up -d\n</code></pre>"},{"location":"DOCKER_MAINTENANCE/#about-this-method_2","title":"About This Method","text":"<p>Docker\u2019s bind mount overlays your host file (<code>my-netalertx.conf</code>) on top of the default file inside the container. The container remains read-only, but Nginx reads your file as if it were the default.</p>"},{"location":"DOCKER_MAINTENANCE/#task-mounting-additional-files-for-plugins","title":"Task: Mounting Additional Files for Plugins","text":""},{"location":"DOCKER_MAINTENANCE/#problem_3","title":"Problem","text":"<p>A plugin (like <code>DHCPLSS</code>) needs to read a file from your host machine (e.g., <code>/var/lib/dhcp/dhcpd.leases</code>).</p>"},{"location":"DOCKER_MAINTENANCE/#solution_3","title":"Solution","text":"<ol> <li>Stop the container:</li> </ol> <p><pre><code>docker-compose down\n</code></pre> 2. Edit <code>docker-compose.yml</code> and add a new line under the <code>volumes:</code> section:</p> <p><pre><code>...\n volumes:\n...\n # Mount for DHCPLSS plugin\n - /var/lib/dhcp/dhcpd.leases:/mnt/dhcpd.leases:ro\n...\n</code></pre> 3. Restart the container:</p> <p><pre><code>docker-compose up -d\n</code></pre> 4. In the NetAlertX web UI, configure the plugin to read from:</p> <pre><code>/mnt/dhcpd.leases\n</code></pre>"},{"location":"DOCKER_MAINTENANCE/#about-this-method_3","title":"About This Method","text":"<p>This maps your host file to a new, read-only (<code>:ro</code>) location inside the container. The plugin can then safely read this file without exposing anything else on your host filesystem.</p>"},{"location":"DOCKER_PORTAINER/","title":"Deploying NetAlertX in Portainer (via Stacks)","text":"<p>This guide shows you how to set up NetAlertX using Portainer\u2019s Stacks feature.</p> <p></p>"},{"location":"DOCKER_PORTAINER/#1-prepare-your-host","title":"1. Prepare Your Host","text":"<p>Before deploying, make sure you have a folder on your Docker host for NetAlertX data. Replace <code>APP_FOLDER</code> with your preferred location, for example <code>/local_data_dir</code> here:</p> <pre><code>mkdir -p /local_data_dir/netalertx/config\nmkdir -p /local_data_dir/netalertx/db\nmkdir -p /local_data_dir/netalertx/log\n</code></pre>"},{"location":"DOCKER_PORTAINER/#2-open-portainer-stacks","title":"2. Open Portainer Stacks","text":"<ol> <li>Log in to your Portainer UI.</li> <li>Navigate to Stacks \u2192 Add stack.</li> <li>Give your stack a name (e.g., <code>netalertx</code>).</li> </ol>"},{"location":"DOCKER_PORTAINER/#3-paste-the-stack-configuration","title":"3. Paste the Stack Configuration","text":"<p>Copy and paste the following YAML into the Web editor:</p> <pre><code>services:\n netalertx:\n container_name: netalertx\n # Use this line for stable release\n image: \"ghcr.io/netalertx/netalertx:latest\"\n # Or, use this for the latest development build\n # image: \"ghcr.io/netalertx/netalertx-dev:latest\"\n network_mode: \"host\"\n restart: unless-stopped\n cap_drop: # Drop all capabilities for enhanced security\n - ALL\n cap_add: # Re-add necessary capabilities\n - NET_RAW\n - NET_ADMIN\n - NET_BIND_SERVICE\n - CHOWN\n - SETUID\n - SETGID\n volumes:\n - ${APP_FOLDER}/netalertx/config:/data/config\n - ${APP_FOLDER}/netalertx/db:/data/db\n # to sync with system time\n - /etc/localtime:/etc/localtime:ro\n tmpfs:\n # All writable runtime state resides under /tmp; comment out to persist logs between restarts\n - \"/tmp:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime\"\n environment:\n - PORT=${PORT}\n - APP_CONF_OVERRIDE=${APP_CONF_OVERRIDE}\n</code></pre>"},{"location":"DOCKER_PORTAINER/#4-configure-environment-variables","title":"4. Configure Environment Variables","text":"<p>In the Environment variables section of Portainer, add the following:</p> <ul> <li><code>APP_FOLDER=/local_data_dir</code> (or wherever you created the directories in step 1)</li> <li><code>PORT=22022</code> (or another port if needed)</li> <li><code>APP_CONF_OVERRIDE={\"GRAPHQL_PORT\":\"22023\"}</code> (optional advanced settings, otherwise the backend API server PORT defaults to <code>20212</code>)</li> </ul> <p>Additional environment variables (advanced / testing):</p> <ul> <li><code>SKIP_TESTS=1</code> \u2014 when set, the container entrypoint will skip all startup checks and print the message <code>Skipping startup checks as SKIP_TESTS is set.</code>. Useful for automated test runs or CI where the container should not perform environment-specific checks.</li> <li><code>SKIP_STARTUP_CHECKS=\"&lt;check names&gt;\"</code> \u2014 space-delimited list of specific startup checks to skip. Names are the human-friendly names derived from files in <code>/entrypoint.d</code> (remove the leading numeric prefix and file extension). Example: <code>SKIP_STARTUP_CHECKS=\"mandatory folders\"</code> will skip <code>30-mandatory-folders.sh</code>.</li> </ul> <p>Note: these variables are primarily useful for non-production scenarios (testing, CI, or specific deployments) and are processed by the entrypoint scripts. See <code>entrypoint.sh</code> and <code>entrypoint.d/*</code> for exact behaviour and available check names.</p>"},{"location":"DOCKER_PORTAINER/#5-ensure-permissions","title":"5. Ensure permissions","text":"<p>Tip</p> <p>If you are facing permissions issues run the following commands on your server. This will change the owner and assure sufficient access to the database and config files that are stored in the <code>/local_data_dir/db</code> and <code>/local_data_dir/config</code> folders (replace <code>local_data_dir</code> with the location where your <code>/db</code> and <code>/config</code> folders are located).</p> <p><code>sudo chown -R 20211:20211 /local_data_dir</code></p> <p><code>sudo chmod -R a+rwx /local_data_dir</code></p>"},{"location":"DOCKER_PORTAINER/#6-deploy-the-stack","title":"6. Deploy the Stack","text":"<ol> <li>Scroll down and click Deploy the stack.</li> <li>Portainer will pull the image and start NetAlertX.</li> <li>Once running, access the app at:</li> </ol> <pre><code>http://&lt;your-docker-host-ip&gt;:22022\n</code></pre>"},{"location":"DOCKER_PORTAINER/#7-verify-and-troubleshoot","title":"7. Verify and Troubleshoot","text":"<ul> <li>Check logs via Portainer \u2192 Containers \u2192 <code>netalertx</code> \u2192 Logs.</li> <li>Logs are stored under <code>${APP_FOLDER}/netalertx/log</code> if you enabled that volume.</li> </ul> <p>Once the application is running, configure it by reading the initial setup guide, or troubleshoot common issues.</p>"},{"location":"DOCKER_SWARM/","title":"Docker Swarm Deployment Guide (IPvlan)","text":"<p>Note</p> <p>This is community-contributed. Due to environment, setup, or networking differences, results may vary. Please open a PR to improve it instead of creating an issue, as the maintainer is not actively maintaining it.</p> <p>This guide describes how to deploy NetAlertX in a Docker Swarm environment using an <code>ipvlan</code> network. This enables the container to receive a LAN IP address directly, which is ideal for network monitoring.</p>"},{"location":"DOCKER_SWARM/#step-1-create-an-ipvlan-config-only-network-on-all-nodes","title":"\u2699\ufe0f Step 1: Create an IPvlan Config-Only Network on All Nodes","text":"<p>Run this command on each node in the Swarm.</p> <pre><code>docker network create -d ipvlan \\\n --subnet=192.168.1.0/24 \\ # \ud83d\udd27 Replace with your LAN subnet\n --gateway=192.168.1.1 \\ # \ud83d\udd27 Replace with your LAN gateway\n -o ipvlan_mode=l2 \\\n -o parent=eno1 \\ # \ud83d\udd27 Replace with your network interface (e.g., eth0, eno1)\n --config-only \\\n ipvlan-swarm-config\n</code></pre>"},{"location":"DOCKER_SWARM/#step-2-create-the-swarm-scoped-ipvlan-network-one-time-setup","title":"\ud83d\udda5\ufe0f Step 2: Create the Swarm-Scoped IPvlan Network (One-Time Setup)","text":"<p>Run this on one Swarm manager node only.</p> <pre><code>docker network create -d ipvlan \\\n --scope swarm \\\n --config-from ipvlan-swarm-config \\\n swarm-ipvlan\n</code></pre>"},{"location":"DOCKER_SWARM/#step-3-deploy-netalertx-with-docker-compose","title":"\ud83e\uddfe Step 3: Deploy NetAlertX with Docker Compose","text":"<p>Use the following Compose snippet to deploy NetAlertX with a static LAN IP assigned via the <code>swarm-ipvlan</code> network.</p> <pre><code>services:\n netalertx:\n image: ghcr.io/netalertx/netalertx:latest\n...\n networks:\n swarm-ipvlan:\n ipv4_address: 192.168.1.240 # \u26a0\ufe0f Choose a free IP from your LAN\n deploy:\n mode: replicated\n replicas: 1\n restart_policy:\n condition: on-failure\n placement:\n constraints:\n - node.role == manager # \ud83d\udd04 Or use: node.labels.netalertx == true\n\nnetworks:\n swarm-ipvlan:\n external: true\n</code></pre>"},{"location":"DOCKER_SWARM/#notes","title":"\u2705 Notes","text":"<ul> <li>The <code>ipvlan</code> setup allows NetAlertX to have a direct IP on your LAN.</li> <li>Replace <code>eno1</code> with your interface, IP addresses, and volume paths to match your environment.</li> <li>Make sure the assigned IP (<code>192.168.1.240</code> above) is not in use or managed by DHCP.</li> <li>You may also use a node label constraint instead of <code>node.role == manager</code> for more control.</li> </ul>"},{"location":"FEATURES/","title":"NetAlertX Features Overview","text":"<p>NetAlertX is a lightweight, flexible platform for monitoring networks, tracking devices, and delivering actionable alerts. It combines discovery, change detection, and multi-channel notification into a single, streamlined solution.</p>"},{"location":"FEATURES/#network-discovery-device-tracking","title":"Network Discovery &amp; Device Tracking","text":"<ul> <li>Automatic Device Detection: Continuously scans your local network to detect all connected devices via ARP, DHCP, SNMP, and compatible controllers.</li> <li>Presence Monitoring: Track when devices appear, disappear, or reconnect on the network.</li> <li>IP &amp; MAC Tracking: Log device IP changes, ensuring accurate identification over time.</li> <li>Import from Existing Systems: Integrates with DHCP servers, Pi-hole, UniFi controllers, and other supported sources to maintain an accurate inventory.</li> </ul>"},{"location":"FEATURES/#lan-visualization","title":"LAN Visualization","text":"<ul> <li>Lightweight Network Map: View a real-time representation of your local network with all connected devices.</li> <li>Device Status Indicators: Quickly identify active, missing, or new devices at a glance.</li> <li>Interactive Overview: Hover over devices to see IP, MAC, and last seen timestamps.</li> <li>Change Highlighting: Newly detected, disconnected, or reconnected devices are visually flagged to reduce oversight.</li> <li>Simple &amp; Efficient: Designed for quick insights without heavy resource usage or complex topology maps.</li> </ul>"},{"location":"FEATURES/#event-driven-alerts","title":"Event-Driven Alerts","text":"<ul> <li>Real-Time Notifications: Receive immediate alerts for new devices, disconnected devices, or unexpected changes.</li> <li>Customizable Filters and Rules: Define rules based on device type, IP ranges, presence, or other network parameters.</li> <li>Alert Deduplication &amp; Suppression: Avoid unnecessary noise with smart alert handling.</li> <li>Historical Logs: Maintain a complete timeline of network events for review and reporting.</li> </ul>"},{"location":"FEATURES/#workflows-for-implementing-business-rules","title":"Workflows for implementing Business rules","text":"<ul> <li>Custom rules: Cretae custom flows and update device information based to scan results.</li> <li>Customizable Triggers: Define rules based on any device data, including device type, IP ranges, presence, or other network parameters.</li> <li>Automated Updates: Automate repetitive tasks, making network management more efficient.</li> </ul>"},{"location":"FEATURES/#multi-channel-notification","title":"Multi-Channel Notification","text":"<ul> <li>Flexible Delivery Options: Send alerts via email, webhooks, MQTT, and more.</li> <li>Integration with Automation: Connect to ticketing systems, workflow engines, and custom scripts for automated responses.</li> <li>Apprise Support: Utilize over 80 pre-built notification services without additional configuration.</li> </ul>"},{"location":"FEATURES/#security-compliance-friendly-logging","title":"Security &amp; Compliance-Friendly Logging","text":"<ul> <li>Device Accountability: Maintain an auditable record of every device that appears or disappears from the network.</li> <li>Change Tracking: Document network events with timestamps for review and compliance reporting.</li> <li>Rogue Device Alerts: Detect and respond to unexpected or unauthorized network connections.</li> </ul>"},{"location":"FEATURES/#mcp-server-and-openapi","title":"MCP Server and OpenAPI","text":"<ul> <li>Data Access &amp; Interaction: The MCP server provides full programmatic access to NetAlertX, allowing you to query, monitor, and interact with network and device data.</li> <li>OpenAPI Integration: Use the OpenAPI interface to fetch device status, network events, and logs, or trigger actions and alerts programmatically.</li> <li>Full Transparency: All scan results, logs, and device information are accessible via the API, enabling auditing, automation, or integration with external systems.</li> <li>Flexible &amp; Reliable: Structured API access ensures predictable, repeatable interactions while allowing real-time data monitoring and operational control.</li> </ul>"},{"location":"FEATURES/#extensible-open-source","title":"Extensible &amp; Open Source","text":"<ul> <li>Plugin System: Extend discovery methods, ingestion types, or notification channels through modular plugins.</li> <li>Community Contributions: Open-source architecture encourages collaboration and improvements.</li> <li>Full Transparency: All logs, scans, and configurations are visible for analysis.</li> </ul> <p>NetAlertX provides a centralized, proactive approach to network awareness, combining device visibility, event-driven alerting, and flexible notifications into a single, deployable solution. Its design prioritizes efficiency, clarity, and actionable insights, making it ideal for monitoring dynamic environments.</p>"},{"location":"FILE_PERMISSIONS/","title":"Managing File Permissions for NetAlertX on a Read-Only Container","text":"<p>Sometimes, permission issues arise if your existing host directories were created by a previous container running as root or another UID. The container will fail to start with \"Permission Denied\" errors.</p> <p>Tip</p> <p>NetAlertX runs in a secure, read-only Alpine-based container under a dedicated <code>netalertx</code> user (UID 20211, GID 20211). All writable paths are either mounted as persistent volumes or <code>tmpfs</code> filesystems. This ensures consistent file ownership and prevents privilege escalation.</p> <p>Try starting the container with all data to be in non-persistent volumes. If this works, the issue might be related to the permissions of your persistent data mount locations on your server.</p> <pre><code>docker run --rm --network=host \\\n -v /etc/localtime:/etc/localtime:ro \\\n --tmpfs /tmp:uid=20211,gid=20211,mode=1700 \\\n -e PORT=20211 \\\n ghcr.io/netalertx/netalertx:latest\n</code></pre> <p>Warning</p> <p>The above should be only used as a test - once the container restarts, all data is lost.</p>"},{"location":"FILE_PERMISSIONS/#writable-paths","title":"Writable Paths","text":"<p>NetAlertX requires certain paths to be writable at runtime. These paths should be mounted either as host volumes or <code>tmpfs</code> in your <code>docker-compose.yml</code> or <code>docker run</code> command:</p> Path Purpose Notes <code>/data/config</code> Application configuration Persistent volume recommended <code>/data/db</code> Database files Persistent volume recommended <code>/tmp/log</code> Logs Lives under <code>/tmp</code>; optional host bind to retain logs <code>/tmp/api</code> API cache Subdirectory of <code>/tmp</code> <code>/tmp/nginx/active-config</code> Active nginx configuration override Mount <code>/tmp</code> (or override specific file) <code>/tmp/run</code> Runtime directories for nginx &amp; PHP Subdirectory of <code>/tmp</code> <code>/tmp</code> PHP session save directory Backed by <code>tmpfs</code> for runtime writes <p>Mounting <code>/tmp</code> as <code>tmpfs</code> automatically covers all of its subdirectories (<code>log</code>, <code>api</code>, <code>run</code>, <code>nginx/active-config</code>, etc.).</p> <p>All these paths will have UID 20211 / GID 20211 inside the container. Files on the host will appear owned by <code>20211:20211</code>.</p>"},{"location":"FILE_PERMISSIONS/#running-as-root","title":"Running as <code>root</code>","text":"<p>You can override the default PUID and PGID using environment variables:</p> <pre><code>...\n environment:\n PUID: 20211 # Runtime PUID override, set to 0 to run as root\n PGID: 20211 # Runtime PGID override\n...\n</code></pre> <p>To run as the root user, it usually looks like this (verify the IDs on your server first by executing <code>id root</code>):</p> <pre><code>...\n environment:\n PUID: 0 # Runtime PUID override, set to 0 to run as root\n PGID: 100 # Runtime PGID override\n...\n</code></pre> <p>If you use a custom <code>PUID</code> (e.g. <code>0</code>) and <code>GUID</code> (e.g. <code>100</code>) make sure you also update the <code>tmpfs</code> ownership, e.g. <code>/tmp:uid=0,gid=100...</code></p>"},{"location":"FILE_PERMISSIONS/#solution","title":"Solution","text":"<ol> <li>Run the container once as root (<code>--user \"0\"</code>) to allow it to correct permissions automatically:</li> </ol> <pre><code>docker run -it --rm --name netalertx --user \"0\" \\\n -v /local_data_dir:/data \\\n --tmpfs /tmp:uid=20211,gid=20211,mode=1700 \\\n ghcr.io/netalertx/netalertx:latest\n</code></pre> <ol> <li>Wait for logs showing permissions being fixed. The container will then hang intentionally.</li> <li>Press Ctrl+C to stop the container.</li> <li>Start the container normally with your <code>docker-compose.yml</code> or <code>docker run</code> command.</li> </ol> <p>The container startup script detects <code>root</code> and runs <code>chown -R 20211:20211</code> on all volumes, fixing ownership for the secure <code>netalertx</code> user.</p> <p>Tip</p> <p>If you are facing permissions issues run the following commands on your server. This will change the owner and assure sufficient access to the database and config files that are stored in the <code>/local_data_dir/db</code> and <code>/local_data_dir/config</code> folders (replace <code>local_data_dir</code> with the location where your <code>/db</code> and <code>/config</code> folders are located).</p> <p><code>sudo chown -R 20211:20211 /local_data_dir</code></p> <p><code>sudo chmod -R a+rwx /local_data_dir</code></p>"},{"location":"FILE_PERMISSIONS/#example-docker-composeyml-with-tmpfs","title":"Example: docker-compose.yml with <code>tmpfs</code>","text":"<pre><code>services:\n netalertx:\n container_name: netalertx\n image: \"ghcr.io/netalertx/netalertx\"\n network_mode: \"host\"\n cap_drop: # Drop all capabilities for enhanced security\n - ALL\n cap_add: # Add only the necessary capabilities\n - NET_ADMIN # Required for ARP scanning\n - NET_RAW # Required for raw socket operations\n - NET_BIND_SERVICE # Required to bind to privileged ports (nbtscan)\n restart: unless-stopped\n volumes:\n - /local_data_dir:/data\n - /etc/localtime:/etc/localtime\n environment:\n - PORT=20211\n tmpfs:\n - \"/tmp:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime\"\n</code></pre> <p>This setup ensures all writable paths are either in <code>tmpfs</code> or host-mounted, and the container never writes outside of controlled volumes.</p>"},{"location":"FIX_OFFLINE_DETECTION/","title":"Troubleshooting: Devices Show Offline When They Are Online","text":"<p>In some network setups, certain devices may intermittently appear as offline in NetAlertX, even though they are connected and responsive. This issue is often more noticeable with devices that have higher IP addresses within the subnet.</p> <p>Note</p> <p>Network presence graph showing increased drop outs before enabling additional <code>ICMP</code> scans and continuous online presence after following this guide. This graph shows a sudden spike in drop outs probably caused by a device software update. </p>"},{"location":"FIX_OFFLINE_DETECTION/#symptoms","title":"Symptoms","text":"<ul> <li>Devices sporadically show as offline in the presence timeline.</li> <li>This behavior often affects devices with higher IPs (e.g., <code>192.168.1.240+</code>).</li> <li>Presence data appears inconsistent or unreliable despite the device being online.</li> </ul>"},{"location":"FIX_OFFLINE_DETECTION/#cause","title":"Cause","text":"<p>This issue is typically related to scanning limitations:</p> <ul> <li>ARP scan timeouts may prevent full subnet coverage.</li> <li> <p>Sole reliance on ARP can result in missed detections:</p> </li> <li> <p>Some devices (like iPhones) suppress or reject frequent ARP requests.</p> </li> <li> <p>ARP responses may be blocked or delayed due to power-saving features or OS behavior.</p> </li> <li> <p>Scanning frequency conflicts, where devices ignore repeated scans within a short period.</p> </li> </ul>"},{"location":"FIX_OFFLINE_DETECTION/#recommended-fixes","title":"Recommended Fixes","text":"<p>To improve presence accuracy and reduce false offline states:</p>"},{"location":"FIX_OFFLINE_DETECTION/#increase-arp-scan-timeout","title":"\u2705 Increase ARP Scan Timeout","text":"<p>Extend the ARP scanner timeout and DURATION to ensure full subnet coverage:</p> <pre><code>ARPSCAN_RUN_TIMEOUT=360\nARPSCAN_DURATION=30\n</code></pre> <p>Adjust based on your network size and device count.</p>"},{"location":"FIX_OFFLINE_DETECTION/#add-icmp-ping-scanning","title":"\u2705 Add ICMP (Ping) Scanning","text":"<p>Enable the <code>ICMP</code> scan plugin to complement ARP detection. ICMP is often more reliable for detecting active hosts, especially when ARP fails.</p> <p>Important</p> <p>If using AdGuard/Pi-hole: If devices still show offline after enabling ICMP, temporarily disable your content blocker. If the issue disappears, whitelist the NetAlertX host IP in your blocker's settings to prevent pings from being dropped.</p>"},{"location":"FIX_OFFLINE_DETECTION/#use-multiple-detection-methods","title":"\u2705 Use Multiple Detection Methods","text":"<p>A combined approach greatly improves detection robustness:</p> <ul> <li><code>ARPSCAN</code> (default)</li> <li><code>ICMP</code> (ping)</li> <li><code>NMAPDEV</code> (nmap)</li> </ul> <p>This hybrid strategy increases reliability, especially for down detection and alerting. See other plugins that might be compatible with your setup. See benefits and drawbacks of individual scan methods in their respective docs.</p>"},{"location":"FIX_OFFLINE_DETECTION/#results","title":"Results","text":"<p>After increasing the ARP timeout and adding ICMP scanning (on select IP ranges), users typically report:</p> <ul> <li>More consistent presence graphs</li> <li>Fewer false offline events</li> <li>Better coverage across all IP ranges</li> </ul>"},{"location":"FIX_OFFLINE_DETECTION/#summary","title":"Summary","text":"Setting Recommendation <code>ARPSCAN_RUN_TIMEOUT</code> Increase to ensure scans reach all IPs <code>ICMP</code> Scan Enable to detect devices ARP might miss Multi-method Scanning Use a mix of ARP, ICMP, and NMAP-based methods <p>Tip: Each environment is unique. Consider fine-tuning scan settings based on your network size, device behavior, and desired detection accuracy.</p> <p>Let us know in the NetAlertX Discussions if you have further feedback or edge cases.</p> <p>See also Remote Networks for more advanced setups.</p>"},{"location":"FRONTEND_DEVELOPMENT/","title":"Frontend development","text":"<p>This page contains tips for frontend development when extending NetAlertX. Guiding principles are:</p> <ol> <li>Maintainability</li> <li>Extendability</li> <li>Reusability</li> <li>Placing more functionality into Plugins and enhancing core Plugins functionality</li> </ol> <p>That means that, when writing code, focus on reusing what's available instead of writing quick fixes. Or creating reusable functions, instead of bespoke functionaility.</p>"},{"location":"FRONTEND_DEVELOPMENT/#examples","title":"\ud83d\udd0d Examples","text":"<p>Some examples how to apply the above:</p> <p>Example 1</p> <p>I want to implement a scan fucntion. Options would be:</p> <ol> <li>To add a manual scan functionality to the <code>deviceDetails.php</code> page.</li> <li>To create a separate page that handles the execution of the scan.</li> <li>To create a configurable Plugin.</li> </ol> <p>From the above, number 3 would be the most appropriate solution. Then followed by number 2. Number 1 would be approved only in special circumstances.</p> <p>Example 2</p> <p>I want to change the behavior of the application. Options to implement this could be:</p> <ol> <li>Hard-code the changes in the code.</li> <li>Implement the changes and add settings to influence the behavior in the <code>initialize.py</code> file so the user can adjust these.</li> <li>Implement the changes and add settings via a setting-only plugin.</li> <li>Implement the changes in a way so the behavior can be toggled on each plugin so the core capabilities of Plugins get extended.</li> </ol> <p>From the above, number 4 would be the most appropriate solution. Then followed by number 3. Number 1 or 2 would be approved only in special circumstances.</p>"},{"location":"FRONTEND_DEVELOPMENT/#frontend-tips","title":"\ud83d\udca1 Frontend tips","text":"<p>Some useful frontend JavaScript functions:</p> <ul> <li><code>getDevDataByMac(macAddress, devicesColumn)</code> - method to retrieve any device data (database column) based on MAC address in the frontend</li> <li><code>getString(string stringKey)</code> - method to retrieve translated strings in the frontend</li> <li><code>getSetting(string stringKey)</code> - method to retrieve settings in the frontend</li> </ul> <p>Check the common.js file for more frontend functions.</p>"},{"location":"HELPER_SCRIPTS/","title":"Community Helper Scripts Overview","text":"<p>This page provides an overview of community-contributed scripts for NetAlertX. These scripts are not actively maintained and are provided as-is.</p>"},{"location":"HELPER_SCRIPTS/#community-scripts","title":"Community Scripts","text":"<p>You can find all scripts in this scripts GitHub folder.</p> Script Name Description Author Version Release Date New Devices Checkmk Script Checks for new devices in NetAlertX and reports status to Checkmk. N/A 1.0 08-Jan-2025 DB Cleanup Script Queries and removes old device-related entries from the database. laxduke 1.0 23-Dec-2024 OPNsense DHCP Lease Converter Retrieves DHCP lease data from OPNsense and converts it to <code>dnsmasq</code> format. im-redactd 1.0 24-Feb-2025"},{"location":"HELPER_SCRIPTS/#important-notes","title":"Important Notes","text":"<p>Note</p> <p>These scripts are community-supplied and not actively maintained. Use at your own discretion.</p> <p>For detailed usage instructions, refer to each script's documentation in each scripts GitHub folder.</p>"},{"location":"HOME_ASSISTANT/","title":"Home Assistant integration overview","text":"<p>NetAlertX comes with MQTT support, allowing you to show all detected devices as devices in Home Assistant. It also supplies a collection of stats, such as number of online devices.</p> <p>Tip</p> <p>You can install NetAlertX also as a Home Assistant addon via the alexbelgium/hassio-addons repository. This is only possible if you run a supervised instance of Home Assistant. If not, you can still run NetAlertX in a separate Docker container and follow this guide to configure MQTT.</p>"},{"location":"HOME_ASSISTANT/#note","title":"\u26a0 Note","text":"<ul> <li>Please note that discovery takes about ~10s per device.</li> <li>Deleting of devices is not handled automatically. Please use MQTT Explorer to delete devices in the broker (Home Assistant), if needed.</li> <li>For optimization reasons, the devices are not always fully synchronized. You can delete Plugin objects as described in the MQTT plugin docs to force a full synchronization.</li> </ul>"},{"location":"HOME_ASSISTANT/#guide","title":"\ud83e\udded Guide","text":"<p>\ud83d\udca1 This guide was tested only with the Mosquitto MQTT broker</p> <ol> <li> <p>Enable Mosquitto MQTT in Home Assistant by following the documentation</p> </li> <li> <p>Configure a user name and password on your broker.</p> </li> <li> <p>Note down the following details that you will need to configure NetAlertX:</p> <ul> <li>MQTT host url (usually your Home Assistant IP)</li> <li>MQTT broker port</li> <li>User</li> <li>Password</li> </ul> </li> <li> <p>Open the NetAlertX &gt; Settings &gt; MQTT settings group</p> <ul> <li>Enable MQTT</li> <li>Fill in the details from above</li> <li>Fill in remaining settings as per description</li> <li>set MQTT_RUN to schedule or on_notification depending on requirements</li> </ul> </li> </ol> <p></p>"},{"location":"HOME_ASSISTANT/#screenshots","title":"\ud83d\udcf7 Screenshots","text":""},{"location":"HOME_ASSISTANT/#troubleshooting","title":"Troubleshooting","text":"<p>If you can't see all devices detected, run <code>sudo arp-scan --interface=eth0 192.168.1.0/24</code> (change these based on your setup, read Subnets docs for details). This command has to be executed the NetAlertX container, not in the Home Assistant container.</p> <p>You can access the NetAlertX container via Portainer on your host or via ssh. The container name will be something like <code>addon_db21ed7f_netalertx</code> (you can copy the <code>db21ed7f_netalertx</code> part from the browser when accessing the UI of NetAlertX).</p>"},{"location":"HOME_ASSISTANT/#accessing-the-netalertx-container-via-ssh","title":"Accessing the NetAlertX container via SSH","text":"<ol> <li>Log into your Home Assistant host via SSH</li> </ol> <p><pre><code>local@local:~ $ ssh pi@192.168.1.9\n</code></pre> 2. Find the NetAlertX container name, in this case <code>addon_db21ed7f_netalertx</code></p> <pre><code>pi@raspberrypi:~ $ sudo docker container ls | grep netalertx\n06c540d97f67 ghcr.io/alexbelgium/netalertx-armv7:25.3.1 \"/init\" 6 days ago Up 6 days (healthy) addon_db21ed7f_netalertx\n</code></pre> <ol> <li>SSH into the NetAlertX cointainer</li> </ol> <pre><code>pi@raspberrypi:~ $ sudo docker exec -it addon_db21ed7f_netalertx /bin/sh\n/ #\n</code></pre> <ol> <li>Execute a test <code>asrp-scan</code> scan</li> </ol> <pre><code>/ # sudo arp-scan --ignoredups --retry=6 192.168.1.0/24 --interface=eth0\nInterface: eth0, type: EN10MB, MAC: dc:a6:32:73:8a:b1, IPv4: 192.168.1.9\nStarting arp-scan 1.10.0 with 256 hosts (https://github.com/royhills/arp-scan)\n192.168.1.1 74:ac:b9:54:09:fb Ubiquiti Networks Inc.\n192.168.1.21 74:ac:b9:ad:c3:30 Ubiquiti Networks Inc.\n192.168.1.58 1c:69:7a:a2:34:7b EliteGroup Computer Systems Co., LTD\n192.168.1.57 f4:92:bf:a3:f3:56 Ubiquiti Networks Inc.\n...\n</code></pre> <p>If your result doesn't contain results similar to the above, double check your subnet, interface and if you are dealing with an inaccessible network segment, read the Remote networks documentation.</p>"},{"location":"HW_INSTALL/","title":"How to install NetAlertX on the server hardware","text":"<p>To download and install NetAlertX on the hardware/server directly use the <code>curl</code> or <code>wget</code> commands at the bottom of this page.</p> <p>Note</p> <p>This is an Experimental feature \ud83e\uddea and it relies on community support.</p> <p>\ud83d\ude4f Looking for maintainers for this installation method \ud83d\ude42 Current community volunteers: - slammingprogramming - ingoratsdorf</p> <p>There is no guarantee that the install script or any other script will gracefully handle other installed software. Data loss is a possibility, it is recommended to install NetAlertX using the supplied Docker image.</p> <p>Warning</p> <p>A warning to the installation method below: Piping to bash is controversial and may be dangerous, as you cannot see the code that's about to be executed on your system.</p> <p>If you trust this repo, you can download the install script via one of the methods (curl/wget) below and it will fo its best to install NetAlertX on your system.</p> <p>Alternatively you can download the installation script from the repository and check the code yourself.</p> <p>NetAlertX will be installed in <code>/app</code> and run on port number <code>20211</code>.</p> <p>Some facts about what and where something will be changed/installed by the HW install setup (may not contain everything!):</p> <ul> <li>dependencies will be installed from the respective system repos</li> <li>required python modules will be installed</li> <li><code>/app</code> directory will be deleted and newly created</li> <li><code>/app</code> will contain the whole repository (downloaded by the install script)</li> <li>The default NGINX site <code>/etc/nginx/sites-enabled/default</code> will be disabled (sym-link deleted or backed up to <code>sites-available</code>)</li> <li><code>/var/www/html/netalertx</code> directory will be deleted and newly created</li> <li><code>/etc/nginx/conf.d/netalertx.conf</code> will be sym-linked to the appropriate installer location (depending on your system installer script)</li> <li>Some files (IEEE device vendors info, ...) will be created in the directory where the installation script is executed</li> </ul>"},{"location":"HW_INSTALL/#limitations","title":"Limitations","text":"<ul> <li>No system service is provided. NetAlertX must be started using <code>/app/install/&lt;system&gt;/start.&lt;system&gt;.sh</code>.</li> <li>No checks for other running software is done.</li> <li>Only tested to work on the system listed in the install directory.</li> <li>EXPERIMENTAL and not recommended way to install NetAlertX.</li> </ul> <p>Tip</p> <p>If the below fails try grabbing and installing one of the previous releases and run the installation from the zip package.</p> <p>These commands will download the <code>install.debian12.sh</code> script from the GitHub repository, make it executable with <code>chmod</code>, and then run it using <code>./install.debian12.sh</code>.</p> <p>Make sure you have the necessary permissions to execute the script.</p>"},{"location":"HW_INSTALL/#debian-12-bookworm","title":"\ud83d\udce5 Debian 12 (Bookworm)","text":""},{"location":"HW_INSTALL/#installation-via-curl","title":"Installation via curl","text":"<pre><code>curl -o install.debian12.sh https://raw.githubusercontent.com/jokob-sk/NetAlertX/main/install/debian12/install.debian12.sh &amp;&amp; sudo chmod +x install.debian12.sh &amp;&amp; sudo ./install.debian12.sh\n</code></pre>"},{"location":"HW_INSTALL/#installation-via-wget","title":"Installation via wget","text":"<pre><code>wget https://raw.githubusercontent.com/jokob-sk/NetAlertX/main/install/debian12/install.debian12.sh -O install.debian12.sh &amp;&amp; sudo chmod +x install.debian12.sh &amp;&amp; sudo ./install.debian12.sh\n</code></pre>"},{"location":"HW_INSTALL/#ubuntu-24-noble-numbat","title":"\ud83d\udce5 Ubuntu 24 (Noble Numbat)","text":"<p>Note</p> <p>Maintained by ingoratsdorf</p>"},{"location":"HW_INSTALL/#installation-via-curl_1","title":"Installation via curl","text":"<pre><code>curl -o install.sh https://raw.githubusercontent.com/jokob-sk/NetAlertX/main/install/ubuntu24/install.sh &amp;&amp; sudo chmod +x install.sh &amp;&amp; sudo ./install.sh\n</code></pre>"},{"location":"HW_INSTALL/#installation-via-wget_1","title":"Installation via wget","text":"<pre><code>wget https://raw.githubusercontent.com/jokob-sk/NetAlertX/main/install/ubuntu24/install.sh -O install.sh &amp;&amp; sudo chmod +x install.sh &amp;&amp; sudo ./install.sh\n</code></pre>"},{"location":"HW_INSTALL/#bare-metal-proxmox","title":"\ud83d\udce5 Bare Metal - Proxmox","text":"<p>Note</p> <p>Use this on a clean LXC/VM for Debian 13 OR Ubuntu 24. The Scipt will detect OS and build acordingly. Maintained by JVKeller</p>"},{"location":"HW_INSTALL/#installation-via-wget_2","title":"Installation via wget","text":"<pre><code>wget https://raw.githubusercontent.com/jokob-sk/NetAlertX/main/install/proxmox/proxmox-install-netalertx.sh -O proxmox-install-netalertx.sh &amp;&amp; chmod +x proxmox-install-netalertx.sh &amp;&amp; ./proxmox-install-netalertx.sh\n</code></pre>"},{"location":"ICONS/","title":"Icons","text":""},{"location":"ICONS/#icons-overview","title":"Icons overview","text":"<p>Icons are used to visually distinguish devices in the app in most of the device listing tables and the network tree. </p> <p></p>"},{"location":"ICONS/#icons-support","title":"Icons Support","text":"<p>Two types of icons are suported:</p> <ul> <li>Free Font Awesome icons (up-to v 6.4.0)</li> <li>SVG icons (for example from iconify.design)</li> </ul> <p>You can assign icons individually on each device in the Details tab.</p>"},{"location":"ICONS/#adding-new-icons","title":"Adding new icons","text":"<ol> <li>You can get an SVG or a Font awesome HTML code</li> </ol> <p>Copying the SVG (for example from iconify.design): </p> <p></p> <p>Copying the HTML code from Font Awesome.</p> <p></p> <ol> <li>Navigate to the device you want to use the icon on and click the \"+\" icon:</li> </ol> <p></p> <ol> <li>Paste in the copied HTML or SVG code and click \"OK\":</li> </ol> <p></p> <ol> <li>\"Save\" the device</li> </ol> <p>Note</p> <p>If you want to mass-apply an icon to all devices of the same device type (Field: Type), you can click the mass-copy button (next to the \"+\" button). A confirmation prompt is displayed. If you proceed, icons of all devices set to the same device type as the current device, will be overwritten with the current device's icon.</p> <ul> <li>The dropdown contains all icons already used in the app for device icons. You might need to navigate away or refresh the page once you add a new icon. </li> </ul>"},{"location":"ICONS/#font-awesome-pro-icons","title":"Font Awesome Pro icons","text":"<p>If you own the premium package of Font Awesome icons you can mount it in your Docker container the following way:</p> <pre><code>/font-awesome:/app/front/lib/font-awesome:ro\n</code></pre> <p>You can use the full range of Font Awesome icons afterwards. </p>"},{"location":"INITIAL_SETUP/","title":"\u26a1 Quick Start Guide","text":"<p>Get NetAlertX up and running in a few simple steps.</p>"},{"location":"INITIAL_SETUP/#1-configure-scanner-plugins","title":"1. Configure Scanner Plugin(s)","text":"<p>Tip</p> <p>Enable additional plugins under Settings \u2192 <code>LOADED_PLUGINS</code>. Make sure to save your changes and reload the page to activate them. </p> <p>Initial configuration: <code>ARPSCAN</code>, <code>INTRNT</code></p> <p>Note</p> <p><code>ARPSCAN</code> and <code>INTRNT</code> scan the current network. You can complement them with other <code>\ud83d\udd0d dev scanner</code> plugins like <code>NMAPDEV</code>, or import devices using <code>\ud83d\udce5 importer</code> plugins. See the Subnet &amp; VLAN Setup Guide and Remote Networks for advanced configurations.</p>"},{"location":"INITIAL_SETUP/#2-choose-a-publisher-plugin","title":"2. Choose a Publisher Plugin","text":"<p>Initial configuration: <code>SMTP</code></p> <p>Note</p> <p>Configure your SMTP settings or enable additional <code>\u25b6\ufe0f publisher</code> plugins to send alerts. For more flexibility, try \ud83d\udcda <code>_publisher_apprise</code>, which supports over 80 notification services.</p>"},{"location":"INITIAL_SETUP/#3-set-up-a-network-topology-diagram","title":"3. Set Up a Network Topology Diagram","text":"<p>Initial configuration: The app auto-selects a root node (MAC <code>internet</code>) and attempts to identify other network devices by vendor or name.</p> <p>Note</p> <p>Visualize and manage your network using the Network Guide. Some plugins (e.g., <code>UNFIMP</code>) build the topology automatically, or you can use Custom Workflows to generate it based on your own rules.</p>"},{"location":"INITIAL_SETUP/#4-configure-notifications","title":"4. Configure Notifications","text":"<p>Initial configuration: Notifies on <code>new_devices</code>, <code>down_devices</code>, and <code>events</code> as defined in <code>NTFPRCS_INCLUDED_SECTIONS</code>.</p> <p>Note</p> <p>Notification settings support global, plugin-specific, and per-device rules. For fine-tuning, refer to the Notification Guide.</p>"},{"location":"INITIAL_SETUP/#5-set-up-workflows","title":"5. Set Up Workflows","text":"<p>Initial configuration: N/A</p> <p>Note</p> <p>Automate responses to device status changes, group management, topology updates, and more. See the Workflows Guide to simplify your network operations.</p>"},{"location":"INITIAL_SETUP/#6-backup-your-configuration","title":"6. Backup Your Configuration","text":"<p>Initial configuration: The <code>CSVBCKP</code> plugin creates a daily backup to <code>/config/devices.csv</code>.</p> <p>Note</p> <p>For a complete backup strategy, follow the Backup Guide.</p>"},{"location":"INITIAL_SETUP/#7-optional-create-custom-plugins","title":"7. (Optional) Create Custom Plugins","text":"<p>Initial configuration: N/A</p> <p>Note</p> <p>Build your own scanner, importer, or publisher plugin. See the Plugin Development Guide and included video tutorials.</p>"},{"location":"INITIAL_SETUP/#recommended-guides","title":"\ud83d\udcc1 Recommended Guides","text":"<ul> <li>\ud83d\udcd8 PiHole Setup Guide</li> <li>\ud83d\udcd8 CSV Import Method</li> <li>\ud83d\udcd8 Community Guides (Chinese, Korean, German, French)</li> </ul>"},{"location":"INITIAL_SETUP/#troubleshooting-help","title":"\ud83d\udee0\ufe0f Troubleshooting &amp; Help","text":"<p>Before opening a new issue:</p> <ul> <li>\ud83d\udcd8 Common Issues</li> <li>\ud83e\uddf0 Debugging Tips</li> <li>\u2705 Browse resolved GitHub issues</li> </ul> <p>Let me know if you want a condensed README version, separate pages for each section, or UI copy based on this!</p>"},{"location":"INSTALLATION/","title":"Installation","text":""},{"location":"INSTALLATION/#installation-options","title":"Installation options","text":"<p>NetAlertX can be installed several ways. The best supported option is Docker, followed by a supervised Home Assistant instance, as an Unraid app, and lastly, on bare metal.</p> <ul> <li>[Installation] Docker (recommended)</li> <li>[Installation] Home Assistant</li> <li>[Installation] Unraid App</li> <li>[Installation] Bare metal (experimental - looking for maintainers)</li> <li>[Installation] Nix flake (community supported) submitted by 2m</li> </ul>"},{"location":"INSTALLATION/#help","title":"Help","text":"<p>If facing issues, please spend a few minutes searching.</p> <ul> <li>Check common issues</li> <li>Have a look at Community guides</li> <li>Search closed or open issues or discussions</li> <li>Check Discord</li> </ul> <p>Note</p> <p>If you can't find a solution anywhere, ask in Discord if you think it's a quick question, otherwise open a new issue. Please fill in as much as possible to speed up the help process.</p>"},{"location":"LOGGING/","title":"Logging","text":"<p>NetAlertX comes with several logs that help to identify application issues. These include nginx logs, app, or plugin logs. For plugin-specific log debugging, please read the Debug Plugins guide.</p> <p>Note</p> <p>When debugging any issue, increase the <code>LOG_LEVEL</code> Setting as per the Debug tips documentation.</p>"},{"location":"LOGGING/#main-logs","title":"Main logs","text":"<p>You can find most of the logs exposed in the UI under Maintenance -&gt; Logs. </p> <p>If the UI is inaccessible, you can access them under <code>/tmp/log</code>.</p> <p></p> <p>In the Maintennace -&gt; Logs you can Purge logs, download the full log file or Filter the lines with some substring to narrow down your search. </p>"},{"location":"LOGGING/#plugin-logging","title":"Plugin logging","text":"<p>If a Plugin supplies data to the main app it's done either vie a SQL query or via a script that updates the <code>last_result.log</code> file in the plugin log folder (<code>app/log/plugins/</code>). These files are processed at the end of the scan and deleted on successful processing.</p> <p>The data is in most of the cases then displayed in the application under Integrations -&gt; Plugins (or Device -&gt; Plugins if the plugin is supplying device-specific data). </p> <p></p>"},{"location":"LOGGING/#viewing-logs-on-the-file-system","title":"Viewing Logs on the File System","text":"<p>You cannot find any log files on the filesystem. The container is <code>read-only</code> and writes logs to a temporary in-memory filesystem (<code>tmpfs</code>) for security and performance. The application follows container best-practices by writing all logs to the standard output (<code>stdout</code>) and standard error (<code>stderr</code>) streams. Docker's logging driver (set in <code>docker-compose.yml</code>) captures this stream automatically, allowing you to access it with the <code>docker logs &lt;image_name&gt;</code> command.</p> <ul> <li>To see all logs since the last restart:</li> </ul> <p><pre><code>docker logs netalertx\n</code></pre> * To watch the logs live (live feed):</p> <pre><code>docker logs -f netalertx\n</code></pre>"},{"location":"LOGGING/#enabling-persistent-file-based-logs","title":"Enabling Persistent File-Based Logs","text":"<p>The default logs are erased every time the container restarts because they are stored in temporary in-memory storage (<code>tmpfs</code>). If you need to keep a persistent, file-based log history, follow the steps below.</p> <p>Note</p> <p>This might lead to performance degradation so this approach is only suggested when actively debugging issues. See the Performance optimization documentation for details.</p> <ol> <li>Stop the container:</li> </ol> <pre><code>docker-compose down\n</code></pre> <ol> <li> <p>Edit your <code>docker-compose.yml</code> file:</p> </li> <li> <p>Comment out the <code>/tmp/log</code> line under the <code>tmpfs:</code> section.</p> </li> <li>Uncomment the \"Retain logs\" line under the <code>volumes:</code> section and set your desired host path.</li> </ol> <p><pre><code>...\n tmpfs:\n # - \"/tmp/log:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime\"\n...\n volumes:\n...\n # Retain logs - comment out tmpfs /tmp/log if you want to retain logs between container restarts\n - /home/adam/netalertx_logs:/tmp/log\n...\n</code></pre> 3. Restart the container:</p> <pre><code>docker-compose up -d\n</code></pre> <p>This change stops Docker from mounting a temporary in-memory volume at <code>/tmp/log</code>. Instead, it \"bind mounts\" a persistent folder from your host computer (e.g., <code>/data/netalertx_logs</code>) to that same location inside the container. </p>"},{"location":"MIGRATION/","title":"Migration","text":"<p>When upgrading from older versions of NetAlertX (or PiAlert by jokob-sk), follow the migration steps below to ensure your data and configuration are properly transferred.</p> <p>Tip</p> <p>It's always important to have a backup strategy in place.</p>"},{"location":"MIGRATION/#migration-scenarios","title":"Migration scenarios","text":"<ul> <li> <p>You are running PiAlert (by jokob-sk) \u2192 Read the 1.1 Migration from PiAlert to NetAlertX <code>v25.5.24</code></p> </li> <li> <p>You are running NetAlertX (by jokob-sk) <code>25.5.24</code> or older \u2192 Read the 1.2 Migration from NetAlertX <code>v25.5.24</code></p> </li> <li> <p>You are running NetAlertX (by jokob-sk) (<code>v25.6.7</code> to <code>v25.10.1</code>) \u2192 Read the 1.3 Migration from NetAlertX <code>v25.10.1</code></p> </li> <li> <p>You are running NetAlertX (by jokob-sk) (<code>v25.11.29</code>) \u2192 Read the 1.4 Migration from NetAlertX <code>v25.11.29</code></p> </li> </ul>"},{"location":"MIGRATION/#10-manual-migration","title":"1.0 Manual Migration","text":"<p>You can migrate data manually, for example by exporting and importing devices using the CSV import method.</p>"},{"location":"MIGRATION/#11-migration-from-pialert-to-netalertx-v25524","title":"1.1 Migration from PiAlert to NetAlertX <code>v25.5.24</code>","text":""},{"location":"MIGRATION/#steps","title":"STEPS:","text":"<p>The application will automatically migrate the database, configuration, and all device information. A banner message will appear at the top of the web UI reminding you to update your Docker mount points.</p> <ol> <li>Stop the container</li> <li>Back up your setup</li> <li>Update the Docker file mount locations in your <code>docker-compose.yml</code> or docker run command (See below New Docker mount locations).</li> <li>Rename the DB and conf files to <code>app.db</code> and <code>app.conf</code> and place them in the appropriate location.</li> <li>Start the container</li> </ol> <p>Tip</p> <p>If you have trouble accessing past backups, config or database files you can copy them into the newly mapped directories, for example by running this command in the container: <code>cp -r /data/config /home/pi/pialert/config/old_backup_files</code>. This should create a folder in the <code>config</code> directory called <code>old_backup_files</code> containing all the files in that location. Another approach is to map the old location and the new one at the same time to copy things over.</p>"},{"location":"MIGRATION/#new-docker-mount-locations","title":"New Docker mount locations","text":"<p>The internal application path in the container has changed from <code>/home/pi/pialert</code> to <code>/app</code>. Update your volume mounts as follows:</p> Old mount point New mount point <code>/home/pi/pialert/config</code> <code>/data/config</code> <code>/home/pi/pialert/db</code> <code>/data/db</code> <p>If you were mounting files directly, please note the file names have changed:</p> Old file name New file name <code>pialert.conf</code> <code>app.conf</code> <code>pialert.db</code> <code>app.db</code> <p>Note</p> <p>The application automatically creates symlinks from the old database and config locations to the new ones, so data loss should not occur. Read the backup strategies guide to backup your setup.</p>"},{"location":"MIGRATION/#examples","title":"Examples","text":"<p>Examples of docker files with the new mount points.</p>"},{"location":"MIGRATION/#example-1-mapping-folders","title":"Example 1: Mapping folders","text":""},{"location":"MIGRATION/#old-docker-composeyml","title":"Old docker-compose.yml","text":"<pre><code>services:\n pialert:\n container_name: pialert\n # use the below line if you want to test the latest dev image\n # image: \"ghcr.io/jokob-sk/netalertx-dev:latest\"\n image: \"jokobsk/pialert:latest\"\n network_mode: \"host\"\n restart: unless-stopped\n volumes:\n - /local_data_dir/config:/home/pi/pialert/config\n - /local_data_dir/db:/home/pi/pialert/db\n # (optional) useful for debugging if you have issues setting up the container\n - /local_data_dir/logs:/home/pi/pialert/front/log\n environment:\n - TZ=Europe/Berlin\n - PORT=20211\n</code></pre>"},{"location":"MIGRATION/#new-docker-composeyml","title":"New docker-compose.yml","text":"<pre><code>services:\n netalertx: # \ud83c\udd95 This has changed\n container_name: netalertx # \ud83c\udd95 This has changed\n image: \"ghcr.io/jokob-sk/netalertx:25.5.24\" # \ud83c\udd95 This has changed\n network_mode: \"host\"\n restart: unless-stopped\n volumes:\n - /local_data_dir/config:/data/config # \ud83c\udd95 This has changed\n - /local_data_dir/db:/data/db # \ud83c\udd95 This has changed\n # (optional) useful for debugging if you have issues setting up the container\n - /local_data_dir/logs:/tmp/log # \ud83c\udd95 This has changed\n environment:\n - TZ=Europe/Berlin\n - PORT=20211\n</code></pre>"},{"location":"MIGRATION/#example-2-mapping-files","title":"Example 2: Mapping files","text":"<p>Note</p> <p>The recommendation is to map folders as in Example 1, map files directly only when needed.</p>"},{"location":"MIGRATION/#old-docker-composeyml_1","title":"Old docker-compose.yml","text":"<pre><code>services:\n pialert:\n container_name: pialert\n # use the below line if you want to test the latest dev image\n # image: \"ghcr.io/jokob-sk/netalertx-dev:latest\"\n image: \"jokobsk/pialert:latest\"\n network_mode: \"host\"\n restart: unless-stopped\n volumes:\n - /local_data_dir/config/pialert.conf:/home/pi/pialert/config/pialert.conf\n - /local_data_dir/db/pialert.db:/home/pi/pialert/db/pialert.db\n # (optional) useful for debugging if you have issues setting up the container\n - /local_data_dir/logs:/home/pi/pialert/front/log\n environment:\n - TZ=Europe/Berlin\n - PORT=20211\n</code></pre>"},{"location":"MIGRATION/#new-docker-composeyml_1","title":"New docker-compose.yml","text":"<pre><code>services:\n netalertx: # \ud83c\udd95 This has changed\n container_name: netalertx # \ud83c\udd95 This has changed\n image: \"ghcr.io/jokob-sk/netalertx:25.5.24\" # \ud83c\udd95 This has changed\n network_mode: \"host\"\n restart: unless-stopped\n volumes:\n - /local_data_dir/config/app.conf:/data/config/app.conf # \ud83c\udd95 This has changed\n - /local_data_dir/db/app.db:/data/db/app.db # \ud83c\udd95 This has changed\n # (optional) useful for debugging if you have issues setting up the container\n - /local_data_dir/logs:/tmp/log # \ud83c\udd95 This has changed\n environment:\n - TZ=Europe/Berlin\n - PORT=20211\n</code></pre>"},{"location":"MIGRATION/#12-migration-from-netalertx-v25524","title":"1.2 Migration from NetAlertX <code>v25.5.24</code>","text":"<p>Versions before <code>v25.10.1</code> require an intermediate migration through <code>v25.5.24</code> to ensure database compatibility. Skipping this step may cause compatibility issues due to database schema changes introduced after <code>v25.5.24</code>.</p>"},{"location":"MIGRATION/#steps_1","title":"STEPS:","text":"<ol> <li>Stop the container</li> <li>Back up your setup</li> <li>Upgrade to <code>v25.5.24</code> by pinning the release version (See Examples below)</li> <li>Start the container and verify everything works as expected.</li> <li>Stop the container</li> <li>Upgrade to <code>v25.10.1</code> by pinning the release version (See Examples below)</li> <li>Start the container and verify everything works as expected.</li> </ol>"},{"location":"MIGRATION/#examples_1","title":"Examples","text":"<p>Examples of docker files with the tagged version.</p>"},{"location":"MIGRATION/#example-1-mapping-folders_1","title":"Example 1: Mapping folders","text":""},{"location":"MIGRATION/#docker-composeyml-changes","title":"docker-compose.yml changes","text":"<pre><code>services:\n netalertx:\n container_name: netalertx\n image: \"ghcr.io/jokob-sk/netalertx:25.5.24\" # \ud83c\udd95 This is important\n network_mode: \"host\"\n restart: unless-stopped\n volumes:\n - /local_data_dir/config:/data/config\n - /local_data_dir/db:/data/db\n # (optional) useful for debugging if you have issues setting up the container\n - /local_data_dir/logs:/tmp/log\n environment:\n - TZ=Europe/Berlin\n - PORT=20211\n</code></pre> <pre><code>services:\n netalertx:\n container_name: netalertx\n image: \"ghcr.io/jokob-sk/netalertx:25.10.1\" # \ud83c\udd95 This is important\n network_mode: \"host\"\n restart: unless-stopped\n volumes:\n - /local_data_dir/config:/data/config\n - /local_data_dir/db:/data/db\n # (optional) useful for debugging if you have issues setting up the container\n - /local_data_dir/logs:/tmp/log\n environment:\n - TZ=Europe/Berlin\n - PORT=20211\n</code></pre>"},{"location":"MIGRATION/#13-migration-from-netalertx-v25101","title":"1.3 Migration from NetAlertX <code>v25.10.1</code>","text":"<p>Starting from <code>v25.10.1</code>, the container uses a more secure, read-only runtime environment, which requires all writable paths (e.g., logs, API cache, temporary data) to be mounted as <code>tmpfs</code> or permanent writable volumes, with sufficient access permissions. The data location has also hanged from <code>/app/db</code> and <code>/app/config</code> to <code>/data/db</code> and <code>/data/config</code>. See detailed steps below.</p>"},{"location":"MIGRATION/#steps_2","title":"STEPS:","text":"<ol> <li>Stop the container</li> <li>Back up your setup</li> <li>Upgrade to <code>v25.10.1</code> by pinning the release version (See the example below)</li> </ol> <pre><code>services:\n netalertx:\n container_name: netalertx\n image: \"ghcr.io/jokob-sk/netalertx:25.10.1\" # \ud83c\udd95 This is important\n network_mode: \"host\"\n restart: unless-stopped\n volumes:\n - /local_data_dir/config:/app/config\n - /local_data_dir/db:/app/db\n # (optional) useful for debugging if you have issues setting up the container\n - /local_data_dir/logs:/tmp/log\n environment:\n - TZ=Europe/Berlin\n - PORT=20211\n</code></pre> <ol> <li>Start the container and verify everything works as expected.</li> <li>Stop the container.</li> <li>Update the <code>docker-compose.yml</code> as per example below.</li> </ol> <p><pre><code>services:\n netalertx:\n container_name: netalertx\n image: \"ghcr.io/jokob-sk/netalertx:25.11.29\" # \ud83c\udd95 This has changed\n network_mode: \"host\"\n cap_drop: # \ud83c\udd95 New line\n - ALL # \ud83c\udd95 New line\n cap_add: # \ud83c\udd95 New line\n - NET_RAW # \ud83c\udd95 New line\n - NET_ADMIN # \ud83c\udd95 New line\n - NET_BIND_SERVICE # \ud83c\udd95 New line\n restart: unless-stopped\n volumes:\n - /local_data_dir:/data # \ud83c\udd95 This folder contains your /db and /config directories and the parent changed from /app to /data\n # Ensuring the timezone is the same as on the server - make sure also the TIMEZONE setting is configured\n - /etc/localtime:/etc/localtime:ro # \ud83c\udd95 New line\n environment:\n - PORT=20211\n # \ud83c\udd95 New \"tmpfs\" section START \ud83d\udd3d\n tmpfs:\n # All writable runtime state resides under /tmp; comment out to persist logs between restarts\n - \"/tmp:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime\"\n # \ud83c\udd95 New \"tmpfs\" section END \ud83d\udd3c\n</code></pre> 7. Perform a one-off migration to the latest <code>netalertx</code> image and <code>20211</code> user.</p> <p>Note</p> <p>The examples below assumes your <code>/config</code> and <code>/db</code> folders are stored in <code>local_data_dir</code>. Replace this path with your actual configuration directory. <code>netalertx</code> is the container name, which might differ from your setup.</p> <p>Automated approach:</p> <p>Run the container with the <code>--user \"0\"</code> parameter. Please note, some systems will require the manual approach below.</p> <pre><code>docker run -it --rm --name netalertx --user \"0\" \\\n -v /local_data_dir/config:/app/config \\\n -v /local_data_dir/db:/app/db \\\n -v /local_data_dir:/data \\\n --tmpfs /tmp:uid=20211,gid=20211,mode=1700 \\\n ghcr.io/jokob-sk/netalertx:latest\n</code></pre> <p>Stop the container and run it as you would normally.</p> <p>Manual approach:</p> <p>Use the manual approach if the Automated approach fails. Execute the below commands:</p> <pre><code>sudo chown -R 20211:20211 /local_data_dir\nsudo chmod -R a+rwx /local_data_dir\n</code></pre> <ol> <li>Start the container and verify everything works as expected.</li> <li>Check the Permissions -&gt; Writable-paths what directories to mount if you'd like to access the API or log files.</li> </ol>"},{"location":"MIGRATION/#14-migration-from-netalertx-v251129","title":"1.4 Migration from NetAlertX <code>v25.11.29</code>","text":"<p>As per user feedback, we\u2019ve re-introduced the ability to control which user the application runs as via the <code>PUID</code> and <code>PGID</code> environment variables. This required additional changes to the container to safely handle permission adjustments at runtime.</p>"},{"location":"MIGRATION/#steps_3","title":"STEPS:","text":"<ol> <li>Stop the container</li> <li>Back up your setup</li> <li>Stop the container</li> <li>Update the <code>docker-compose.yml</code> as per example below.</li> </ol> <pre><code>services:\n netalertx:\n container_name: netalertx\n image: \"ghcr.io/netalertx/netalertx\"\n network_mode: \"host\"\n cap_drop:\n - ALL\n cap_add:\n - NET_RAW\n - NET_ADMIN\n - NET_BIND_SERVICE\n - CHOWN # \ud83c\udd95 New line\n - SETUID # \ud83c\udd95 New line\n - SETGID # \ud83c\udd95 New line\n restart: unless-stopped\n volumes:\n - /local_data_dir:/data\n # Ensuring the timezone is the same as on the server - make sure also the TIMEZONE setting is configured\n - /etc/localtime:/etc/localtime:ro\n environment:\n - PORT=20211\n # - PUID=0 # New optional variable to run as root\n # - GUID=100 # New optional variable to run as root\n tmpfs:\n # All writable runtime state resides under /tmp; comment out to persist logs between restarts\n - \"/tmp:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime\"\n</code></pre> <ol> <li>If you use a custom <code>PUID</code> (e.g. <code>0</code>) and <code>GUID</code> (e.g. <code>100</code>) make sure you also update the <code>tmpfs</code> ownership, e.g. <code>/tmp:uid=0,gid=100...</code></li> <li>Start the container and verify everything works as expected.</li> <li>If running a reverse proxy review the Reverse proxy documentation as a new <code>BACKEND_API_URL</code> setting was added.</li> </ol>"},{"location":"NAME_RESOLUTION/","title":"Device Name Resolution","text":"<p>Name resolution in NetAlertX relies on multiple plugins to resolve device names from IP addresses. If you are seeing <code>(name not found)</code> as device names, follow these steps to diagnose and fix the issue.</p> <p>Tip</p> <p>Before proceeding, make sure Reverse DNS is enabled on your network. You can control how names are handled and cleaned using the <code>NEWDEV_NAME_CLEANUP_REGEX</code> setting. To auto-update Fully Qualified Domain Names (FQDN), enable the <code>REFRESH_FQDN</code> setting.</p>"},{"location":"NAME_RESOLUTION/#required-plugins","title":"Required Plugins","text":"<p>For best results, ensure the following name resolution plugins are enabled:</p> <ul> <li>AVAHISCAN \u2013 Uses mDNS/Avahi to resolve local network names.</li> <li>NBTSCAN \u2013 Queries NetBIOS to find device names.</li> <li>NSLOOKUP \u2013 Performs standard DNS lookups.</li> <li>DIGSCAN \u2013 Performs Name Resolution with the Dig utility (DNS).</li> </ul> <p>You can check which plugins are active in your Settings section and enable any that are missing.</p> <p>There are other plugins that can supply device names as well, but they rely on bespoke hardware and services. See Plugins overview for details and look for plugins with name discovery (\ud83c\udd8e) features.</p>"},{"location":"NAME_RESOLUTION/#checking-logs","title":"Checking Logs","text":"<p>If names are not resolving, check the logs for errors or timeouts.</p> <p>See how to explore logs in the Logging guide.</p> <p>Logs will show which plugins attempted resolution and any failures encountered.</p>"},{"location":"NAME_RESOLUTION/#adjusting-timeout-settings","title":"Adjusting Timeout Settings","text":"<p>If resolution is slow or failing due to timeouts, increase the timeout settings in your configuration, for example.</p> <pre><code>NSLOOKUP_RUN_TIMEOUT = 30\n</code></pre> <p>Raising the timeout may help if your network has high latency or slow DNS responses.</p>"},{"location":"NAME_RESOLUTION/#checking-plugin-objects","title":"Checking Plugin Objects","text":"<p>Each plugin stores results in its respective object. You can inspect these objects to see if they contain valid name resolution data.</p> <p>See Logging guide and Debug plugins guides for details.</p> <p>If the object contains no results, the issue may be with DNS settings or network access.</p>"},{"location":"NAME_RESOLUTION/#improving-name-resolution","title":"Improving name resolution","text":"<p>For more details how to improve name resolution refer to the Reverse DNS Documentation.</p>"},{"location":"NETWORK_TREE/","title":"How to Set Up Your Network Page","text":"<p>The Network page lets you map how devices connect \u2014 visually and logically. It\u2019s especially useful for planning infrastructure, assigning parent-child relationships, and spotting gaps.</p> <p></p> <p>To get started, you\u2019ll need to define at least one root node and mark certain devices as network nodes (like Switches or Routers).</p> <p>Start by creating a root device with the MAC address <code>Internet</code>, if the application didn\u2019t create one already. This special MAC address (<code>Internet</code>) is required for the root network node \u2014 no other value is currently supported. Set its Type to a valid network type \u2014 such as <code>Router</code> or <code>Gateway</code>.</p> <p>Tip</p> <p>If you don\u2019t have one, use the Create new device button on the Devices page to add a root device.</p>"},{"location":"NETWORK_TREE/#quick-setup","title":"\u26a1 Quick Setup","text":"<ol> <li>Open the device you want to use as a network node (e.g. a Switch).</li> <li>Set its Type to one of the following: <code>AP</code>, <code>Firewall</code>, <code>Gateway</code>, <code>PLC</code>, <code>Powerline</code>, <code>Router</code>, <code>Switch</code>, <code>USB LAN Adapter</code>, <code>USB WIFI Adapter</code>, <code>WLAN</code> (Or add custom types under Settings \u2192 General \u2192 <code>NETWORK_DEVICE_TYPES</code>.)</li> <li>Save the device.</li> <li>Go to the Network page \u2014 supported device types will appear as tabs.</li> <li>Use the Assign button to connect unassigned devices to a network node.</li> <li>If the Port is <code>0</code> or empty, a Wi-Fi icon is shown. Otherwise, an Ethernet icon appears.</li> </ol> <p>Note</p> <p>Use bulk editing with CSV Export to fix <code>Internet</code> root assignments or update many devices at once.</p>"},{"location":"NETWORK_TREE/#example-setting-up-a-raspberrypi-as-a-switch","title":"Example: Setting up a <code>raspberrypi</code> as a Switch","text":"<p>Let\u2019s walk through setting up a device named <code>raspberrypi</code> to act as a network Switch that other devices connect through.</p>"},{"location":"NETWORK_TREE/#1-set-device-type-and-parent","title":"1. Set Device Type and Parent","text":"<ul> <li>Go to the Devices page</li> <li>Open the device detail view for <code>raspberrypi</code></li> <li>In the Type dropdown, select <code>Switch</code></li> </ul> <ul> <li>Optionally assign a Parent Node (where this device connects to) and the Relationship type of the connection. The <code>nic</code> relationship type can affect parent notifications \u2014 see the setting description and Notifications documentation for more.</li> <li>A device\u2019s parent MAC will be overwritten by plugins if its current value is any of the following: \"null\", \"(unknown)\" \"(Unknown)\".</li> <li>If you want plugins to be able to overwrite the parent value (for example, when mixing plugins that do not provide parent MACs like <code>ARPSCAN</code> with those that do, like <code>UNIFIAPI</code>), you must set the setting <code>NEWDEV_devParentMAC</code> to None.</li> </ul> <p>Note</p> <p>Only certain device types can act as network nodes: <code>AP</code>, <code>Firewall</code>, <code>Gateway</code>, <code>Hypervisor</code>, <code>PLC</code>, <code>Powerline</code>, <code>Router</code>, <code>Switch</code>, <code>USB LAN Adapter</code>, <code>USB WIFI Adapter</code>, <code>WLAN</code> You can add custom types via the <code>NETWORK_DEVICE_TYPES</code> setting.</p> <ul> <li>Click Save</li> </ul>"},{"location":"NETWORK_TREE/#2-confirm-the-device-appears-as-a-network-node","title":"2. Confirm The Device Appears as a Network Node","text":"<p>You can confirm that <code>raspberrypi</code> now acts as a network device in two places:</p> <ul> <li>Navigate to a different device and verify that <code>raspberrypi</code> now appears as an option for a Parent Node:</li> </ul> <p></p> <ul> <li>Go to the Network page \u2014 you'll now see a <code>raspberrypi</code> tab, meaning it's recognized as a network node (Switch):</li> </ul> <p></p> <ul> <li>You can now assign other devices to it.</li> </ul>"},{"location":"NETWORK_TREE/#3-assign-connected-devices","title":"3. Assign Connected Devices","text":"<ul> <li>Use the Assign button to link other devices (e.g. PCs) to <code>raspberrypi</code>.</li> <li>After assigning, connected devices will appear beneath the <code>raspberrypi</code> switch node.</li> </ul> <ul> <li>Relationship lines may vary in color based on the selected Relationship type. These are editable on the device details page where you can also assign a parent node.</li> </ul> <p>Hovering over devices in the tree reveals connection details and tooltips for quick inspection.</p> <p>Note</p> <p>Selecting certain relationship types hides the device in the default device views. You can change this behavior by adjusting the <code>UI_hide_rel_types</code> setting, which by default is set to <code>[\"nic\",\"virtual\"]</code>. This means devices with <code>devParentRelType</code> set to <code>nic</code> or <code>virtual</code> will not be shown. All devices, regardless of relationship type, are always accessible in the All devices view.</p>"},{"location":"NETWORK_TREE/#troubleshooting","title":"Troubleshooting","text":"<p>If the Network page doesn't load re-set your parent nodes. This can be done with bulk-edit.</p> <ol> <li>Backup your setup just in case</li> <li>Navigate to Maintenance -&gt; Multi edit ( (1), (2) )</li> <li>Add all devices (3) (clear the cache with the refresh button if you seem to be missing devices in the dropdown (4))</li> <li>Select None as parent node (5) and save (6)</li> </ol> <p></p> <ol> <li>Find now your root Internet Node by searching for \"Internet\" in the My Devices view</li> <li>If not found, make sure the <code>INTRNT</code> plugin runs and creates the internet device</li> <li>If above fails, create a manual device with the MAC set to <code>Internet</code></li> </ol> <p></p> <ol> <li>You should be able to start again to configure your Network view.</li> </ol>"},{"location":"NETWORK_TREE/#summary","title":"\u2705 Summary","text":"<p>To configure devices on the Network page:</p> <ul> <li>Ensure a device with MAC <code>Internet</code> is set up as the root</li> <li>Assign valid Type values to switches, routers, and other supported nodes that represent network devices</li> <li>Use the Assign button to connect devices logically to their parent node</li> </ul> <p>Need to reset or undo changes? Use backups or bulk editing to manage devices at scale. You can also automate device assignment with Workflows.</p>"},{"location":"NOTIFICATIONS/","title":"Notifications \ud83d\udce7","text":"<p>There are 4 ways how to influence notifications:</p> <ol> <li>On the device itself</li> <li>On the settings of the plugin</li> <li>Globally</li> <li>Ignoring devices</li> </ol> <p>Note</p> <p>It's recommended to use the same schedule interval for all plugins responsible for scanning devices, otherwise false positives might be reported if different devices are discovered by different plugins. Check the Settings &gt; Enabled settings section for a warning: </p>"},{"location":"NOTIFICATIONS/#device-settings","title":"Device settings \ud83d\udcbb","text":"<p>The following device properties influence notifications. You can:</p> <ol> <li>Alert Events - Enables alerts of connections, disconnections, IP changes (down and down reconnected notifications are still sent even if this is disabled).</li> <li>Alert Down - Alerts when a device goes down. This setting overrides a disabled Alert Events setting, so you will get a notification of a device going down even if you don't have Alert Events ticked. Disabling this will disable down and down reconnected notifications on the device.</li> <li>Can Sleep - Marks the device as sleep-capable (e.g. a battery-powered sensor that deep-sleeps between readings). When enabled, offline periods within the Alert down after (sleep) (<code>NTFPRCS_sleep_time</code>) global window are shown as Sleeping (aqua badge \ud83c\udf19) instead of Down, and no down alert is fired during that window. Once the window expires the device falls back to normal down-alert logic. \u26a0 Requires Alert Down to be enabled \u2014 sleeping suppresses the alert during the window only.</li> <li>Skip repeated notifications, if for example you know there is a temporary issue and want to pause the same notification for this device for a given time.</li> <li>Require NICs Online - Indicates whether this device should be considered online only if all associated NICs (devices with the <code>nic</code> relationship type) are online. If disabled, the device is considered online if any NIC is online. If a NIC is online it sets the parent (this) device's status to online irrespectivelly of the detected device's status. The Relationship type is set on the childern device.</li> </ol> <p>Note</p> <p>Please read through the NTFPRCS plugin documentation to understand how device and global settings influence the notification processing.</p>"},{"location":"NOTIFICATIONS/#plugin-settings","title":"Plugin settings \ud83d\udd0c","text":"<p>On almost all plugins there are 2 core settings, <code>&lt;plugin&gt;_WATCH</code> and <code>&lt;plugin&gt;_REPORT_ON</code>.</p> <ol> <li><code>&lt;plugin&gt;_WATCH</code> specifies the columns which the app should watch. If watched columns change the device state is considered changed. This changed status is then used to decide to send out notifications based on the <code>&lt;plugin&gt;_REPORT_ON</code> setting.</li> <li><code>&lt;plugin&gt;_REPORT_ON</code> let's you specify on which events the app should notify you. This is related to the <code>&lt;plugin&gt;_WATCH</code> setting. So if you select <code>watched-changed</code> and in <code>&lt;plugin&gt;_WATCH</code> you only select <code>Watched_Value1</code>, then a notification is triggered if <code>Watched_Value1</code> is changed from the previous value, but no notification is send if <code>Watched_Value2</code> changes.</li> </ol> <p>Click the Read more in the docs. Link at the top of each plugin to get more details on how the given plugin works.</p>"},{"location":"NOTIFICATIONS/#global-settings","title":"Global settings \u2699","text":"<p>In Notification Processing settings, you can specify blanket rules. These allow you to specify exceptions to the Plugin and Device settings and will override those.</p> <ol> <li>Notify on (<code>NTFPRCS_INCLUDED_SECTIONS</code>) allows you to specify which events trigger notifications. Usual setups will have <code>new_devices</code>, <code>down_devices</code>, and possibly <code>down_reconnected</code> set. Including <code>plugin</code> (dependenton the Plugin <code>&lt;plugin&gt;_WATCH</code> and <code>&lt;plugin&gt;_REPORT_ON</code> settings) and <code>events</code> (dependent on the on-device Alert Events setting) might be too noisy for most setups. More info in the NTFPRCS plugin on what events these selections include.</li> <li>Alert down after (<code>NTFPRCS_alert_down_time</code>) is useful if you want to wait for some time before the system sends out a down notification for a device. This is related to the on-device Alert down setting and only devices with this checked will trigger a down notification.</li> <li>Alert down after (sleep) (<code>NTFPRCS_sleep_time</code>) sets the sleep window in minutes. If a device has Can Sleep enabled and goes offline, it is shown as Sleeping (aqua \ud83c\udf19 badge) for this many minutes before down-alert logic kicks in. Default is <code>30</code> minutes. Changing this setting takes effect after saving \u2014 no restart required.</li> </ol> <p>You can filter out unwanted notifications globally. This could be because of a misbehaving device (GoogleNest/GoogleHub (See also ARPSAN docs and the <code>--exclude-broadcast</code> flag)) which flips between IP addresses, or because you want to ignore new device notifications of a certain pattern.</p> <ol> <li>Events Filter (<code>NTFPRCS_event_condition</code>) - Filter out Events from notifications.</li> <li>New Devices Filter (<code>NTFPRCS_new_dev_condition</code>) - Filter out New Devices from notifications, but log and keep a new device in the system.</li> </ol>"},{"location":"NOTIFICATIONS/#ignoring-devices","title":"Ignoring devices \ud83d\udcbb","text":"<p>You can completely ignore detected devices globally. This could be because your instance detects docker containers, you want to ignore devices from a specific manufacturer via MAC rules or you want to ignore devices on a specific IP range.</p> <ol> <li>Ignored MACs (<code>NEWDEV_ignored_MACs</code>) - List of MACs to ignore.</li> <li>Ignored IPs (<code>NEWDEV_ignored_IPs</code>) - List of IPs to ignore.</li> </ol>"},{"location":"PERFORMANCE/","title":"Performance Optimization Guide","text":"<p>There are several ways to improve the application's performance. The application has been tested on a range of devices, from Raspberry Pi 4 units to NAS and NUC systems. If you are running the application on a lower-end device, fine-tuning the performance settings can significantly improve the user experience.</p>"},{"location":"PERFORMANCE/#common-causes-of-slowness","title":"Common Causes of Slowness","text":"<p>Performance issues are usually caused by:</p> <ul> <li>Incorrect settings \u2013 The app may restart unexpectedly. Check <code>app.log</code> under Maintenance \u2192 Logs for details.</li> <li>Too many background processes \u2013 Disable unnecessary scanners.</li> <li>Long scan durations \u2013 Limit the number of scanned devices.</li> <li>Excessive disk operations \u2013 Optimize scanning and logging settings.</li> <li>Maintenance plugin failures \u2013 If cleanup tasks fail, performance can degrade over time.</li> </ul> <p>The application performs regular maintenance and database cleanup. If these tasks are failing, you will see slowdowns.</p>"},{"location":"PERFORMANCE/#database-and-log-file-size","title":"Database and Log File Size","text":"<p>A large database or oversized log files can impact performance. You can check database and table sizes on the Maintenance page.</p> <p></p> <p>Note</p> <ul> <li>For ~100 devices, the database should be around 50 MB.</li> <li>No table should exceed 10,000 rows in a healthy system.</li> <li>Actual values vary based on network activity and plugin settings.</li> </ul>"},{"location":"PERFORMANCE/#maintenance-plugins","title":"Maintenance Plugins","text":"<p>Two plugins help maintain the system\u2019s performance:</p>"},{"location":"PERFORMANCE/#1-database-cleanup-dbclnp","title":"1. Database Cleanup (DBCLNP)","text":"<ul> <li>Handles database maintenance and cleanup.</li> <li>See the DB Cleanup Plugin Docs.</li> <li>Ensure it\u2019s not failing by checking logs.</li> <li>Adjust the schedule (<code>DBCLNP_RUN_SCHD</code>) and timeout (<code>DBCLNP_RUN_TIMEOUT</code>) if necessary.</li> </ul>"},{"location":"PERFORMANCE/#2-maintenance-maint","title":"2. Maintenance (MAINT)","text":"<ul> <li>Cleans logs and performs general maintenance tasks.</li> <li>See the Maintenance Plugin Docs.</li> <li>Verify proper operation via logs.</li> <li>Adjust the schedule (<code>MAINT_RUN_SCHD</code>) and timeout (<code>MAINT_RUN_TIMEOUT</code>) if needed.</li> </ul>"},{"location":"PERFORMANCE/#database-performance-tuning","title":"Database Performance Tuning","text":"<p>The application automatically maintains database performance as data accumulates. However, you can adjust settings to balance CPU usage, disk usage, and responsiveness.</p>"},{"location":"PERFORMANCE/#wal-size-tuning-storage-vs-cpu-tradeoff","title":"WAL Size Tuning (Storage vs. CPU Tradeoff)","text":"<p>The SQLite Write-Ahead Log (WAL) is a temporary file that grows during normal operation. On systems with constrained resources (NAS, Raspberry Pi), controlling WAL size is important.</p> <p>Setting: <code>PRAGMA_JOURNAL_SIZE_LIMIT</code> (default: 50 MB)</p> Setting Effect Use Case 10\u201320 MB Smaller storage footprint; more frequent disk operations NAS with SD card (storage priority) 50 MB (default) Balanced; recommended for most setups General use 75\u2013100 MB Smoother performance; larger WAL on disk High-speed NAS or servers <p>Recommendation: For NAS devices with SD cards, leave at default (50 MB) or increase slightly (75 MB). Avoid very low values (&lt; 10 MB) as they cause frequent disk thrashing and CPU spikes.</p>"},{"location":"PERFORMANCE/#automatic-cleanup","title":"Automatic Cleanup","text":"<p>The DB cleanup plugin (<code>DBCLNP</code>) automatically optimizes query performance and trims old data:</p> <ul> <li>Deletes old events \u2013 Controlled by <code>DAYS_TO_KEEP_EVENTS</code> (default: 90 days)</li> <li>Trims plugin history \u2013 Keeps recent entries only (controlled by <code>PLUGINS_KEEP_HIST</code>)</li> <li>Optimizes queries \u2013 Updates database statistics so queries remain fast</li> </ul> <p>If cleanup fails, performance degrades quickly. Check Maintenance \u2192 Logs for errors. If you see frequent failures, increase the timeout (<code>DBCLNP_RUN_TIMEOUT</code>).</p>"},{"location":"PERFORMANCE/#scan-frequency-and-coverage","title":"Scan Frequency and Coverage","text":"<p>Frequent scans increase resource usage, network traffic, and database read/write cycles.</p>"},{"location":"PERFORMANCE/#optimizations","title":"Optimizations","text":"<ul> <li>Increase scan intervals (<code>&lt;PLUGIN&gt;_RUN_SCHD</code>) on busy networks or low-end hardware.</li> <li>Increase timeouts (<code>&lt;PLUGIN&gt;_RUN_TIMEOUT</code>) to avoid plugin failures.</li> <li>Reduce subnet size \u2013 e.g., use <code>/24</code> instead of <code>/16</code> to reduce scan load.</li> </ul> <p>Some plugins also include options to limit which devices are scanned. If certain plugins consistently run long, consider narrowing their scope.</p> <p>For example, the ICMP plugin allows scanning only IPs that match a specific regular expression.</p>"},{"location":"PERFORMANCE/#storing-temporary-files-in-memory","title":"Storing Temporary Files in Memory","text":"<p>On devices with slower I/O, you can improve performance by storing temporary files (and optionally the database) in memory using <code>tmpfs</code>.</p> <p>Warning</p> <p>Storing the database in <code>tmpfs</code> is generally discouraged. Use this only if device data and historical records are not required to persist. If needed, you can pair this setup with the <code>SYNC</code> plugin to store important persistent data on another node. See the Plugins docs for details.</p> <p>Using <code>tmpfs</code> reduces disk writes and speeds up I/O, but all data stored in memory will be lost on restart.</p> <p>Below is an optimized <code>docker-compose.yml</code> snippet using non-persistent logs, API data, and DB:</p> <pre><code>services:\n netalertx:\n container_name: netalertx\n # Use this line for the stable release\n image: \"ghcr.io/netalertx/netalertx:latest\"\n # Or use this line for the latest development build\n # image: \"ghcr.io/netalertx/netalertx-dev:latest\"\n network_mode: \"host\"\n restart: unless-stopped\n\n cap_drop: # Drop all capabilities for enhanced security\n - ALL\n cap_add: # Re-add necessary capabilities\n - NET_RAW\n - NET_ADMIN\n - NET_BIND_SERVICE\n - CHOWN\n - SETUID\n - SETGID\n\n volumes:\n - ${APP_FOLDER}/netalertx/config:/data/config\n - /etc/localtime:/etc/localtime:ro\n\n tmpfs:\n # All writable runtime state resides under /tmp; comment out to persist logs between restarts\n - \"/tmp:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime\"\n - \"/data/db:uid=20211,gid=20211,mode=1700\" # \u26a0 You will lose historical data on restart\n\n environment:\n - PORT=${PORT}\n - APP_CONF_OVERRIDE=${APP_CONF_OVERRIDE}\n</code></pre>"},{"location":"PIHOLE_GUIDE/","title":"Integration with PiHole","text":"<p>NetAlertX comes with 3 plugins suitable for integrating with your existing PiHole instance. The first plugin uses the v6 API, the second plugin is using a direct SQLite DB connection, the other leverages the <code>DHCP.leases</code> file generated by PiHole. You can combine multiple approaches and also supplement scans with other plugins.</p>"},{"location":"PIHOLE_GUIDE/#approach-1-piholeapi-plugin-import-devices-directly-from-pihole-v6-api","title":"Approach 1: <code>PIHOLEAPI</code> Plugin - Import devices directly from PiHole v6 API","text":"<p>To use this approach make sure the Web UI password in Pi-hole is set.</p> Setting Description Recommended value <code>PIHOLEAPI_URL</code> Your Pi-hole base URL including port. <code>http://192.168.1.82:9880/</code> <code>PIHOLEAPI_RUN_SCHD</code> If you run multiple device scanner plugins, align the schedules of all plugins to the same value. <code>*/5 * * * *</code> <code>PIHOLEAPI_PASSWORD</code> The Web UI base64 encoded (en-/decoding handled by the app) admin password. <code>passw0rd</code> <code>PIHOLEAPI_SSL_VERIFY</code> Whether to verify HTTPS certificates. Disable only for self-signed certificates. <code>False</code> <code>PIHOLEAPI_API_MAXCLIENTS</code> Maximum number of devices to request from Pi-hole. Defaults are usually fine. <code>500</code> <code>PIHOLEAPI_FAKE_MAC</code> Generate FAKE MAC from IP. <code>False</code> <p>Check the PiHole API plugin readme for details and troubleshooting.</p>"},{"location":"PIHOLE_GUIDE/#docker-compose-changes","title":"docker-compose changes","text":"<p>No changes needed</p>"},{"location":"PIHOLE_GUIDE/#approach-2-dhcplss-plugin-import-devices-from-the-pihole-dhcp-leases-file","title":"Approach 2: <code>DHCPLSS</code> Plugin - Import devices from the PiHole DHCP leases file","text":""},{"location":"PIHOLE_GUIDE/#settings","title":"Settings","text":"Setting Description Recommended value <code>DHCPLSS_RUN</code> When the plugin should run. <code>schedule</code> <code>DHCPLSS_RUN_SCHD</code> If you run multiple device scanner plugins, align the schedules of all plugins to the same value. <code>*/5 * * * *</code> <code>DHCPLSS_paths_to_check</code> You need to map the value in this setting in the <code>docker-compose.yml</code> file. The in-container path must contain <code>pihole</code> so it's parsed correctly. <code>['/etc/pihole/dhcp.leases']</code> <p>Check the DHCPLSS plugin readme for details</p>"},{"location":"PIHOLE_GUIDE/#docker-compose-changes_1","title":"docker-compose changes","text":"Path Description <code>:/etc/pihole/dhcp.leases</code> PiHole's <code>dhcp.leases</code> file. Required if you want to use PiHole <code>dhcp.leases</code> file. This has to be matched with a corresponding <code>DHCPLSS_paths_to_check</code> setting entry (the path in the container must contain <code>pihole</code>)"},{"location":"PIHOLE_GUIDE/#approach-3-pihole-plugin-import-devices-directly-from-the-pihole-database","title":"Approach 3: <code>PIHOLE</code> Plugin - Import devices directly from the PiHole database","text":"Setting Description Recommended value <code>PIHOLE_RUN</code> When the plugin should run. <code>schedule</code> <code>PIHOLE_RUN_SCHD</code> If you run multiple device scanner plugins, align the schedules of all plugins to the same value. <code>*/5 * * * *</code> <code>PIHOLE_DB_PATH</code> You need to map the value in this setting in the <code>docker-compose.yml</code> file. <code>/etc/pihole/pihole-FTL.db</code> <p>Check the PiHole plugin readme for details</p>"},{"location":"PIHOLE_GUIDE/#docker-compose-changes_2","title":"docker-compose changes","text":"Path Description <code>:/etc/pihole/pihole-FTL.db</code> PiHole's <code>pihole-FTL.db</code> database file. <p>Check out other plugins that can help you discover more about your network or check how to scan Remote networks.</p>"},{"location":"PLUGINS/","title":"\ud83d\udd0c Plugins","text":"<p>NetAlertX supports additional plugins to extend its functionality, each with its own settings and options. Plugins can be loaded via the General -&gt; <code>LOADED_PLUGINS</code> setting. For custom plugin development, refer to the Plugin development guide.</p> <p>Note</p> <p>Please check this Plugins debugging guide and the corresponding Plugin documentation in the below table if you are facing issues.</p>"},{"location":"PLUGINS/#quick-start","title":"\u26a1 Quick start","text":"<p>Tip</p> <p>You can load additional Plugins via the General -&gt; <code>LOADED_PLUGINS</code> setting. You need to save the settings for the new plugins to load (cache/page reload may be necessary). </p> <ol> <li>Pick your <code>\ud83d\udd0d dev scanner</code> plugin (e.g. <code>ARPSCAN</code> or <code>NMAPDEV</code>), or import devices into the application with an <code>\ud83d\udce5 importer</code> plugin. (See Enabling plugins below)</li> <li>Pick a <code>\u25b6\ufe0f publisher</code> plugin, if you want to send notifications. If you don't see a publisher you'd like to use, look at the \ud83d\udcda_publisher_apprise plugin which is a proxy for over 80 notification services.</li> <li>Setup your Network topology diagram</li> <li>Fine-tune Notifications</li> <li>Setup Workflows</li> <li>Backup your setup</li> <li>Contribute and Create custom plugins</li> </ol>"},{"location":"PLUGINS/#plugin-types","title":"Plugin types","text":"Plugin type Icon Description When to run Required Data source ? publisher \u25b6\ufe0f Sending notifications to services. <code>on_notification</code> \u2716 Script dev scanner \ud83d\udd0d Create devices in the app, manages online/offline device status. <code>schedule</code> \u2716 Script / SQLite DB name discovery \ud83c\udd8e Discovers names of devices via various protocols. <code>before_name_updates</code>, <code>schedule</code> \u2716 Script importer \ud83d\udce5 Importing devices from another service. <code>schedule</code> \u2716 Script / SQLite DB system \u2699 Providing core system functionality. <code>schedule</code> / always on \u2716/\u2714 Script / Template other \u267b Other plugins misc \u2716 Script / Template"},{"location":"PLUGINS/#features","title":"Features","text":"Icon Description \ud83d\udda7 Auto-imports the network topology diagram \ud83d\udd04 Has the option to sync some data back into the plugin source"},{"location":"PLUGINS/#available-plugins","title":"Available Plugins","text":"<p>Device-detecting plugins insert values into the <code>CurrentScan</code> database table. The plugins that are not required are safe to ignore, however, it makes sense to have at least some device-detecting plugins enabled, such as <code>ARPSCAN</code> or <code>NMAPDEV</code>.</p> ID Plugin docs Type Description Features Required <code>APPRISE</code> _publisher_apprise \u25b6\ufe0f Apprise notification proxy <code>ARPSCAN</code> arp_scan \ud83d\udd0d ARP-scan on current network <code>AVAHISCAN</code> avahi_scan \ud83c\udd8e Avahi (mDNS-based) name resolution <code>ASUSWRT</code> asuswrt_import \ud83d\udd0d Import connected devices from AsusWRT <code>CSVBCKP</code> csv_backup \u2699 CSV devices backup <code>CUSTPROP</code> custom_props \u2699 Managing custom device properties values Yes <code>DBCLNP</code> db_cleanup \u2699 Database cleanup Yes* <code>DDNS</code> ddns_update \u2699 DDNS update <code>DHCPLSS</code> dhcp_leases \ud83d\udd0d/\ud83d\udce5/\ud83c\udd8e Import devices from DHCP leases <code>DHCPSRVS</code> dhcp_servers \u267b DHCP servers <code>DIGSCAN</code> dig_scan \ud83c\udd8e Dig (DNS) Name resolution <code>FREEBOX</code> freebox \ud83d\udd0d/\u267b/\ud83c\udd8e Pull data and names from Freebox/Iliadbox <code>ICMP</code> icmp_scan \u267b ICMP (ping) status checker <code>INTRNT</code> internet_ip \ud83d\udd0d Internet IP scanner <code>INTRSPD</code> internet_speedtest \u267b Internet speed test <code>IPNEIGH</code> ipneigh \ud83d\udd0d Scan ARP (IPv4) and NDP (IPv6) tables <code>LUCIRPC</code> luci_import \ud83d\udd0d Import connected devices from OpenWRT <code>MAINT</code> maintenance \u2699 Maintenance of logs, etc. <code>MQTT</code> _publisher_mqtt \u25b6\ufe0f MQTT for synching to Home Assistant <code>MTSCAN</code> mikrotik_scan \ud83d\udd0d Mikrotik device import &amp; sync <code>NBTSCAN</code> nbtscan_scan \ud83c\udd8e Nbtscan (NetBIOS-based) name resolution <code>NEWDEV</code> newdev_template \u2699 New device template Yes <code>NMAP</code> nmap_scan \u267b Nmap port scanning &amp; discovery <code>NMAPDEV</code> nmap_dev_scan \ud83d\udd0d Nmap dev scan on current network <code>NSLOOKUP</code> nslookup_scan \ud83c\udd8e NSLookup (DNS-based) name resolution <code>NTFPRCS</code> notification_processing \u2699 Notification processing Yes <code>NTFY</code> _publisher_ntfy \u25b6\ufe0f NTFY notifications <code>OMDSDN</code> omada_sdn_imp \ud83d\udce5/\ud83c\udd8e \u274c UNMAINTAINED use <code>OMDSDNOPENAPI</code> \ud83d\udda7 \ud83d\udd04 <code>OMDSDNOPENAPI</code> omada_sdn_openapi \ud83d\udce5/\ud83c\udd8e OMADA TP-Link import via OpenAPI \ud83d\udda7 <code>PIHOLE</code> pihole_scan \ud83d\udd0d/\ud83c\udd8e/\ud83d\udce5 Pi-hole device import &amp; sync <code>PIHOLEAPI</code> pihole_api_scan \ud83d\udd0d/\ud83c\udd8e/\ud83d\udce5 Pi-hole device import &amp; sync via API v6+ <code>PUSHSAFER</code> _publisher_pushsafer \u25b6\ufe0f Pushsafer notifications <code>PUSHOVER</code> _publisher_pushover \u25b6\ufe0f Pushover notifications <code>SETPWD</code> set_password \u2699 Set password Yes <code>SMTP</code> _publisher_email \u25b6\ufe0f Email notifications <code>SNMPDSC</code> snmp_discovery \ud83d\udd0d/\ud83d\udce5 SNMP device import &amp; sync <code>SYNC</code> sync \ud83d\udd0d/\u2699/\ud83d\udce5 Sync &amp; import from NetAlertX instances \ud83d\udda7 \ud83d\udd04 Yes <code>TELEGRAM</code> _publisher_telegram \u25b6\ufe0f Telegram notifications <code>UI</code> ui_settings \u267b UI specific settings Yes <code>UNFIMP</code> unifi_import \ud83d\udd0d/\ud83d\udce5/\ud83c\udd8e UniFi device import &amp; sync \ud83d\udda7 <code>UNIFIAPI</code> unifi_api_import \ud83d\udd0d/\ud83d\udce5/\ud83c\udd8e UniFi device import (SM API, multi-site) <code>VNDRPDT</code> vendor_update \u2699 Vendor database update <code>WEBHOOK</code> _publisher_webhook \u25b6\ufe0f Webhook notifications <code>WEBMON</code> website_monitor \u267b Website down monitoring <code>WOL</code> wake_on_lan \u267b Automatic wake-on-lan <p>* The database cleanup plugin (<code>DBCLNP</code>) is not required but the app will become unusable after a while if not executed. \u274c marked for removal/unmaintained - looking for help \u231aIt's recommended to use the same schedule interval for all plugins responsible for discovering new devices.</p>"},{"location":"PLUGINS/#enabling-plugins","title":"Enabling plugins","text":"<p>Plugins can be enabled via Settings, and can be disabled as needed.</p> <ol> <li>Research which plugin you'd like to use, enable <code>DISCOVER_PLUGINS</code> and load the required plugins in Settings via the <code>LOADED_PLUGINS</code> setting.</li> <li>Save the changes and review the Settings of the newly loaded plugins.</li> <li>Change the <code>&lt;prefix&gt;_RUN</code> Setting to the recommended or custom value as per the documentation of the given setting<ul> <li>If using <code>schedule</code> on a <code>\ud83d\udd0d dev scanner</code> plugin, make sure the schedules are the same across all <code>\ud83d\udd0d dev scanner</code> plugins</li> </ul> </li> </ol>"},{"location":"PLUGINS/#disabling-unloading-and-ignoring-plugins","title":"Disabling, Unloading and Ignoring plugins","text":"<ol> <li>Change the <code>&lt;prefix&gt;_RUN</code> Setting to <code>disabled</code> if you want to disable the plugin, but keep the settings</li> <li>If you want to speed up the application, you can unload the plugin by unselecting it in the <code>LOADED_PLUGINS</code> setting.<ul> <li>Careful, once you save the Settings Unloaded plugin settings will be lost (old <code>app.conf</code> files are kept in the <code>/config</code> folder)</li> </ul> </li> <li>You can completely ignore plugins by placing a <code>ignore_plugin</code> file into the plugin directory. Ignored plugins won't show up in the <code>LOADED_PLUGINS</code> setting.</li> </ol>"},{"location":"PLUGINS/#developing-new-custom-plugins","title":"\ud83c\udd95 Developing new custom plugins","text":"<p>If you want to develop a custom plugin, please read this Plugin development guide.</p>"},{"location":"PLUGINS_DEV/","title":"Plugin Development Guide","text":"<p>This comprehensive guide covers how to build plugins for NetAlertX.</p> <p>Tip</p> <p>New to plugin development? Start with the Quick Start Guide to get a working plugin in 5 minutes.</p> <p>NetAlertX comes with a plugin system to feed events from third-party scripts into the UI and then send notifications, if desired. The highlighted core functionality this plugin system supports:</p> <ul> <li>Dynamic UI generation - Automatically create tables for discovered objects</li> <li>Data filtering - Filter and link values in the Devices UI</li> <li>User settings - Surface plugin configuration in the Settings UI</li> <li>Rich display types - Color-coded badges, links, formatted text, and more</li> <li>Database integration - Import plugin data into NetAlertX tables like <code>CurrentScan</code> or <code>Devices</code></li> </ul> <p>Note</p> <p>For a high-level overview of how the <code>config.json</code> is used and its lifecycle, see the config.json Lifecycle Guide.</p>"},{"location":"PLUGINS_DEV/#quick-links","title":"Quick Links","text":""},{"location":"PLUGINS_DEV/#getting-started","title":"\ud83d\ude80 Getting Started","text":"<ul> <li>Quick Start Guide - Create a working plugin in 5 minutes</li> <li>Development Environment Setup - Set up your local development environment</li> </ul>"},{"location":"PLUGINS_DEV/#core-concepts","title":"\ud83d\udcda Core Concepts","text":"<ul> <li>Data Contract - The exact output format plugins must follow (9-13 columns, pipe-delimited)</li> <li>Data Sources - How plugins retrieve data (scripts, databases, templates)</li> <li>Plugin Settings System - Let users configure your plugin via the UI</li> <li>UI Components - Display plugin results with color coding, links, and more</li> </ul>"},{"location":"PLUGINS_DEV/#architecture","title":"\ud83c\udfd7\ufe0f Architecture","text":"<ul> <li>Plugin Config Lifecycle - How <code>config.json</code> is loaded and used</li> <li>Full Plugin Development Reference - Comprehensive details on all aspects</li> </ul>"},{"location":"PLUGINS_DEV/#troubleshooting","title":"\ud83d\udc1b Troubleshooting","text":"<ul> <li>Debugging Plugins - Troubleshoot plugin issues</li> <li>Plugin Examples - Study existing plugins as reference implementations</li> </ul>"},{"location":"PLUGINS_DEV/#video-tutorial","title":"\ud83c\udfa5 Video Tutorial","text":""},{"location":"PLUGINS_DEV/#screenshots","title":"\ud83d\udcf8 Screenshots","text":""},{"location":"PLUGINS_DEV/#use-cases","title":"Use Cases","text":"<p>Plugins are infinitely flexible. Here are some examples:</p> <ul> <li>Device Discovery - Scan networks using ARP, mDNS, DHCP leases, or custom protocols</li> <li>Service Monitoring - Monitor web services, APIs, or network services for availability</li> <li>Integration - Import devices from PiHole, Home Assistant, Unifi, or other systems</li> <li>Enrichment - Add data like geolocation, threat intelligence, or asset metadata</li> <li>Alerting - Send notifications to Slack, Discord, Telegram, email, or webhooks</li> <li>Reporting - Generate insights from existing NetAlertX database (open ports, recent changes, etc.)</li> <li>Custom Logic - Create fake devices, trigger automations, or implement custom heuristics</li> </ul> <p>If you can imagine it and script it, you can build a plugin.</p>"},{"location":"PLUGINS_DEV/#limitations-notes","title":"Limitations &amp; Notes","text":"<ul> <li>Plugin data is deduplicated hourly (same Primary ID + Secondary ID + User Data = duplicate removed)</li> <li>Currently, only <code>CurrentScan</code> table supports update/overwrite of existing objects</li> <li>Plugin results must follow the strict Data Contract</li> <li>Plugins run with the same permissions as the NetAlertX process</li> <li>External dependencies must be installed in the container</li> </ul>"},{"location":"PLUGINS_DEV/#plugin-development-workflow","title":"Plugin Development Workflow","text":""},{"location":"PLUGINS_DEV/#step-1-understand-the-basics","title":"Step 1: Understand the Basics","text":"<ol> <li>Read Quick Start Guide - 5 minute overview</li> <li>Study the Data Contract - Understand the output format</li> <li>Choose a Data Source - Where does your data come from?</li> </ol>"},{"location":"PLUGINS_DEV/#step-2-create-your-plugin","title":"Step 2: Create Your Plugin","text":"<ol> <li>Copy the <code>__template</code> plugin folder (see below for structure)</li> <li>Update <code>config.json</code> with your plugin metadata</li> <li>Implement <code>script.py</code> (or configure alternative data source)</li> <li>Test locally in the devcontainer</li> </ol>"},{"location":"PLUGINS_DEV/#step-3-configure-display","title":"Step 3: Configure &amp; Display","text":"<ol> <li>Define Settings for user configuration</li> <li>Design UI Components for result display</li> <li>Map to database tables if needed (for notifications, etc.)</li> </ol>"},{"location":"PLUGINS_DEV/#step-4-deploy-test","title":"Step 4: Deploy &amp; Test","text":"<ol> <li>Restart the backend</li> <li>Test via Settings \u2192 Plugin Settings</li> <li>Verify results in UI and logs</li> <li>Check <code>/tmp/log/plugins/last_result.&lt;PREFIX&gt;.log</code></li> </ol> <p>See Quick Start Guide for detailed step-by-step instructions.</p>"},{"location":"PLUGINS_DEV/#plugin-file-structure","title":"Plugin File Structure","text":"<p>Every plugin lives in its own folder under <code>/app/front/plugins/</code>.</p> <p>Important: Folder name must match the <code>\"code_name\"</code> value in <code>config.json</code></p> <pre><code>/app/front/plugins/\n\u251c\u2500\u2500 __template/ # Copy this as a starting point\n\u2502 \u251c\u2500\u2500 config.json # Plugin manifest (configuration)\n\u2502 \u251c\u2500\u2500 script.py # Your plugin logic (optional, depends on data_source)\n\u2502 \u2514\u2500\u2500 README.md # Setup and usage documentation\n\u251c\u2500\u2500 my_plugin/ # Your new plugin\n\u2502 \u251c\u2500\u2500 config.json # REQUIRED - Plugin manifest\n\u2502 \u251c\u2500\u2500 script.py # OPTIONAL - Python script (if using script data source)\n\u2502 \u251c\u2500\u2500 README.md # REQUIRED - Documentation for users\n\u2502 \u2514\u2500\u2500 other_files... # Your supporting files\n</code></pre>"},{"location":"PLUGINS_DEV/#plugin-manifest-configjson","title":"Plugin Manifest (config.json)","text":"<p>The <code>config.json</code> file is the plugin manifest - it tells NetAlertX everything about your plugin:</p> <ul> <li>Metadata: Plugin name, description, icon</li> <li>Execution: When to run, what command to run, timeout</li> <li>Settings: User-configurable options</li> <li>Data contract: Column definitions and how to display results</li> <li>Integration: Database mappings, notifications, filters</li> </ul> <p>Example minimal config.json:</p> <pre><code>{\n \"code_name\": \"my_plugin\",\n \"unique_prefix\": \"MYPLN\",\n \"display_name\": [{\"language_code\": \"en_us\", \"string\": \"My Plugin\"}],\n \"description\": [{\"language_code\": \"en_us\", \"string\": \"My awesome plugin\"}],\n \"icon\": \"fa-plug\",\n \"data_source\": \"script\",\n \"execution_order\": \"Layer_0\",\n \"settings\": [\n {\n \"function\": \"RUN\",\n \"type\": {\"dataType\": \"string\", \"elements\": [{\"elementType\": \"select\", \"elementOptions\": [], \"transformers\": []}]},\n \"default_value\": \"disabled\",\n \"options\": [\"disabled\", \"once\", \"schedule\"],\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"When to run\"}]\n },\n {\n \"function\": \"CMD\",\n \"type\": {\"dataType\": \"string\", \"elements\": [{\"elementType\": \"input\", \"elementOptions\": [], \"transformers\": []}]},\n \"default_value\": \"python3 /app/front/plugins/my_plugin/script.py\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Command\"}]\n }\n ],\n \"database_column_definitions\": []\n}\n</code></pre> <p>For comprehensive <code>config.json</code> documentation, see PLUGINS_DEV_CONFIG.md</p>"},{"location":"PLUGINS_DEV/#full-reference-below","title":"Full Reference (Below)","text":"<p>The sections below provide complete reference documentation for all plugin development topics. Use the quick links above to jump to specific sections, or read sequentially for a deep dive.</p> <p>More on specifics below.</p>"},{"location":"PLUGINS_DEV/#data-contract-output-format","title":"Data Contract &amp; Output Format","text":"<p>For detailed information on plugin output format, see PLUGINS_DEV_DATA_CONTRACT.md.</p> <p>Quick reference: - Format: Pipe-delimited (<code>|</code>) text file - Location: <code>/tmp/log/plugins/last_result.&lt;PREFIX&gt;.log</code> - Columns: 9 required + 4 optional = 13 maximum - Helper: Use <code>plugin_helper.py</code> for easy formatting</p>"},{"location":"PLUGINS_DEV/#the-9-mandatory-columns","title":"The 9 Mandatory Columns","text":"Column Name Required Example 0 Object_PrimaryID YES <code>\"device_name\"</code> or <code>\"192.168.1.1\"</code> 1 Object_SecondaryID no <code>\"secondary_id\"</code> or <code>null</code> 2 DateTime YES <code>\"2023-01-02 15:56:30\"</code> 3 Watched_Value1 YES <code>\"online\"</code> or <code>\"200\"</code> 4 Watched_Value2 no <code>\"ip_address\"</code> or <code>null</code> 5 Watched_Value3 no <code>null</code> 6 Watched_Value4 no <code>null</code> 7 Extra no <code>\"additional data\"</code> or <code>null</code> 8 ForeignKey no <code>\"aa:bb:cc:dd:ee:ff\"</code> or <code>null</code> <p>See Data Contract for examples, validation, and debugging tips.</p>"},{"location":"PLUGINS_DEV/#configjson-settings-configuration","title":"Config.json: Settings &amp; Configuration","text":"<p>For detailed settings documentation, see PLUGINS_DEV_SETTINGS.md and PLUGINS_DEV_DATASOURCES.md.</p>"},{"location":"PLUGINS_DEV/#setting-object-structure","title":"Setting Object Structure","text":"<p>Every setting in your plugin has this structure:</p> <pre><code>{\n \"function\": \"UNIQUE_CODE\",\n \"type\": {\"dataType\": \"string\", \"elements\": [...]},\n \"default_value\": \"...\",\n \"options\": [...],\n \"localized\": [\"name\", \"description\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Display Name\"}],\n \"description\": [{\"language_code\": \"en_us\", \"string\": \"Help text\"}]\n}\n</code></pre>"},{"location":"PLUGINS_DEV/#reserved-function-names","title":"Reserved Function Names","text":"<p>These control core plugin behavior:</p> Function Purpose Required Options <code>RUN</code> When to execute YES <code>disabled</code>, <code>once</code>, <code>schedule</code>, <code>always_after_scan</code>, <code>before_name_updates</code>, <code>on_new_device</code> <code>RUN_SCHD</code> Cron schedule If <code>RUN=schedule</code> Cron format: <code>\"0 * * * *\"</code> <code>CMD</code> Command to run YES Shell command or script path <code>RUN_TIMEOUT</code> Max execution time optional Seconds: <code>\"60\"</code> <code>WATCH</code> Monitor for changes optional Column names <code>REPORT_ON</code> When to notify optional <code>new</code>, <code>watched-changed</code>, <code>watched-not-changed</code>, <code>missing-in-last-scan</code> <code>DB_PATH</code> External DB path If using SQLite <code>/path/to/db.db</code> <p>See PLUGINS_DEV_SETTINGS.md for full component types and examples.</p>"},{"location":"PLUGINS_DEV/#filters-data-display","title":"Filters &amp; Data Display","text":"<p>For comprehensive display configuration, see PLUGINS_DEV_UI_COMPONENTS.md.</p>"},{"location":"PLUGINS_DEV/#filters","title":"Filters","text":"<p>Control which rows display in the UI:</p> <pre><code>{\n \"data_filters\": [\n {\n \"compare_column\": \"Object_PrimaryID\",\n \"compare_operator\": \"==\",\n \"compare_field_id\": \"txtMacFilter\",\n \"compare_js_template\": \"'{value}'.toString()\",\n \"compare_use_quotes\": true\n }\n ]\n}\n</code></pre> <p>See UI Components: Filters for full documentation.</p>"},{"location":"PLUGINS_DEV/#database-mapping","title":"Database Mapping","text":"<p>To import plugin data into NetAlertX tables for device discovery or notifications:</p> <pre><code>{\n \"mapped_to_table\": \"CurrentScan\",\n \"database_column_definitions\": [\n {\n \"column\": \"Object_PrimaryID\",\n \"mapped_to_column\": \"scanMac\",\n \"show\": true,\n \"type\": \"device_mac\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"MAC Address\"}]\n }\n ]\n}\n</code></pre> <p>See UI Components: Database Mapping for full documentation.</p>"},{"location":"PLUGINS_DEV/#static-value-mapping","title":"Static Value Mapping","text":"<p>To always map a static value (not read from plugin output):</p> <pre><code>{\n \"column\": \"NameDoesntMatter\",\n \"mapped_to_column\": \"scanSourcePlugin\",\n \"mapped_to_column_data\": {\n \"value\": \"MYPLN\"\n }\n}\n</code></pre>"},{"location":"PLUGINS_DEV/#ui-component-types","title":"UI Component Types","text":"<p>Plugin results are displayed in the web interface using various component types. See PLUGINS_DEV_UI_COMPONENTS.md for complete documentation.</p>"},{"location":"PLUGINS_DEV/#common-display-types","title":"Common Display Types","text":"<p>Read settings in your Python script:</p> <pre><code>from helper import get_setting_value\n\n# Read a setting by code name (prefix + function)\napi_url = get_setting_value('MYPLN_API_URL')\napi_key = get_setting_value('MYPLN_API_KEY')\nwatch_columns = get_setting_value('MYPLN_WATCH')\n\nprint(f\"Connecting to {api_url}\")\n</code></pre> <p>Pass settings as command parameters:</p> <p>Define <code>params</code> in config to pass settings as script arguments:</p> <pre><code>{\n \"params\": [\n {\n \"name\": \"api_url\",\n \"type\": \"setting\",\n \"value\": \"MYPLN_API_URL\"\n }\n ]\n}\n</code></pre> <p>Then use in <code>CMD</code>: <code>python3 script.py --url={api_url}</code></p> <p>See PLUGINS_DEV_SETTINGS.md for complete settings documentation, and PLUGINS_DEV_DATASOURCES.md for data source details.</p>"},{"location":"PLUGINS_DEV/#quick-reference-key-concepts","title":"Quick Reference: Key Concepts","text":""},{"location":"PLUGINS_DEV/#plugin-output-format","title":"Plugin Output Format","text":"<p><pre><code>Object_PrimaryID|Object_SecondaryID|DateTime|Watched_Value1|Watched_Value2|Watched_Value3|Watched_Value4|Extra|ForeignKey\n</code></pre> 9 required columns, 4 optional helpers = 13 max</p> <p>See: Data Contract</p>"},{"location":"PLUGINS_DEV/#plugin-metadata-configjson","title":"Plugin Metadata (config.json)","text":"<pre><code>{\n \"code_name\": \"my_plugin\", // Folder name\n \"unique_prefix\": \"MYPLN\", // Settings prefix\n \"display_name\": [...], // UI label\n \"data_source\": \"script\", // Where data comes from\n \"settings\": [...], // User configurable\n \"database_column_definitions\": [...] // How to display\n}\n</code></pre> <p>See: Full Guide, Settings</p>"},{"location":"PLUGINS_DEV/#reserved-settings","title":"Reserved Settings","text":"<ul> <li><code>RUN</code> - When to execute (disabled, once, schedule, always_after_scan, etc.)</li> <li><code>RUN_SCHD</code> - Cron schedule</li> <li><code>CMD</code> - Command/script to execute</li> <li><code>RUN_TIMEOUT</code> - Max execution time</li> <li><code>WATCH</code> - Monitor for changes</li> <li><code>REPORT_ON</code> - Notification trigger</li> </ul> <p>See: Settings System</p>"},{"location":"PLUGINS_DEV/#display-types","title":"Display Types","text":"<p><code>label</code>, <code>device_mac</code>, <code>device_ip</code>, <code>url</code>, <code>threshold</code>, <code>replace</code>, <code>regex</code>, <code>textbox_save</code>, and more.</p> <p>See: UI Components</p>"},{"location":"PLUGINS_DEV/#tools-references","title":"Tools &amp; References","text":"<ul> <li>Template Plugin: <code>/app/front/plugins/__template/</code> - Start here!</li> <li>Helper Library: <code>/app/front/plugins/plugin_helper.py</code> - Use for output formatting</li> <li>Settings Helper: <code>/app/server/helper.py</code> - Use <code>get_setting_value()</code> in scripts</li> <li>Example Plugins: <code>/app/front/plugins/*/</code> - Study working implementations</li> <li>Logs: <code>/tmp/log/plugins/</code> - Plugin output and execution logs</li> <li>Backend Logs: <code>/tmp/log/stdout.log</code> - Core system logs</li> </ul>"},{"location":"PLUGINS_DEV_CONFIG/","title":"Plugins Implementation Details","text":"<p>Plugins provide data to the NetAlertX core, which processes it to detect changes, discover new devices, raise alerts, and apply heuristics.</p>"},{"location":"PLUGINS_DEV_CONFIG/#overview-plugin-data-flow","title":"Overview: Plugin Data Flow","text":"<ol> <li>Each plugin runs on a defined schedule.</li> <li>Aligning all plugin schedules is recommended so they execute in the same loop.</li> <li>During execution, all plugins write their collected data into the <code>CurrentScan</code> table.</li> <li>After all plugins complete, the <code>CurrentScan</code> table is evaluated to detect new devices, changes, and triggers.</li> </ol> <p>Although plugins run independently, they contribute to the shared <code>CurrentScan</code> table. To inspect its contents, set <code>LOG_LEVEL=trace</code> and check for the log section:</p> <pre><code>================ CurrentScan table content ================\n</code></pre>"},{"location":"PLUGINS_DEV_CONFIG/#configjson-lifecycle","title":"<code>config.json</code> Lifecycle","text":"<p>This section outlines how each plugin\u2019s <code>config.json</code> manifest is read, validated, and used by the core and plugins. It also describes plugin output expectations and the main plugin categories.</p> <p>Tip</p> <p>For detailed schema and examples, see the Plugin Development Guide.</p>"},{"location":"PLUGINS_DEV_CONFIG/#1-loading","title":"1. Loading","text":"<ul> <li>On startup, the core loads <code>config.json</code> for each plugin.</li> <li>The file acts as a plugin manifest, defining metadata, runtime configuration, and database mappings.</li> </ul>"},{"location":"PLUGINS_DEV_CONFIG/#2-validation","title":"2. Validation","text":"<ul> <li>The core validates required keys (for example, <code>RUN</code>).</li> <li>Missing or invalid entries may be replaced with defaults or cause the plugin to be disabled.</li> </ul>"},{"location":"PLUGINS_DEV_CONFIG/#3-preparation","title":"3. Preparation","text":"<ul> <li>Plugin parameters (paths, commands, and options) are prepared for execution.</li> <li>Database mappings (<code>mapped_to_table</code>, <code>database_column_definitions</code>) are parsed to define how data integrates with the main app.</li> </ul>"},{"location":"PLUGINS_DEV_CONFIG/#4-execution","title":"4. Execution","text":"<ul> <li> <p>Plugins may run:</p> </li> <li> <p>On a fixed schedule.</p> </li> <li>Once at startup.</li> <li>After a notification or other trigger.</li> <li>The scheduler executes plugins according to their <code>interval</code>.</li> </ul>"},{"location":"PLUGINS_DEV_CONFIG/#5-parsing","title":"5. Parsing","text":"<ul> <li>Plugin output must be pipe-delimited (<code>|</code>).</li> <li>The core parses each output line following the Plugin Interface Contract, splitting and mapping fields accordingly.</li> </ul>"},{"location":"PLUGINS_DEV_CONFIG/#6-mapping","title":"6. Mapping","text":"<ul> <li>Parsed fields are inserted into the plugin\u2019s <code>Plugins_*</code> table.</li> <li> <p>Data can be mapped into other tables (e.g., <code>Devices</code>, <code>CurrentScan</code>) as defined by:</p> </li> <li> <p><code>database_column_definitions</code></p> </li> <li><code>mapped_to_table</code></li> </ul> <p>Example: <code>Object_PrimaryID \u2192 devMAC</code></p>"},{"location":"PLUGINS_DEV_CONFIG/#6a-plugin-output-contract","title":"6a. Plugin Output Contract","text":"<p>All plugins must follow the Plugin Interface Contract defined in <code>PLUGINS_DEV.md</code>. Output values are pipe-delimited in a fixed order.</p>"},{"location":"PLUGINS_DEV_CONFIG/#identifiers","title":"Identifiers","text":"<ul> <li><code>Object_PrimaryID</code> and <code>Object_SecondaryID</code> uniquely identify records (for example, <code>MAC|IP</code>).</li> </ul>"},{"location":"PLUGINS_DEV_CONFIG/#watched-values-watched_value14","title":"Watched Values (<code>Watched_Value1\u20134</code>)","text":"<ul> <li>Used by the core to detect changes between runs.</li> <li>Changes in these fields can trigger notifications.</li> </ul>"},{"location":"PLUGINS_DEV_CONFIG/#extra-field-extra","title":"Extra Field (<code>Extra</code>)","text":"<ul> <li>Optional additional value.</li> <li>Stored in the database but not used for alerts.</li> </ul>"},{"location":"PLUGINS_DEV_CONFIG/#helper-values-helper_value13","title":"Helper Values (<code>Helper_Value1\u20133</code>)","text":"<ul> <li>Optional auxiliary data (for display or plugin logic).</li> <li>Stored but not alert-triggering.</li> </ul>"},{"location":"PLUGINS_DEV_CONFIG/#mapping","title":"Mapping","text":"<ul> <li>While the output format is flexible, the plugin\u2019s manifest determines the destination and type of each field.</li> </ul>"},{"location":"PLUGINS_DEV_CONFIG/#7-persistence","title":"7. Persistence","text":"<ul> <li>Parsed data is upserted into the database.</li> <li>Conflicts are resolved using the combined key: <code>Object_PrimaryID + Object_SecondaryID</code>.</li> </ul>"},{"location":"PLUGINS_DEV_CONFIG/#plugin-categories","title":"Plugin Categories","text":"<p>Plugins fall into several functional categories depending on their purpose and expected outputs.</p>"},{"location":"PLUGINS_DEV_CONFIG/#1-device-discovery-plugins","title":"1. Device Discovery Plugins","text":"<ul> <li>Inputs: None, subnet, or discovery API.</li> <li>Outputs: <code>MAC</code> and <code>IP</code> for new or updated device records in <code>Devices</code>.</li> <li>Mapping: Required \u2013 usually into <code>CurrentScan</code>.</li> <li>Examples: <code>ARPSCAN</code>, <code>NMAPDEV</code>.</li> </ul>"},{"location":"PLUGINS_DEV_CONFIG/#2-device-data-enrichment-plugins","title":"2. Device Data Enrichment Plugins","text":"<ul> <li>Inputs: Device identifiers (<code>MAC</code>, <code>IP</code>).</li> <li>Outputs: Additional metadata (for example, open ports or sensors).</li> <li>Mapping: Controlled via manifest definitions.</li> <li>Examples: <code>NMAP</code>, <code>MQTT</code>.</li> </ul>"},{"location":"PLUGINS_DEV_CONFIG/#3-name-resolver-plugins","title":"3. Name Resolver Plugins","text":"<ul> <li>Inputs: Device identifiers (<code>MAC</code>, <code>IP</code>, hostname`).</li> <li>Outputs: Updated <code>devName</code> and <code>devFQDN</code>.</li> <li>Mapping: Typically none.</li> <li>Note: Adding new resolvers currently requires a core change.</li> <li>Examples: <code>NBTSCAN</code>, <code>NSLOOKUP</code>.</li> </ul>"},{"location":"PLUGINS_DEV_CONFIG/#4-generic-plugins","title":"4. Generic Plugins","text":"<ul> <li>Inputs: Custom, based on the plugin logic or script.</li> <li>Outputs: Data displayed under Integrations \u2192 Plugins only.</li> <li>Mapping: Not required.</li> <li>Examples: <code>INTRSPD</code>, custom monitoring scripts.</li> </ul>"},{"location":"PLUGINS_DEV_CONFIG/#5-configuration-only-plugins","title":"5. Configuration-Only Plugins","text":"<ul> <li>Inputs/Outputs: None at runtime.</li> <li>Purpose: Used for configuration or maintenance tasks.</li> <li>Examples: <code>MAINT</code>, <code>CSVBCKP</code>.</li> </ul>"},{"location":"PLUGINS_DEV_CONFIG/#post-processing","title":"Post-Processing","text":"<p>After persistence:</p> <ul> <li>The core generates notifications for any watched value changes.</li> <li>The UI updates with new or modified data.</li> <li>Plugins with UI-enabled data display under Integrations \u2192 Plugins.</li> </ul>"},{"location":"PLUGINS_DEV_CONFIG/#field-update-authorization-set_always-set_empty","title":"Field Update Authorization (SET_ALWAYS / SET_EMPTY)","text":"<p>For tracked fields (devMac, devName, devLastIP, devVendor, devFQDN, devSSID, devParentMAC, devParentPort, devParentRelType, devVlan), plugins can configure how they interact with the authoritative field update system.</p>"},{"location":"PLUGINS_DEV_CONFIG/#set_always","title":"SET_ALWAYS","text":"<p>Mandatory when field is tracked.</p> <p>Controls whether a plugin field is enabled:</p> <ul> <li><code>[\"devName\", \"devLastIP\"]</code> - Plugin can always overwrite this field when authorized (subject to source-based permissions)</li> </ul> <p>Authorization logic: Even with a field listed in <code>SET_ALWAYS</code>, the plugin respects source-based permissions:</p> <ul> <li>Cannot overwrite <code>USER</code> source (user manually edited)</li> <li>Cannot overwrite <code>LOCKED</code> source (user locked field)</li> <li>Can overwrite <code>NEWDEV</code> or plugin-owned sources (if plugin has SET_ALWAYS enabled)</li> <li>Will update plugin-owned sources if value the same</li> </ul> <p>Example in config.json:</p> <pre><code>{\n \"SET_ALWAYS\": [\"devName\", \"devLastIP\"]\n}\n</code></pre>"},{"location":"PLUGINS_DEV_CONFIG/#set_empty","title":"SET_EMPTY","text":"<p>Optional field override.</p> <p>Restricts when a plugin can update a field:</p> <ul> <li><code>\"SET_EMPTY\": [\"devName\", \"devLastIP\"]</code> - Overwrite these fields only if current value is empty OR source is <code>NEWDEV</code></li> </ul> <p>Use case: Some plugins discover optional enrichment data (like vendor/hostname) that shouldn't override user-set or existing values. Use <code>SET_EMPTY</code> to be less aggressive.</p>"},{"location":"PLUGINS_DEV_CONFIG/#authorization-decision-flow","title":"Authorization Decision Flow","text":"<ol> <li>Source check: Is field LOCKED or USER? \u2192 REJECT (protected)</li> <li>Field in SET_ALWAYS check: Is SET_ALWAYS enabled for this plugin+field? \u2192 YES: ALLOW (can overwrite empty values, NEWDEV, plugin sources, etc.) | NO: Continue to step 3</li> <li>Field in SET_EMPTY check: Is SET_EMPTY enabled AND field non-empty+non-NEWDEV? \u2192 REJECT</li> <li>Default behavior: Allow overwrite if field empty or NEWDEV source</li> </ol> <p>Note: Check each plugin's <code>config.json</code> manifest for its specific SET_ALWAYS/SET_EMPTY configuration.</p>"},{"location":"PLUGINS_DEV_CONFIG/#summary","title":"Summary","text":"<p>The lifecycle of a plugin configuration is:</p> <p>Load \u2192 Validate \u2192 Prepare \u2192 Execute \u2192 Parse \u2192 Map \u2192 Persist \u2192 Post-process</p> <p>Each plugin must:</p> <ul> <li>Follow the output contract.</li> <li>Declare its type and expected output structure.</li> <li>Define mappings and watched values clearly in <code>config.json</code>.</li> </ul>"},{"location":"PLUGINS_DEV_DATASOURCES/","title":"Plugin Data Sources","text":"<p>Learn how to configure different data sources for your plugin.</p>"},{"location":"PLUGINS_DEV_DATASOURCES/#overview","title":"Overview","text":"<p>Data sources determine where the plugin gets its data and what format it returns. NetAlertX supports multiple data source types, each suited for different use cases.</p> Data Source Type Purpose Returns Example <code>script</code> Code Execution Execute Linux commands or Python scripts Pipeline Scan network, collect metrics, call APIs <code>app-db-query</code> Database Query Query the NetAlertX database Result set Show devices, open ports, recent events <code>sqlite-db-query</code> External DB Query external SQLite databases Result set PiHole database, external logs <code>template</code> Template Generate values from templates Values Initialize default settings"},{"location":"PLUGINS_DEV_DATASOURCES/#data-source-script","title":"Data Source: <code>script</code>","text":"<p>Execute any Linux command or Python script and capture its output.</p>"},{"location":"PLUGINS_DEV_DATASOURCES/#configuration","title":"Configuration","text":"<pre><code>{\n \"data_source\": \"script\",\n \"show_ui\": true,\n \"mapped_to_table\": \"CurrentScan\"\n}\n</code></pre>"},{"location":"PLUGINS_DEV_DATASOURCES/#how-it-works","title":"How It Works","text":"<ol> <li>Command specified in <code>CMD</code> setting is executed</li> <li>Script writes results to <code>/tmp/log/plugins/last_result.&lt;PREFIX&gt;.log</code></li> <li>Core reads file and parses pipe-delimited results</li> <li>Results inserted into database</li> </ol>"},{"location":"PLUGINS_DEV_DATASOURCES/#example-simple-python-script","title":"Example: Simple Python Script","text":"<pre><code>{\n \"function\": \"CMD\",\n \"type\": {\"dataType\": \"string\", \"elements\": [{\"elementType\": \"input\", \"elementOptions\": [], \"transformers\": []}]},\n \"default_value\": \"python3 /app/front/plugins/my_plugin/script.py\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Command\"}]\n}\n</code></pre>"},{"location":"PLUGINS_DEV_DATASOURCES/#example-bash-script","title":"Example: Bash Script","text":"<pre><code>{\n \"function\": \"CMD\",\n \"default_value\": \"bash /app/front/plugins/my_plugin/script.sh\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Command\"}]\n}\n</code></pre>"},{"location":"PLUGINS_DEV_DATASOURCES/#best-practices","title":"Best Practices","text":"<ul> <li>Always use absolute paths (e.g., <code>/app/front/plugins/...</code>)</li> <li>Use <code>plugin_helper.py</code> for output formatting</li> <li>Add timeouts via <code>RUN_TIMEOUT</code> setting (default: 60s)</li> <li>Log errors to <code>/tmp/log/plugins/&lt;PREFIX&gt;.log</code></li> <li>Handle missing dependencies gracefully</li> </ul>"},{"location":"PLUGINS_DEV_DATASOURCES/#output-format","title":"Output Format","text":"<p>Must write to: <code>/tmp/log/plugins/last_result.&lt;PREFIX&gt;.log</code></p> <p>Format: Pipe-delimited, 9 or 13 columns</p> <p>See Plugin Data Contract for exact format</p>"},{"location":"PLUGINS_DEV_DATASOURCES/#data-source-app-db-query","title":"Data Source: <code>app-db-query</code>","text":"<p>Query the NetAlertX SQLite database and display results.</p>"},{"location":"PLUGINS_DEV_DATASOURCES/#configuration_1","title":"Configuration","text":"<pre><code>{\n \"data_source\": \"app-db-query\",\n \"show_ui\": true,\n \"mapped_to_table\": \"CurrentScan\"\n}\n</code></pre>"},{"location":"PLUGINS_DEV_DATASOURCES/#how-it-works_1","title":"How It Works","text":"<ol> <li>SQL query specified in <code>CMD</code> setting is executed against <code>app.db</code></li> <li>Results parsed according to column definitions</li> <li>Inserted into plugin display/database</li> </ol>"},{"location":"PLUGINS_DEV_DATASOURCES/#sql-query-requirements","title":"SQL Query Requirements","text":"<ul> <li>Must return exactly 9 or 13 columns matching the data contract</li> <li>Column names must match (order matters!)</li> <li>Must be readable SQLite SQL (not vendor-specific)</li> </ul>"},{"location":"PLUGINS_DEV_DATASOURCES/#example-open-ports-from-nmap","title":"Example: Open Ports from Nmap","text":"<pre><code>{\n \"function\": \"CMD\",\n \"type\": {\"dataType\": \"string\", \"elements\": [{\"elementType\": \"input\", \"elementOptions\": [], \"transformers\": []}]},\n \"default_value\": \"SELECT dv.devName as Object_PrimaryID, cast(dv.devLastIP as VARCHAR(100)) || ':' || cast(SUBSTR(ns.Port, 0, INSTR(ns.Port, '/')) as VARCHAR(100)) as Object_SecondaryID, datetime() as DateTime, ns.Service as Watched_Value1, ns.State as Watched_Value2, null as Watched_Value3, null as Watched_Value4, ns.Extra as Extra, dv.devMac as ForeignKey FROM (SELECT * FROM Nmap_Scan) ns LEFT JOIN (SELECT devName, devMac, devLastIP FROM Devices) dv ON ns.MAC = dv.devMac\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"SQL to run\"}],\n \"description\": [{\"language_code\": \"en_us\", \"string\": \"This SQL query populates the plugin table\"}]\n}\n</code></pre>"},{"location":"PLUGINS_DEV_DATASOURCES/#example-recent-device-events","title":"Example: Recent Device Events","text":"<pre><code>SELECT\n e.EventValue as Object_PrimaryID,\n d.devName as Object_SecondaryID,\n e.EventDateTime as DateTime,\n e.EventType as Watched_Value1,\n d.devLastIP as Watched_Value2,\n null as Watched_Value3,\n null as Watched_Value4,\n e.EventDetails as Extra,\n d.devMac as ForeignKey\nFROM\n Events e\nLEFT JOIN\n Devices d ON e.DeviceMac = d.devMac\nWHERE\n e.EventDateTime &gt; datetime('now', '-24 hours')\nORDER BY\n e.EventDateTime DESC\n</code></pre> <p>See the Database documentation for a list of common columns.</p>"},{"location":"PLUGINS_DEV_DATASOURCES/#data-source-sqlite-db-query","title":"Data Source: <code>sqlite-db-query</code>","text":"<p>Query an external SQLite database mounted in the container.</p>"},{"location":"PLUGINS_DEV_DATASOURCES/#configuration_2","title":"Configuration","text":"<p>First, define the database path in a setting:</p> <pre><code>{\n \"function\": \"DB_PATH\",\n \"type\": {\"dataType\": \"string\", \"elements\": [{\"elementType\": \"input\", \"elementOptions\": [], \"transformers\": []}]},\n \"default_value\": \"/etc/pihole/pihole-FTL.db\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Database path\"}],\n \"description\": [{\"language_code\": \"en_us\", \"string\": \"Path to external SQLite database\"}]\n}\n</code></pre> <p>Then set data source and query:</p> <pre><code>{\n \"data_source\": \"sqlite-db-query\",\n \"show_ui\": true\n}\n</code></pre>"},{"location":"PLUGINS_DEV_DATASOURCES/#how-it-works_2","title":"How It Works","text":"<ol> <li>External database file path specified in <code>DB_PATH</code> setting</li> <li>Database mounted at that path (e.g., via Docker volume)</li> <li>SQL query executed against external database using <code>EXTERNAL_&lt;PREFIX&gt;.</code> prefix</li> <li>Results returned in standard format</li> </ol>"},{"location":"PLUGINS_DEV_DATASOURCES/#sql-query-example-pihole-data","title":"SQL Query Example: PiHole Data","text":"<pre><code>{\n \"function\": \"CMD\",\n \"default_value\": \"SELECT hwaddr as Object_PrimaryID, cast('http://' || (SELECT ip FROM EXTERNAL_PIHOLE.network_addresses WHERE network_id = id ORDER BY lastseen DESC LIMIT 1) as VARCHAR(100)) || ':' || cast(SUBSTR((SELECT name FROM EXTERNAL_PIHOLE.network_addresses WHERE network_id = id ORDER BY lastseen DESC LIMIT 1), 0, INSTR((SELECT name FROM EXTERNAL_PIHOLE.network_addresses WHERE network_id = id ORDER BY lastseen DESC LIMIT 1), '/')) as VARCHAR(100)) as Object_SecondaryID, datetime() as DateTime, macVendor as Watched_Value1, lastQuery as Watched_Value2, (SELECT name FROM EXTERNAL_PIHOLE.network_addresses WHERE network_id = id ORDER BY lastseen DESC LIMIT 1) as Watched_Value3, null as Watched_Value4, '' as Extra, hwaddr as ForeignKey FROM EXTERNAL_PIHOLE.network WHERE hwaddr NOT LIKE 'ip-%' AND hwaddr &lt;&gt; '00:00:00:00:00:00'\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"SQL to run\"}]\n}\n</code></pre>"},{"location":"PLUGINS_DEV_DATASOURCES/#key-points","title":"Key Points","text":"<ul> <li>Prefix all external tables with <code>EXTERNAL_&lt;PREFIX&gt;.</code></li> <li>For PiHole (<code>PIHOLE</code> prefix): <code>EXTERNAL_PIHOLE.network</code></li> <li>For custom database (<code>CUSTOM</code> prefix): <code>EXTERNAL_CUSTOM.my_table</code></li> <li>Database must be valid SQLite</li> <li>Path must be accessible inside the container</li> <li>No columns beyond the data contract required</li> </ul>"},{"location":"PLUGINS_DEV_DATASOURCES/#docker-volume-setup","title":"Docker Volume Setup","text":"<p>To mount external database in docker-compose:</p> <pre><code>services:\n netalertx:\n volumes:\n - /path/on/host/pihole-FTL.db:/etc/pihole/pihole-FTL.db:ro\n</code></pre>"},{"location":"PLUGINS_DEV_DATASOURCES/#data-source-template","title":"Data Source: <code>template</code>","text":"<p>Generate values from a template. Usually used for initialization and default settings.</p>"},{"location":"PLUGINS_DEV_DATASOURCES/#configuration_3","title":"Configuration","text":"<pre><code>{\n \"data_source\": \"template\"\n}\n</code></pre>"},{"location":"PLUGINS_DEV_DATASOURCES/#how-it-works_3","title":"How It Works","text":"<ul> <li>Not widely used in standard plugins</li> <li>Used internally for generating default values</li> <li>Check <code>newdev_template</code> plugin for implementation example</li> </ul>"},{"location":"PLUGINS_DEV_DATASOURCES/#example-default-device-template","title":"Example: Default Device Template","text":"<pre><code>{\n \"function\": \"DEFAULT_DEVICE_PROPERTIES\",\n \"type\": {\"dataType\": \"string\", \"elements\": [{\"elementType\": \"textarea\", \"elementOptions\": [], \"transformers\": []}]},\n \"default_value\": \"type=Unknown|vendor=Unknown\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Default properties\"}]\n}\n</code></pre>"},{"location":"PLUGINS_DEV_DATASOURCES/#data-source-plugin_type","title":"Data Source: <code>plugin_type</code>","text":"<p>Declare the plugin category. Controls where settings appear in the UI.</p>"},{"location":"PLUGINS_DEV_DATASOURCES/#configuration_4","title":"Configuration","text":"<pre><code>{\n \"data_source\": \"plugin_type\",\n \"value\": \"scanner\"\n}\n</code></pre>"},{"location":"PLUGINS_DEV_DATASOURCES/#supported-values","title":"Supported Values","text":"Value Section Purpose <code>scanner</code> Device Scanners Discovers devices on network <code>system</code> System Plugins Core system functionality <code>publisher</code> Notification/Alert Plugins Sends notifications/alerts <code>importer</code> Data Importers Imports devices from external sources <code>other</code> Other Plugins Miscellaneous functionality"},{"location":"PLUGINS_DEV_DATASOURCES/#example","title":"Example","text":"<pre><code>{\n \"settings\": [\n {\n \"function\": \"plugin_type\",\n \"type\": {\"dataType\": \"string\", \"elements\": []},\n \"default_value\": \"scanner\",\n \"options\": [\"scanner\"],\n \"data_source\": \"plugin_type\",\n \"value\": \"scanner\",\n \"localized\": []\n }\n ]\n}\n</code></pre>"},{"location":"PLUGINS_DEV_DATASOURCES/#execution-order","title":"Execution Order","text":"<p>Control plugin execution priority. Higher priority plugins run first.</p> <pre><code>{\n \"execution_order\": \"Layer_0\"\n}\n</code></pre>"},{"location":"PLUGINS_DEV_DATASOURCES/#levels-highest-to-lowest-priority","title":"Levels (highest to lowest priority)","text":"<ul> <li><code>Layer_0</code> - Highest priority (run first)</li> <li><code>Layer_1</code></li> <li><code>Layer_2</code></li> <li>...</li> </ul>"},{"location":"PLUGINS_DEV_DATASOURCES/#example-device-discovery","title":"Example: Device Discovery","text":"<pre><code>{\n \"code_name\": \"device_scanner\",\n \"unique_prefix\": \"DEVSCAN\",\n \"execution_order\": \"Layer_0\",\n \"data_source\": \"script\",\n \"mapped_to_table\": \"CurrentScan\"\n}\n</code></pre>"},{"location":"PLUGINS_DEV_DATASOURCES/#performance-considerations","title":"Performance Considerations","text":""},{"location":"PLUGINS_DEV_DATASOURCES/#script-source","title":"Script Source","text":"<ul> <li>Pros: Flexible, can call external tools</li> <li>Cons: Slower than database queries</li> <li>Optimization: Add caching, use timeouts</li> <li>Default timeout: 60 seconds (set <code>RUN_TIMEOUT</code>)</li> </ul>"},{"location":"PLUGINS_DEV_DATASOURCES/#database-query-source","title":"Database Query Source","text":"<ul> <li>Pros: Fast, native query optimization</li> <li>Cons: Limited to SQL capabilities</li> <li>Optimization: Use indexes, avoid complex joins</li> <li>Timeout: Usually instant</li> </ul>"},{"location":"PLUGINS_DEV_DATASOURCES/#external-db-query-source","title":"External DB Query Source","text":"<ul> <li>Pros: Direct access to external data</li> <li>Cons: Network latency, external availability</li> <li>Optimization: Use indexes in external DB, selective queries</li> <li>Timeout: Depends on DB response time</li> </ul>"},{"location":"PLUGINS_DEV_DATASOURCES/#debugging-data-sources","title":"Debugging Data Sources","text":""},{"location":"PLUGINS_DEV_DATASOURCES/#check-script-output","title":"Check Script Output","text":"<pre><code># Run script manually\npython3 /app/front/plugins/my_plugin/script.py\n\n# Check result file\ncat /tmp/log/plugins/last_result.MYPREFIX.log\n</code></pre>"},{"location":"PLUGINS_DEV_DATASOURCES/#test-sql-query","title":"Test SQL Query","text":"<pre><code># Connect to app database\nsqlite3 /data/db/app.db\n\n# Run query\nsqlite&gt; SELECT ... ;\n</code></pre>"},{"location":"PLUGINS_DEV_DATASOURCES/#monitor-execution","title":"Monitor Execution","text":"<pre><code># Watch backend logs\ntail -f /tmp/log/stdout.log | grep -i \"data_source\\|MYPREFIX\"\n</code></pre>"},{"location":"PLUGINS_DEV_DATASOURCES/#see-also","title":"See Also","text":"<ul> <li>Plugin Data Contract - Output format specification</li> <li>Plugin Settings System - How to define settings</li> <li>Quick Start Guide - Create your first plugin</li> <li>Database Schema - Available tables and columns</li> <li>Debugging Plugins - Troubleshooting data issues</li> </ul>"},{"location":"PLUGINS_DEV_DATA_CONTRACT/","title":"Plugin Data Contract","text":"<p>This document specifies the exact interface between plugins and the NetAlertX core.</p> <p>Important</p> <p>Every plugin must output data in this exact format to be recognized and processed correctly.</p>"},{"location":"PLUGINS_DEV_DATA_CONTRACT/#overview","title":"Overview","text":"<p>Plugins communicate with NetAlertX by writing results to a pipe-delimited log file. The core reads this file, parses the data, and processes it for notifications, device discovery, and data integration.</p> <p>File Location: <code>/tmp/log/plugins/last_result.&lt;PREFIX&gt;.log</code></p> <p>Format: Pipe-delimited (<code>|</code>), one record per line</p> <p>Required Columns: 9 (mandatory) + up to 4 optional helper columns = 13 total</p>"},{"location":"PLUGINS_DEV_DATA_CONTRACT/#column-specification","title":"Column Specification","text":"<p>Note</p> <p>The order of columns is FIXED and cannot be changed. All 9 mandatory columns must be provided. If you use any optional column (<code>HelpVal1</code>), you must supply all optional columns (<code>HelpVal1</code> through <code>HelpVal4</code>).</p>"},{"location":"PLUGINS_DEV_DATA_CONTRACT/#mandatory-columns-08","title":"Mandatory Columns (0\u20138)","text":"Order Column Name Type Required Description 0 <code>Object_PrimaryID</code> string YES The primary identifier for grouping. Examples: device MAC, hostname, service name, or any unique ID 1 <code>Object_SecondaryID</code> string no Secondary identifier for relationships (e.g., IP address, port, sub-ID). Use <code>null</code> if not needed 2 <code>DateTime</code> string YES Timestamp when the event/data was collected. Format: <code>YYYY-MM-DD HH:MM:SS</code> 3 <code>Watched_Value1</code> string YES Primary watched value. Changes trigger notifications. Examples: IP address, status, version 4 <code>Watched_Value2</code> string no Secondary watched value. Use <code>null</code> if not needed 5 <code>Watched_Value3</code> string no Tertiary watched value. Use <code>null</code> if not needed 6 <code>Watched_Value4</code> string no Quaternary watched value. Use <code>null</code> if not needed 7 <code>Extra</code> string no Any additional metadata to display in UI and notifications. Use <code>null</code> if not needed 8 <code>ForeignKey</code> string no Foreign key linking to parent object (usually MAC address for device relationship). Use <code>null</code> if not needed"},{"location":"PLUGINS_DEV_DATA_CONTRACT/#optional-columns-912","title":"Optional Columns (9\u201312)","text":"Order Column Name Type Required Description 9 <code>HelpVal1</code> string conditional Helper value 1. If used, all help values must be supplied 10 <code>HelpVal2</code> string conditional Helper value 2. If used, all help values must be supplied 11 <code>HelpVal3</code> string conditional Helper value 3. If used, all help values must be supplied 12 <code>HelpVal4</code> string conditional Helper value 4. If used, all help values must be supplied"},{"location":"PLUGINS_DEV_DATA_CONTRACT/#usage-guide","title":"Usage Guide","text":""},{"location":"PLUGINS_DEV_DATA_CONTRACT/#emptynull-values","title":"Empty/Null Values","text":"<ul> <li>Represent empty values as the literal string <code>null</code> (not Python <code>None</code>, SQL <code>NULL</code>, or empty string)</li> <li>Example: <code>device_id|null|2023-01-02 15:56:30|status|null|null|null|null|null</code></li> </ul>"},{"location":"PLUGINS_DEV_DATA_CONTRACT/#watched-values","title":"Watched Values","text":"<p>What are Watched Values?</p> <p>Watched values are fields that the NetAlertX core monitors for changes between scans. When a watched value differs from the previous scan, it can trigger notifications.</p> <p>How to use them:</p> <ul> <li><code>Watched_Value1</code>: Always required; primary indicator of status/state</li> <li><code>Watched_Value2\u20134</code>: Optional; use for secondary/tertiary state information</li> <li>Leave unused ones as <code>null</code></li> </ul> <p>Example:</p> <ul> <li>Device scanner: <code>Watched_Value1 = \"online\"</code> or <code>\"offline\"</code></li> <li>Port scanner: <code>Watched_Value1 = \"80\"</code> (port number), <code>Watched_Value2 = \"open\"</code> (state)</li> <li>Service monitor: <code>Watched_Value1 = \"200\"</code> (HTTP status), <code>Watched_Value2 = \"0.45\"</code> (response time)</li> </ul>"},{"location":"PLUGINS_DEV_DATA_CONTRACT/#foreign-key","title":"Foreign Key","text":"<p>Use the <code>ForeignKey</code> column to link objects to a parent device by MAC address:</p> <pre><code>device_name|192.168.1.100|2023-01-02 15:56:30|online|null|null|null|Found on network|aa:bb:cc:dd:ee:ff\n \u2191\n ForeignKey (MAC)\n</code></pre> <p>This allows NetAlertX to:</p> <ul> <li>Display the object on the device details page</li> <li>Send notifications when the parent device is involved</li> <li>Link events across plugins</li> </ul>"},{"location":"PLUGINS_DEV_DATA_CONTRACT/#examples","title":"Examples","text":""},{"location":"PLUGINS_DEV_DATA_CONTRACT/#valid-data-9-columns-minimal","title":"Valid Data (9 columns, minimal)","text":"<pre><code>https://example.com|null|2023-01-02 15:56:30|200|null|null|null|null|null\nprinter-hp-1|192.168.1.50|2023-01-02 15:56:30|online|50%|null|null|Last seen in office|aa:11:22:33:44:55\ngateway.local|null|2023-01-02 15:56:30|active|v2.1.5|null|null|Firmware version|null\n</code></pre>"},{"location":"PLUGINS_DEV_DATA_CONTRACT/#valid-data-13-columns-with-helpers","title":"Valid Data (13 columns, with helpers)","text":"<pre><code>service-api|192.168.1.100:8080|2023-01-02 15:56:30|200|45ms|true|null|Responding normally|aa:bb:cc:dd:ee:ff|extra1|extra2|extra3|extra4\nhost-web-1|10.0.0.20|2023-01-02 15:56:30|active|256GB|online|ok|Production server|null|cpu:80|mem:92|disk:45|alerts:0\n</code></pre>"},{"location":"PLUGINS_DEV_DATA_CONTRACT/#invalid-data-common-errors","title":"Invalid Data (Common Errors)","text":"<p>\u274c Missing required column (only 8 separators instead of 8): <pre><code>https://google.com|null|2023-01-02 15:56:30|200|0.7898||null|null\n \u2191\n Missing pipe\n</code></pre></p> <p>\u274c Missing mandatory Watched_Value1 (column 3): <pre><code>https://duckduckgo.com|192.168.1.1|2023-01-02 15:56:30|null|0.9898|null|null|Best|null\n \u2191\n Must not be null\n</code></pre></p> <p>\u274c Incomplete optional columns (has HelpVal1 but missing HelpVal2\u20134): <pre><code>device|null|2023-01-02 15:56:30|status|null|null|null|null|null|helper1\n \u2191\n Has helper but incomplete\n</code></pre></p> <p>\u2705 Complete with helpers (all 4 helpers provided): <pre><code>device|null|2023-01-02 15:56:30|status|null|null|null|null|null|h1|h2|h3|h4\n</code></pre></p> <p>\u2705 Complete without helpers (9 columns exactly): <pre><code>device|null|2023-01-02 15:56:30|status|null|null|null|null|null\n</code></pre></p>"},{"location":"PLUGINS_DEV_DATA_CONTRACT/#using-plugin_helperpy","title":"Using <code>plugin_helper.py</code>","text":"<p>The easiest way to ensure correct output is to use the <code>plugin_helper.py</code> library:</p> <pre><code>from plugin_helper import Plugin_Objects\n\n# Initialize with your plugin's prefix\nplugin_objects = Plugin_Objects(\"YOURPREFIX\")\n\n# Add objects\nplugin_objects.add_object(\n Object_PrimaryID=\"device_id\",\n Object_SecondaryID=\"192.168.1.1\",\n DateTime=\"2023-01-02 15:56:30\",\n Watched_Value1=\"online\",\n Watched_Value2=None,\n Watched_Value3=None,\n Watched_Value4=None,\n Extra=\"Additional data\",\n ForeignKey=\"aa:bb:cc:dd:ee:ff\",\n HelpVal1=None,\n HelpVal2=None,\n HelpVal3=None,\n HelpVal4=None\n)\n\n# Write results (handles formatting, sanitization, and file creation)\nplugin_objects.write_result_file()\n</code></pre> <p>The library automatically:</p> <ul> <li>Validates data types</li> <li>Sanitizes string values</li> <li>Normalizes MAC addresses</li> <li>Writes to the correct file location</li> <li>Creates the file in <code>/tmp/log/plugins/last_result.&lt;PREFIX&gt;.log</code></li> </ul>"},{"location":"PLUGINS_DEV_DATA_CONTRACT/#de-duplication","title":"De-duplication","text":"<p>The core runs de-duplication once per hour on the <code>Plugins_Objects</code> table:</p> <ul> <li>Duplicate Detection Key: Combination of <code>Object_PrimaryID</code>, <code>Object_SecondaryID</code>, <code>Plugin</code> (auto-filled from <code>unique_prefix</code>), and <code>UserData</code></li> <li>Resolution: Oldest duplicate entries are removed, newest are kept</li> <li>Use Case: Prevents duplicate notifications when the same object is detected multiple times</li> </ul>"},{"location":"PLUGINS_DEV_DATA_CONTRACT/#datetime-format","title":"DateTime Format","text":"<p>Required Format: <code>YYYY-MM-DD HH:MM:SS</code></p> <p>Examples: - <code>2023-01-02 15:56:30</code> \u2705 - <code>2023-1-2 15:56:30</code> \u274c (missing leading zeros) - <code>2023-01-02T15:56:30</code> \u274c (wrong separator) - <code>15:56:30 2023-01-02</code> \u274c (wrong order)</p> <p>Python Helper: <pre><code>from datetime import datetime\n\n# Current time in correct format\nnow = datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n# Output: \"2023-01-02 15:56:30\"\n</code></pre></p> <p>Bash Helper: <pre><code># Current time in correct format\ndate '+%Y-%m-%d %H:%M:%S'\n# Output: 2023-01-02 15:56:30\n</code></pre></p>"},{"location":"PLUGINS_DEV_DATA_CONTRACT/#validation-checklist","title":"Validation Checklist","text":"<p>Before writing your plugin's <code>script.py</code>, ensure:</p> <ul> <li> 9 or 13 columns in each output line (8 or 12 pipe separators)</li> <li> Mandatory columns filled:</li> <li>Column 0: <code>Object_PrimaryID</code> (not null)</li> <li>Column 2: <code>DateTime</code> in <code>YYYY-MM-DD HH:MM:SS</code> format</li> <li>Column 3: <code>Watched_Value1</code> (not null)</li> <li> Null values as literal string <code>null</code> (not empty string or special chars)</li> <li> No extra pipes or misaligned columns</li> <li> If using optional helpers (columns 9\u201312), all 4 must be present</li> <li> File written to <code>/tmp/log/plugins/last_result.&lt;PREFIX&gt;.log</code></li> <li> One record per line (newline-delimited)</li> <li> No header row (data only)</li> </ul>"},{"location":"PLUGINS_DEV_DATA_CONTRACT/#debugging","title":"Debugging","text":"<p>View raw plugin output: <pre><code>cat /tmp/log/plugins/last_result.YOURPREFIX.log\n</code></pre></p> <p>Check line count: <pre><code>wc -l /tmp/log/plugins/last_result.YOURPREFIX.log\n</code></pre></p> <p>Validate column count (should be 8 or 12 pipes per line): <pre><code>cat /tmp/log/plugins/last_result.YOURPREFIX.log | awk -F'|' '{print NF}' | sort | uniq\n# Output: 9 (for minimal) or 13 (for with helpers)\n</code></pre></p> <p>Check core processing in logs: <pre><code>tail -f /tmp/log/stdout.log | grep -i \"YOURPREFIX\\|Plugins_Objects\"\n</code></pre></p>"},{"location":"PLUGINS_DEV_DATA_CONTRACT/#see-also","title":"See Also","text":"<ul> <li>Plugin Settings System - How to accept user input</li> <li>Data Sources - Different data source types</li> <li>Debugging Plugins - Troubleshooting plugin issues</li> </ul>"},{"location":"PLUGINS_DEV_QUICK_START/","title":"Plugin Development Quick Start","text":"<p>Get a working plugin up and running in 5 minutes.</p>"},{"location":"PLUGINS_DEV_QUICK_START/#prerequisites","title":"Prerequisites","text":"<ul> <li>Read Development Environment Setup Guide to set up your local environment</li> <li>Understand Plugin Architecture Overview</li> </ul>"},{"location":"PLUGINS_DEV_QUICK_START/#quick-start-steps","title":"Quick Start Steps","text":""},{"location":"PLUGINS_DEV_QUICK_START/#1-create-your-plugin-folder","title":"1. Create Your Plugin Folder","text":"<p>Start from the template to get the basic structure:</p> <pre><code>cd /workspaces/NetAlertX/front/plugins\ncp -r __template my_plugin\ncd my_plugin\n</code></pre>"},{"location":"PLUGINS_DEV_QUICK_START/#2-update-configjson-identifiers","title":"2. Update <code>config.json</code> Identifiers","text":"<p>Edit <code>my_plugin/config.json</code> and update these critical fields:</p> <pre><code>{\n \"code_name\": \"my_plugin\",\n \"unique_prefix\": \"MYPLN\",\n \"display_name\": [{\"language_code\": \"en_us\", \"string\": \"My Plugin\"}],\n \"description\": [{\"language_code\": \"en_us\", \"string\": \"My custom plugin\"}]\n}\n</code></pre> <p>Important: - <code>code_name</code> must match the folder name - <code>unique_prefix</code> must be unique and uppercase (check existing plugins to avoid conflicts) - <code>unique_prefix</code> is used as a prefix for all generated settings (e.g., <code>MYPLN_RUN</code>, <code>MYPLN_CMD</code>)</p>"},{"location":"PLUGINS_DEV_QUICK_START/#3-implement-your-script","title":"3. Implement Your Script","text":"<p>Edit <code>my_plugin/script.py</code> and implement your data collection logic:</p> <pre><code>#!/usr/bin/env python3\n\nimport sys\nimport os\nsys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../server'))\nsys.path.insert(0, os.path.join(os.path.dirname(__file__), '../plugins'))\n\nfrom plugin_helper import Plugin_Objects, mylog\nfrom helper import get_setting_value\nfrom const import logPath\n\npluginName = \"MYPLN\"\n\nLOG_PATH = logPath + \"/plugins\"\nLOG_FILE = os.path.join(LOG_PATH, f\"script.{pluginName}.log\")\nRESULT_FILE = os.path.join(LOG_PATH, f\"last_result.{pluginName}.log\")\n\n# Initialize\nplugin_objects = Plugin_Objects(RESULT_FILE)\n\ntry:\n # Your data collection logic here\n # For example, scan something and collect results\n\n # Add an object to results\n plugin_objects.add_object(\n Object_PrimaryID=\"example_id\",\n Object_SecondaryID=None,\n DateTime=\"2023-01-02 15:56:30\",\n Watched_Value1=\"value1\",\n Watched_Value2=None,\n Watched_Value3=None,\n Watched_Value4=None,\n Extra=\"additional_data\",\n ForeignKey=None\n )\n\n # Write results to the log file\n plugin_objects.write_result_file()\n\nexcept Exception as e:\n mylog(\"none\", f\"Error: {e}\")\n sys.exit(1)\n</code></pre>"},{"location":"PLUGINS_DEV_QUICK_START/#4-configure-execution","title":"4. Configure Execution","text":"<p>Edit the <code>RUN</code> and <code>CMD</code> settings in <code>config.json</code>:</p> <pre><code>{\n \"function\": \"RUN\",\n \"type\": {\"dataType\":\"string\", \"elements\": [{\"elementType\": \"select\", \"elementOptions\": [], \"transformers\": []}]},\n \"default_value\": \"disabled\",\n \"options\": [\"disabled\", \"once\", \"schedule\"],\n \"localized\": [\"name\", \"description\"],\n \"name\": [{\"language_code\":\"en_us\", \"string\": \"When to run\"}],\n \"description\": [{\"language_code\":\"en_us\", \"string\": \"Enable plugin execution\"}]\n},\n{\n \"function\": \"CMD\",\n \"type\": {\"dataType\":\"string\", \"elements\": [{\"elementType\": \"input\", \"elementOptions\": [], \"transformers\": []}]},\n \"default_value\": \"python3 /app/front/plugins/my_plugin/script.py\",\n \"localized\": [\"name\", \"description\"],\n \"name\": [{\"language_code\":\"en_us\", \"string\": \"Command\"}],\n \"description\": [{\"language_code\":\"en_us\", \"string\": \"Command to execute\"}]\n}\n</code></pre>"},{"location":"PLUGINS_DEV_QUICK_START/#5-test-your-plugin","title":"5. Test Your Plugin","text":""},{"location":"PLUGINS_DEV_QUICK_START/#in-dev-container","title":"In Dev Container","text":"<pre><code># Test the script directly\npython3 /workspaces/NetAlertX/front/plugins/my_plugin/script.py\n\n# Check the results\ncat /tmp/log/plugins/last_result.MYPLN.log\n</code></pre>"},{"location":"PLUGINS_DEV_QUICK_START/#via-ui","title":"Via UI","text":"<ol> <li>Restart backend: Run task <code>[Dev Container] Start Backend (Python)</code></li> <li>Open Settings \u2192 Plugin Settings \u2192 My Plugin</li> <li>Set <code>My Plugin - When to run</code> to <code>once</code></li> <li>Click Save</li> <li>Check <code>/tmp/log/plugins/last_result.MYPLN.log</code> for output</li> </ol>"},{"location":"PLUGINS_DEV_QUICK_START/#6-check-results","title":"6. Check Results","text":"<p>Verify your plugin is working:</p> <pre><code># Check if result file was generated\nls -la /tmp/log/plugins/last_result.MYPLN.log\n\n# View contents\ncat /tmp/log/plugins/last_result.MYPLN.log\n\n# Check backend logs for errors\ntail -f /tmp/log/stdout.log | grep \"my_plugin\\|MYPLN\"\n</code></pre>"},{"location":"PLUGINS_DEV_QUICK_START/#next-steps","title":"Next Steps","text":"<p>Now that you have a working basic plugin:</p> <ol> <li>Add Settings: Customize behavior via user-configurable settings (see PLUGINS_DEV_SETTINGS.md)</li> <li>Implement Data Contract: Structure your output correctly (see PLUGINS_DEV_DATA_CONTRACT.md)</li> <li>Configure UI: Display plugin results in the web interface (see PLUGINS_DEV_UI_COMPONENTS.md)</li> <li>Map to Database: Import data into NetAlertX tables like <code>CurrentScan</code> or <code>Devices</code></li> <li>Set Schedules: Run your plugin on a schedule (see PLUGINS_DEV_CONFIG.md)</li> </ol>"},{"location":"PLUGINS_DEV_QUICK_START/#common-issues","title":"Common Issues","text":"Issue Solution \"Module not found\" errors Ensure <code>sys.path</code> includes <code>/app/server</code> and <code>/app/front/plugins</code> Settings not appearing Restart backend and clear browser cache Results not showing up Check <code>/tmp/log/plugins/*.log</code> and <code>/tmp/log/stdout.log</code> for errors Permission denied Plugin runs in container, use absolute paths like <code>/app/front/plugins/...</code>"},{"location":"PLUGINS_DEV_QUICK_START/#resources","title":"Resources","text":"<ul> <li>Full Plugin Development Guide</li> <li>Plugin Data Contract</li> <li>Plugin Settings System</li> <li>Data Sources</li> <li>UI Components</li> <li>Debugging Plugins</li> </ul>"},{"location":"PLUGINS_DEV_SETTINGS/","title":"Plugin Settings System","text":"<p>Learn how to let users configure your plugin via the NetAlertX UI Settings page.</p> <p>Tip</p> <p>For the higher-level settings flow and lifecycle, see Settings System Documentation.</p>"},{"location":"PLUGINS_DEV_SETTINGS/#overview","title":"Overview","text":"<p>Plugin settings allow users to configure:</p> <ul> <li>Execution schedule (when the plugin runs)</li> <li>Runtime parameters (API keys, URLs, thresholds)</li> <li>Behavior options (which features to enable/disable)</li> <li>Command overrides (customize the executed script)</li> </ul> <p>All settings are defined in your plugin's <code>config.json</code> file under the <code>\"settings\"</code> array.</p>"},{"location":"PLUGINS_DEV_SETTINGS/#setting-definition-structure","title":"Setting Definition Structure","text":"<p>Each setting is a JSON object with required and optional properties:</p> <pre><code>{\n \"function\": \"UNIQUE_CODE\",\n \"type\": {\n \"dataType\": \"string\",\n \"elements\": [\n {\n \"elementType\": \"input\",\n \"elementOptions\": [],\n \"transformers\": []\n }\n ]\n },\n \"default_value\": \"default_value_here\",\n \"options\": [],\n \"localized\": [\"name\", \"description\"],\n \"name\": [\n {\n \"language_code\": \"en_us\",\n \"string\": \"Display Name\"\n }\n ],\n \"description\": [\n {\n \"language_code\": \"en_us\",\n \"string\": \"Help text describing the setting\"\n }\n ]\n}\n</code></pre>"},{"location":"PLUGINS_DEV_SETTINGS/#required-properties","title":"Required Properties","text":"Property Type Description Example <code>function</code> string Unique identifier for the setting. Used in manifest and when reading values. See Reserved Function Names for special values <code>\"MY_CUSTOM_SETTING\"</code> <code>type</code> object Defines the UI component and data type See Component Types <code>default_value</code> varies Initial value shown in UI <code>\"https://example.com\"</code> <code>localized</code> array Which properties have translations <code>[\"name\", \"description\"]</code> <code>name</code> array Display name in Settings UI (localized) See Localized Strings <code>description</code> array Help text in Settings UI (localized) See Localized Strings"},{"location":"PLUGINS_DEV_SETTINGS/#optional-properties","title":"Optional Properties","text":"Property Type Description Example <code>options</code> array Valid values for select/checkbox controls <code>[\"option1\", \"option2\"]</code> <code>events</code> string Trigger action button: <code>\"test\"</code> or <code>\"run\"</code> <code>\"test\"</code> for notifications <code>maxLength</code> number Character limit for input fields <code>100</code> <code>readonly</code> boolean Make field read-only <code>true</code> <code>override_value</code> object Template-based value override (WIP) Work in Progress"},{"location":"PLUGINS_DEV_SETTINGS/#reserved-function-names","title":"Reserved Function Names","text":"<p>These function names have special meaning and control core plugin behavior:</p>"},{"location":"PLUGINS_DEV_SETTINGS/#core-execution-settings","title":"Core Execution Settings","text":"Function Purpose Type Required Options <code>RUN</code> When to execute the plugin select YES <code>\"disabled\"</code>, <code>\"once\"</code>, <code>\"schedule\"</code>, <code>\"always_after_scan\"</code>, <code>\"before_name_updates\"</code>, <code>\"on_new_device\"</code>, <code>\"before_config_save\"</code> <code>RUN_SCHD</code> Cron schedule input If <code>RUN=\"schedule\"</code> Cron format: <code>\"0 * * * *\"</code> (hourly) <code>CMD</code> Command/script to execute input YES Linux command or path to script <code>RUN_TIMEOUT</code> Maximum execution time in seconds input optional Numeric: <code>\"60\"</code>, <code>\"120\"</code>, etc."},{"location":"PLUGINS_DEV_SETTINGS/#data-filtering-settings","title":"Data &amp; Filtering Settings","text":"Function Purpose Type Required Options <code>WATCH</code> Which columns to monitor for changes multi-select optional Column names from data contract <code>REPORT_ON</code> When to send notifications select optional <code>\"new\"</code>, <code>\"watched-changed\"</code>, <code>\"watched-not-changed\"</code>, <code>\"missing-in-last-scan\"</code> <code>DB_PATH</code> External database path input If using SQLite plugin File path: <code>\"/etc/pihole/pihole-FTL.db\"</code>"},{"location":"PLUGINS_DEV_SETTINGS/#api-data-settings","title":"API &amp; Data Settings","text":"Function Purpose Type Required Options <code>API_SQL</code> Generate API JSON file (reserved) Not implemented \u2014"},{"location":"PLUGINS_DEV_SETTINGS/#component-types","title":"Component Types","text":""},{"location":"PLUGINS_DEV_SETTINGS/#text-input","title":"Text Input","text":"<p>Simple text field for API keys, URLs, thresholds, etc.</p> <pre><code>{\n \"function\": \"URL\",\n \"type\": {\n \"dataType\": \"string\",\n \"elements\": [\n {\n \"elementType\": \"input\",\n \"elementOptions\": [],\n \"transformers\": []\n }\n ]\n },\n \"default_value\": \"https://api.example.com\",\n \"localized\": [\"name\", \"description\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"API URL\"}],\n \"description\": [{\"language_code\": \"en_us\", \"string\": \"The API endpoint to query\"}]\n}\n</code></pre>"},{"location":"PLUGINS_DEV_SETTINGS/#password-input","title":"Password Input","text":"<p>Secure field with SHA256 hashing transformer.</p> <pre><code>{\n \"function\": \"API_KEY\",\n \"type\": {\n \"dataType\": \"string\",\n \"elements\": [\n {\n \"elementType\": \"input\",\n \"elementOptions\": [{\"type\": \"password\"}],\n \"transformers\": [\"sha256\"]\n }\n ]\n },\n \"default_value\": \"\",\n \"localized\": [\"name\", \"description\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"API Key\"}],\n \"description\": [{\"language_code\": \"en_us\", \"string\": \"Stored securely with SHA256 hashing\"}]\n}\n</code></pre>"},{"location":"PLUGINS_DEV_SETTINGS/#dropdownselect","title":"Dropdown/Select","text":"<p>Choose from predefined options.</p> <pre><code>{\n \"function\": \"RUN\",\n \"type\": {\n \"dataType\": \"string\",\n \"elements\": [\n {\n \"elementType\": \"select\",\n \"elementOptions\": [],\n \"transformers\": []\n }\n ]\n },\n \"default_value\": \"disabled\",\n \"options\": [\"disabled\", \"once\", \"schedule\", \"always_after_scan\"],\n \"localized\": [\"name\", \"description\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"When to run\"}],\n \"description\": [{\"language_code\": \"en_us\", \"string\": \"Select execution trigger\"}]\n}\n</code></pre>"},{"location":"PLUGINS_DEV_SETTINGS/#multi-select","title":"Multi-Select","text":"<p>Select multiple values (returns array).</p> <pre><code>{\n \"function\": \"WATCH\",\n \"type\": {\n \"dataType\": \"array\",\n \"elements\": [\n {\n \"elementType\": \"select\",\n \"elementOptions\": [{\"isMultiSelect\": true}],\n \"transformers\": []\n }\n ]\n },\n \"default_value\": [],\n \"options\": [\"Status\", \"IP_Address\", \"Response_Time\"],\n \"localized\": [\"name\", \"description\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Watch columns\"}],\n \"description\": [{\"language_code\": \"en_us\", \"string\": \"Which columns trigger notifications on change\"}]\n}\n</code></pre>"},{"location":"PLUGINS_DEV_SETTINGS/#checkbox","title":"Checkbox","text":"<p>Boolean toggle.</p> <pre><code>{\n \"function\": \"ENABLED\",\n \"type\": {\n \"dataType\": \"boolean\",\n \"elements\": [\n {\n \"elementType\": \"input\",\n \"elementOptions\": [{\"type\": \"checkbox\"}],\n \"transformers\": []\n }\n ]\n },\n \"default_value\": false,\n \"localized\": [\"name\", \"description\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Enable feature\"}],\n \"description\": [{\"language_code\": \"en_us\", \"string\": \"Toggle this feature on/off\"}]\n}\n</code></pre>"},{"location":"PLUGINS_DEV_SETTINGS/#textarea","title":"Textarea","text":"<p>Multi-line text input.</p> <pre><code>{\n \"function\": \"CUSTOM_CONFIG\",\n \"type\": {\n \"dataType\": \"string\",\n \"elements\": [\n {\n \"elementType\": \"textarea\",\n \"elementOptions\": [],\n \"transformers\": []\n }\n ]\n },\n \"default_value\": \"\",\n \"localized\": [\"name\", \"description\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Custom Configuration\"}],\n \"description\": [{\"language_code\": \"en_us\", \"string\": \"Enter configuration (one per line)\"}]\n}\n</code></pre>"},{"location":"PLUGINS_DEV_SETTINGS/#read-only-label","title":"Read-Only Label","text":"<p>Display information without user input.</p> <pre><code>{\n \"function\": \"STATUS_DISPLAY\",\n \"type\": {\n \"dataType\": \"string\",\n \"elements\": [\n {\n \"elementType\": \"input\",\n \"elementOptions\": [{\"readonly\": true}],\n \"transformers\": []\n }\n ]\n },\n \"default_value\": \"Ready\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Status\"}]\n}\n</code></pre>"},{"location":"PLUGINS_DEV_SETTINGS/#using-settings-in-your-script","title":"Using Settings in Your Script","text":""},{"location":"PLUGINS_DEV_SETTINGS/#method-1-via-get_setting_value-helper","title":"Method 1: Via <code>get_setting_value()</code> Helper","text":"<p>Recommended approach \u2014 clean and simple:</p> <pre><code>from helper import get_setting_value\n\n# Read the setting by function name with plugin prefix\napi_url = get_setting_value('MYPLN_API_URL')\napi_key = get_setting_value('MYPLN_API_KEY')\nwatch_columns = get_setting_value('MYPLN_WATCH') # Returns list if multi-select\n\n# Use in your script\nmylog(\"none\", f\"Connecting to {api_url} with key {api_key}\")\n</code></pre>"},{"location":"PLUGINS_DEV_SETTINGS/#method-2-via-command-parameters","title":"Method 2: Via Command Parameters","text":"<p>For more complex scenarios where you need to pass settings as command-line arguments:</p> <p>Define <code>params</code> in your <code>config.json</code>:</p> <pre><code>{\n \"params\": [\n {\n \"name\": \"api_url\",\n \"type\": \"setting\",\n \"value\": \"MYPLN_API_URL\"\n },\n {\n \"name\": \"timeout\",\n \"type\": \"setting\",\n \"value\": \"MYPLN_RUN_TIMEOUT\"\n }\n ]\n}\n</code></pre> <p>Update your <code>CMD</code> setting:</p> <pre><code>{\n \"function\": \"CMD\",\n \"default_value\": \"python3 /app/front/plugins/my_plugin/script.py --url={api_url} --timeout={timeout}\"\n}\n</code></pre> <p>The framework will replace <code>{api_url}</code> and <code>{timeout}</code> with actual values before execution.</p>"},{"location":"PLUGINS_DEV_SETTINGS/#method-3-via-environment-variables-check-with-maintainer","title":"Method 3: Via Environment Variables (check with maintainer)","text":"<p>Settings are also available as environment variables:</p> <pre><code># Environment variable format: &lt;PREFIX&gt;_&lt;FUNCTION&gt;\nMY_PLUGIN_API_URL\nMY_PLUGIN_API_KEY\nMY_PLUGIN_RUN\n</code></pre> <p>In Python: <pre><code>import os\n\napi_url = os.environ.get('MYPLN_API_URL', 'default_value')\n</code></pre></p>"},{"location":"PLUGINS_DEV_SETTINGS/#localized-strings","title":"Localized Strings","text":"<p>Settings and UI text support multiple languages. Define translations in the <code>name</code> and <code>description</code> arrays:</p> <pre><code>{\n \"localized\": [\"name\", \"description\"],\n \"name\": [\n {\n \"language_code\": \"en_us\",\n \"string\": \"API URL\"\n },\n {\n \"language_code\": \"es_es\",\n \"string\": \"URL de API\"\n },\n {\n \"language_code\": \"de_de\",\n \"string\": \"API-URL\"\n }\n ],\n \"description\": [\n {\n \"language_code\": \"en_us\",\n \"string\": \"Enter the API endpoint URL\"\n },\n {\n \"language_code\": \"es_es\",\n \"string\": \"Ingrese la URL del endpoint de API\"\n },\n {\n \"language_code\": \"de_de\",\n \"string\": \"Geben Sie die API-Endpunkt-URL ein\"\n }\n ]\n}\n</code></pre> <ul> <li><code>en_us</code> - English (required)</li> </ul>"},{"location":"PLUGINS_DEV_SETTINGS/#examples","title":"Examples","text":""},{"location":"PLUGINS_DEV_SETTINGS/#example-1-website-monitor-plugin","title":"Example 1: Website Monitor Plugin","text":"<pre><code>{\n \"settings\": [\n {\n \"function\": \"RUN\",\n \"type\": {\"dataType\": \"string\", \"elements\": [{\"elementType\": \"select\", \"elementOptions\": [], \"transformers\": []}]},\n \"default_value\": \"disabled\",\n \"options\": [\"disabled\", \"once\", \"schedule\"],\n \"localized\": [\"name\", \"description\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"When to run\"}],\n \"description\": [{\"language_code\": \"en_us\", \"string\": \"Enable website monitoring\"}]\n },\n {\n \"function\": \"RUN_SCHD\",\n \"type\": {\"dataType\": \"string\", \"elements\": [{\"elementType\": \"input\", \"elementOptions\": [], \"transformers\": []}]},\n \"default_value\": \"*/5 * * * *\",\n \"localized\": [\"name\", \"description\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Schedule\"}],\n \"description\": [{\"language_code\": \"en_us\", \"string\": \"Cron format (default: every 5 minutes)\"}]\n },\n {\n \"function\": \"CMD\",\n \"type\": {\"dataType\": \"string\", \"elements\": [{\"elementType\": \"input\", \"elementOptions\": [], \"transformers\": []}]},\n \"default_value\": \"python3 /app/front/plugins/website_monitor/script.py urls={urls}\",\n \"localized\": [\"name\", \"description\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Command\"}],\n \"description\": [{\"language_code\": \"en_us\", \"string\": \"Command to execute\"}]\n },\n {\n \"function\": \"RUN_TIMEOUT\",\n \"type\": {\"dataType\": \"string\", \"elements\": [{\"elementType\": \"input\", \"elementOptions\": [], \"transformers\": []}]},\n \"default_value\": \"60\",\n \"localized\": [\"name\", \"description\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Timeout\"}],\n \"description\": [{\"language_code\": \"en_us\", \"string\": \"Maximum execution time in seconds\"}]\n },\n {\n \"function\": \"URLS\",\n \"type\": {\"dataType\": \"array\", \"elements\": [{\"elementType\": \"input\", \"elementOptions\": [], \"transformers\": []}]},\n \"default_value\": [\"https://example.com\"],\n \"maxLength\": 200,\n \"localized\": [\"name\", \"description\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"URLs to monitor\"}],\n \"description\": [{\"language_code\": \"en_us\", \"string\": \"One URL per line\"}]\n },\n {\n \"function\": \"WATCH\",\n \"type\": {\"dataType\": \"array\", \"elements\": [{\"elementType\": \"select\", \"elementOptions\": [{\"isMultiSelect\": true}], \"transformers\": []}]},\n \"default_value\": [\"Status_Code\"],\n \"options\": [\"Status_Code\", \"Response_Time\", \"Certificate_Expiry\"],\n \"localized\": [\"name\", \"description\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Watch columns\"}],\n \"description\": [{\"language_code\": \"en_us\", \"string\": \"Which changes trigger notifications\"}]\n }\n ]\n}\n</code></pre>"},{"location":"PLUGINS_DEV_SETTINGS/#example-2-pihole-integration-plugin","title":"Example 2: PiHole Integration Plugin","text":"<pre><code>{\n \"settings\": [\n {\n \"function\": \"RUN\",\n \"type\": {\"dataType\": \"string\", \"elements\": [{\"elementType\": \"select\", \"elementOptions\": [], \"transformers\": []}]},\n \"default_value\": \"disabled\",\n \"options\": [\"disabled\", \"schedule\"],\n \"localized\": [\"name\", \"description\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"When to run\"}],\n \"description\": [{\"language_code\": \"en_us\", \"string\": \"Enable PiHole integration\"}]\n },\n {\n \"function\": \"DB_PATH\",\n \"type\": {\"dataType\": \"string\", \"elements\": [{\"elementType\": \"input\", \"elementOptions\": [], \"transformers\": []}]},\n \"default_value\": \"/etc/pihole/pihole-FTL.db\",\n \"localized\": [\"name\", \"description\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Database path\"}],\n \"description\": [{\"language_code\": \"en_us\", \"string\": \"Path to pihole-FTL.db inside container\"}]\n },\n {\n \"function\": \"API_KEY\",\n \"type\": {\"dataType\": \"string\", \"elements\": [{\"elementType\": \"input\", \"elementOptions\": [{\"type\": \"password\"}], \"transformers\": [\"sha256\"]}]},\n \"default_value\": \"\",\n \"localized\": [\"name\", \"description\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"API Key\"}],\n \"description\": [{\"language_code\": \"en_us\", \"string\": \"PiHole API key (optional, stored securely)\"}]\n }\n ]\n}\n</code></pre>"},{"location":"PLUGINS_DEV_SETTINGS/#validation-testing","title":"Validation &amp; Testing","text":""},{"location":"PLUGINS_DEV_SETTINGS/#check-settings-are-recognized","title":"Check Settings Are Recognized","text":"<p>After saving your <code>config.json</code>:</p> <ol> <li>Restart the backend: Run task <code>[Dev Container] Start Backend (Python)</code></li> <li>Open Settings page in UI</li> <li>Navigate to Plugin Settings</li> <li>Look for your plugin's settings</li> </ol>"},{"location":"PLUGINS_DEV_SETTINGS/#read-setting-values-in-script","title":"Read Setting Values in Script","text":"<p>Test that values are accessible:</p> <pre><code>from helper import get_setting_value\n\n# Try to read a setting\nvalue = get_setting_value('MYPLN_API_URL')\nmylog('none', f\"Setting value: {value}\")\n\n# Should print the user-configured value or default\n</code></pre>"},{"location":"PLUGINS_DEV_SETTINGS/#debug-settings","title":"Debug Settings","text":"<p>Check backend logs:</p> <pre><code>tail -f /tmp/log/stdout.log | grep -i \"setting\\|MYPLN\"\n</code></pre>"},{"location":"PLUGINS_DEV_SETTINGS/#see-also","title":"See Also","text":"<ul> <li>Settings System Documentation - Full settings flow and lifecycle</li> <li>Quick Start Guide - Get a working plugin quickly</li> <li>Plugin Data Contract - Output data format</li> <li>UI Components - Display plugin results</li> </ul>"},{"location":"PLUGINS_DEV_UI_COMPONENTS/","title":"Plugin UI Components","text":"<p>Configure how your plugin's data is displayed in the NetAlertX web interface.</p>"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#overview","title":"Overview","text":"<p>Plugin results are displayed in the UI via the Plugins page and Device details tabs. You control the appearance and functionality of these displays by defining <code>database_column_definitions</code> in your plugin's <code>config.json</code>.</p> <p>Each column definition specifies: - Which data field to display - How to render it (label, link, color-coded badge, etc.) - What CSS classes to apply - What transformations to apply (regex, string replacement, etc.)</p>"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#column-definition-structure","title":"Column Definition Structure","text":"<pre><code>{\n \"column\": \"Object_PrimaryID\",\n \"mapped_to_column\": \"devMac\",\n \"mapped_to_column_data\": null,\n \"css_classes\": \"col-sm-2\",\n \"show\": true,\n \"type\": \"device_mac\",\n \"default_value\": \"\",\n \"options\": [],\n \"options_params\": [],\n \"localized\": [\"name\"],\n \"name\": [\n {\n \"language_code\": \"en_us\",\n \"string\": \"MAC Address\"\n }\n ]\n}\n</code></pre>"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#properties","title":"Properties","text":"Property Type Required Description <code>column</code> string YES Source column name from data contract (e.g., <code>Object_PrimaryID</code>, <code>Watched_Value1</code>) <code>mapped_to_column</code> string no Target database column if mapping to a table like <code>CurrentScan</code> <code>mapped_to_column_data</code> object no Static value to map instead of using column data <code>css_classes</code> string no Bootstrap CSS classes for width/spacing (e.g., <code>\"col-sm-2\"</code>, <code>\"col-sm-6\"</code>) <code>show</code> boolean YES Whether to display in UI (must be <code>true</code> to appear) <code>type</code> string YES How to render the value (see Render Types) <code>default_value</code> varies YES Default if column is empty <code>options</code> array no Options for <code>select</code>/<code>threshold</code>/<code>replace</code>/<code>regex</code> types <code>options_params</code> array no Dynamic options from SQL or settings <code>localized</code> array YES Which properties need translations (e.g., <code>[\"name\", \"description\"]</code>) <code>name</code> array YES Display name in UI (localized strings) <code>description</code> array no Help text in UI (localized strings) <code>maxLength</code> number no Character limit for input fields"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#render-types","title":"Render Types","text":""},{"location":"PLUGINS_DEV_UI_COMPONENTS/#display-only-types","title":"Display-Only Types","text":"<p>These render as read-only display elements:</p>"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#label","title":"<code>label</code>","text":"<p>Plain text display (read-only).</p> <pre><code>{\n \"column\": \"Watched_Value1\",\n \"show\": true,\n \"type\": \"label\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Status\"}]\n}\n</code></pre> <p>Output: <code>online</code></p>"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#device_mac","title":"<code>device_mac</code>","text":"<p>Renders as a clickable link to the device with the given MAC address.</p> <pre><code>{\n \"column\": \"ForeignKey\",\n \"show\": true,\n \"type\": \"device_mac\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Device\"}]\n}\n</code></pre> <p>Input: <code>aa:bb:cc:dd:ee:ff</code> Output: Clickable link to device details page</p>"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#device_ip","title":"<code>device_ip</code>","text":"<p>Resolves an IP address to a MAC address and creates a device link.</p> <pre><code>{\n \"column\": \"Object_SecondaryID\",\n \"show\": true,\n \"type\": \"device_ip\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Host\"}]\n}\n</code></pre> <p>Input: <code>192.168.1.100</code> Output: Link to device with that IP (if known)</p>"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#device_name_mac","title":"<code>device_name_mac</code>","text":"<p>Creates a device link with the target device's name as the link label.</p> <pre><code>{\n \"column\": \"Object_PrimaryID\",\n \"show\": true,\n \"type\": \"device_name_mac\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Device Name\"}]\n}\n</code></pre> <p>Input: <code>aa:bb:cc:dd:ee:ff</code> Output: Device name (clickable link to device)</p>"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#url","title":"<code>url</code>","text":"<p>Renders as a clickable HTTP/HTTPS link.</p> <pre><code>{\n \"column\": \"Watched_Value1\",\n \"show\": true,\n \"type\": \"url\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Endpoint\"}]\n}\n</code></pre> <p>Input: <code>https://example.com/api</code> Output: Clickable link</p>"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#url_http_https","title":"<code>url_http_https</code>","text":"<p>Creates two links (HTTP and HTTPS) as lock icons for the given IP/hostname.</p> <pre><code>{\n \"column\": \"Object_SecondaryID\",\n \"show\": true,\n \"type\": \"url_http_https\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Web Links\"}]\n}\n</code></pre> <p>Input: <code>192.168.1.50</code> Output: \ud83d\udd13 HTTP link | \ud83d\udd12 HTTPS link</p>"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#textarea_readonly","title":"<code>textarea_readonly</code>","text":"<p>Multi-line read-only display with newlines preserved.</p> <pre><code>{\n \"column\": \"Extra\",\n \"show\": true,\n \"type\": \"textarea_readonly\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Details\"}]\n}\n</code></pre>"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#interactive-types","title":"Interactive Types","text":""},{"location":"PLUGINS_DEV_UI_COMPONENTS/#textbox_save","title":"<code>textbox_save</code>","text":"<p>User-editable text box that persists changes to the database (typically <code>UserData</code> column).</p> <pre><code>{\n \"column\": \"UserData\",\n \"show\": true,\n \"type\": \"textbox_save\",\n \"default_value\": \"\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Notes\"}]\n}\n</code></pre>"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#styledtransformed-types","title":"Styled/Transformed Types","text":""},{"location":"PLUGINS_DEV_UI_COMPONENTS/#label-with-threshold","title":"<code>label</code> with <code>threshold</code>","text":"<p>Color-codes values based on ranges. Useful for status codes, latency, capacity percentages.</p> <pre><code>{\n \"column\": \"Watched_Value1\",\n \"show\": true,\n \"type\": \"threshold\",\n \"options\": [\n {\n \"maximum\": 199,\n \"hexColor\": \"#792D86\" // Purple for &lt;199\n },\n {\n \"maximum\": 299,\n \"hexColor\": \"#5B862D\" // Green for 200-299\n },\n {\n \"maximum\": 399,\n \"hexColor\": \"#7D862D\" // Orange for 300-399\n },\n {\n \"maximum\": 499,\n \"hexColor\": \"#BF6440\" // Red-orange for 400-499\n },\n {\n \"maximum\": 999,\n \"hexColor\": \"#D33115\" // Dark red for 500+\n }\n ],\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"HTTP Status\"}]\n}\n</code></pre> <p>How it works:</p> <ul> <li>Value <code>150</code> \u2192 Purple (\u2264199)</li> <li>Value <code>250</code> \u2192 Green (\u2264299)</li> <li>Value <code>350</code> \u2192 Orange (\u2264399)</li> <li>Value <code>450</code> \u2192 Red-orange (\u2264499)</li> <li>Value <code>550</code> \u2192 Dark red (&gt;500)</li> </ul>"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#replace","title":"<code>replace</code>","text":"<p>Replaces specific values with display strings or HTML.</p> <pre><code>{\n \"column\": \"Watched_Value2\",\n \"show\": true,\n \"type\": \"replace\",\n \"options\": [\n {\n \"equals\": \"online\",\n \"replacement\": \"&lt;i class='fa-solid fa-circle' style='color: green;'&gt;&lt;/i&gt; Online\"\n },\n {\n \"equals\": \"offline\",\n \"replacement\": \"&lt;i class='fa-solid fa-circle' style='color: red;'&gt;&lt;/i&gt; Offline\"\n },\n {\n \"equals\": \"idle\",\n \"replacement\": \"&lt;i class='fa-solid fa-circle' style='color: yellow;'&gt;&lt;/i&gt; Idle\"\n }\n ],\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Status\"}]\n}\n</code></pre> <p>Output Examples: - <code>\"online\"</code> \u2192 \ud83d\udfe2 Online - <code>\"offline\"</code> \u2192 \ud83d\udd34 Offline - <code>\"idle\"</code> \u2192 \ud83d\udfe1 Idle</p>"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#regex","title":"<code>regex</code>","text":"<p>Applies a regular expression to extract/transform values.</p> <pre><code>{\n \"column\": \"Watched_Value1\",\n \"show\": true,\n \"type\": \"regex\",\n \"options\": [\n {\n \"type\": \"regex\",\n \"param\": \"([0-9]{1,3}\\\\.[0-9]{1,3}\\\\.[0-9]{1,3}\\\\.[0-9]{1,3})\"\n }\n ],\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"IP Address\"}]\n}\n</code></pre> <ul> <li>Input: <code>Host: 192.168.1.100 Port: 8080</code></li> <li>Output: <code>192.168.1.100</code></li> </ul>"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#eval","title":"<code>eval</code>","text":"<p>Evaluates JavaScript code with access to the column value (use <code>${value}</code> or <code>{value}</code>).</p> <pre><code>{\n \"column\": \"Watched_Value1\",\n \"show\": true,\n \"type\": \"eval\",\n \"default_value\": \"\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Formatted Value\"}]\n}\n</code></pre> <p>Example with custom formatting: <pre><code>{\n \"column\": \"Watched_Value1\",\n \"show\": true,\n \"type\": \"eval\",\n \"options\": [\n {\n \"type\": \"eval\",\n \"param\": \"`&lt;b&gt;${value}&lt;/b&gt; units`\"\n }\n ],\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Value with Units\"}]\n}\n</code></pre></p> <ul> <li>Input: <code>42</code></li> <li>Output: 42 units</li> </ul>"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#chaining-types","title":"Chaining Types","text":"<p>You can chain multiple transformations with dot notation:</p> <pre><code>{\n \"column\": \"Watched_Value3\",\n \"show\": true,\n \"type\": \"regex.url_http_https\",\n \"options\": [\n {\n \"type\": \"regex\",\n \"param\": \"([\\\\d.:]+)\" // Extract IP/host\n }\n ],\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"HTTP/S Links\"}]\n}\n</code></pre> <p>Flow:</p> <ol> <li>Apply regex to extract <code>192.168.1.50</code> from input</li> <li>Create HTTP/HTTPS links for that host</li> </ol>"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#dynamic-options","title":"Dynamic Options","text":""},{"location":"PLUGINS_DEV_UI_COMPONENTS/#sql-driven-select","title":"SQL-Driven Select","text":"<p>Use SQL query results to populate dropdown options:</p> <pre><code>{\n \"column\": \"Watched_Value2\",\n \"show\": true,\n \"type\": \"select\",\n \"options\": [\"{value}\"],\n \"options_params\": [\n {\n \"name\": \"value\",\n \"type\": \"sql\",\n \"value\": \"SELECT devType as id, devType as name FROM Devices UNION SELECT 'Unknown' as id, 'Unknown' as name ORDER BY id\"\n }\n ],\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Device Type\"}]\n}\n</code></pre> <p>The SQL query must return exactly 2 columns:</p> <ul> <li>First column (id): Option value</li> <li>Second column (name): Display label</li> </ul>"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#setting-driven-select","title":"Setting-Driven Select","text":"<p>Use plugin settings to populate options:</p> <pre><code>{\n \"column\": \"Watched_Value1\",\n \"show\": true,\n \"type\": \"select\",\n \"options\": [\"{value}\"],\n \"options_params\": [\n {\n \"name\": \"value\",\n \"type\": \"setting\",\n \"value\": \"MYPLN_AVAILABLE_STATUSES\"\n }\n ],\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Status\"}]\n}\n</code></pre>"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#mapping-to-database-tables","title":"Mapping to Database Tables","text":""},{"location":"PLUGINS_DEV_UI_COMPONENTS/#mapping-to-currentscan","title":"Mapping to <code>CurrentScan</code>","text":"<p>To import plugin data into the device scan pipeline (for notifications, heuristics, etc.):</p> <ol> <li>Add <code>\"mapped_to_table\": \"CurrentScan\"</code> at the root level of <code>config.json</code></li> <li>Add <code>\"mapped_to_column\"</code> property to each column definition</li> </ol> <pre><code>{\n \"code_name\": \"my_device_scanner\",\n \"unique_prefix\": \"MYSCAN\",\n \"mapped_to_table\": \"CurrentScan\",\n \"database_column_definitions\": [\n {\n \"column\": \"Object_PrimaryID\",\n \"mapped_to_column\": \"scanMac\",\n \"show\": true,\n \"type\": \"device_mac\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"MAC Address\"}]\n },\n {\n \"column\": \"Object_SecondaryID\",\n \"mapped_to_column\": \"scanLastIP\",\n \"show\": true,\n \"type\": \"device_ip\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"IP Address\"}]\n },\n {\n \"column\": \"NameDoesntMatter\",\n \"mapped_to_column\": \"scanSourcePlugin\",\n \"mapped_to_column_data\": {\n \"value\": \"MYSCAN\"\n },\n \"show\": true,\n \"type\": \"label\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Scan Method\"}]\n }\n ]\n}\n</code></pre>"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#using-static-values","title":"Using Static Values","text":"<p>Use <code>mapped_to_column_data</code> to map a static value instead of reading from a column:</p> <pre><code>{\n \"column\": \"NameDoesntMatter\",\n \"mapped_to_column\": \"scanSourcePlugin\",\n \"mapped_to_column_data\": {\n \"value\": \"MYSCAN\"\n },\n \"show\": true,\n \"type\": \"label\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Discovery Method\"}]\n}\n</code></pre> <p>This always sets <code>scanSourcePlugin</code> to <code>\"MYSCAN\"</code> regardless of column data.</p>"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#filters","title":"Filters","text":"<p>Control which rows are displayed based on filter conditions. Filters are applied on the client-side in JavaScript.</p> <pre><code>{\n \"data_filters\": [\n {\n \"compare_column\": \"Object_PrimaryID\",\n \"compare_operator\": \"==\",\n \"compare_field_id\": \"txtMacFilter\",\n \"compare_js_template\": \"'{value}'.toString()\",\n \"compare_use_quotes\": true\n }\n ]\n}\n</code></pre> Property Description <code>compare_column</code> The column from plugin results to compare (left side) <code>compare_operator</code> JavaScript operator: <code>==</code>, <code>!=</code>, <code>&lt;</code>, <code>&gt;</code>, <code>&lt;=</code>, <code>&gt;=</code>, <code>includes</code>, <code>startsWith</code> <code>compare_field_id</code> HTML input field ID containing the filter value (right side) <code>compare_js_template</code> JavaScript template to transform values. Use <code>{value}</code> placeholder <code>compare_use_quotes</code> If <code>true</code>, wrap result in quotes for string comparison <p>Example: Filter by MAC address</p> <pre><code>{\n \"data_filters\": [\n {\n \"compare_column\": \"ForeignKey\",\n \"compare_operator\": \"==\",\n \"compare_field_id\": \"txtMacFilter\",\n \"compare_js_template\": \"'{value}'.toString()\",\n \"compare_use_quotes\": true\n }\n ]\n}\n</code></pre> <p>When viewing a device detail page, the <code>txtMacFilter</code> field is populated with that device's MAC, and only rows where <code>ForeignKey == MAC</code> are shown.</p>"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#example-complete-column-definitions","title":"Example: Complete Column Definitions","text":"<pre><code>{\n \"database_column_definitions\": [\n {\n \"column\": \"Object_PrimaryID\",\n \"mapped_to_column\": \"scanMac\",\n \"css_classes\": \"col-sm-2\",\n \"show\": true,\n \"type\": \"device_mac\",\n \"default_value\": \"\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"MAC Address\"}]\n },\n {\n \"column\": \"Object_SecondaryID\",\n \"mapped_to_column\": \"scanLastIP\",\n \"css_classes\": \"col-sm-2\",\n \"show\": true,\n \"type\": \"device_ip\",\n \"default_value\": \"unknown\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"IP Address\"}]\n },\n {\n \"column\": \"DateTime\",\n \"css_classes\": \"col-sm-2\",\n \"show\": true,\n \"type\": \"label\",\n \"default_value\": \"\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Last Seen\"}]\n },\n {\n \"column\": \"Watched_Value1\",\n \"css_classes\": \"col-sm-2\",\n \"show\": true,\n \"type\": \"threshold\",\n \"options\": [\n {\"maximum\": 199, \"hexColor\": \"#792D86\"},\n {\"maximum\": 299, \"hexColor\": \"#5B862D\"},\n {\"maximum\": 399, \"hexColor\": \"#7D862D\"},\n {\"maximum\": 499, \"hexColor\": \"#BF6440\"},\n {\"maximum\": 999, \"hexColor\": \"#D33115\"}\n ],\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"HTTP Status\"}]\n },\n {\n \"column\": \"Watched_Value2\",\n \"css_classes\": \"col-sm-1\",\n \"show\": true,\n \"type\": \"label\",\n \"default_value\": \"\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Response Time\"}]\n },\n {\n \"column\": \"Extra\",\n \"css_classes\": \"col-sm-3\",\n \"show\": true,\n \"type\": \"textarea_readonly\",\n \"default_value\": \"\",\n \"localized\": [\"name\"],\n \"name\": [{\"language_code\": \"en_us\", \"string\": \"Additional Info\"}]\n }\n ]\n}\n</code></pre>"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#css-classes","title":"CSS Classes","text":"<p>Use Bootstrap grid classes to control column widths in tables:</p> Class Width Usage <code>col-sm-1</code> ~8% Very narrow (icons, status) <code>col-sm-2</code> ~16% Narrow (MACs, IPs) <code>col-sm-3</code> ~25% Medium (names, URLs) <code>col-sm-4</code> ~33% Medium-wide (descriptions) <code>col-sm-6</code> ~50% Wide (large content)"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#validation-checklist","title":"Validation Checklist","text":"<ul> <li> All columns have <code>\"show\": true</code> or <code>false</code></li> <li> Display columns with <code>\"type\"</code> specified from supported types</li> <li> Localized strings include at least <code>en_us</code></li> <li> <code>mapped_to_column</code> matches target table schema (if using mapping)</li> <li> Options/thresholds have correct structure</li> <li> CSS classes are valid Bootstrap grid classes</li> <li> Chaining types (e.g., <code>regex.url_http_https</code>) are supported combinations</li> </ul>"},{"location":"PLUGINS_DEV_UI_COMPONENTS/#see-also","title":"See Also","text":"<ul> <li>Plugin Data Contract - What data fields are available</li> <li>Plugin Settings System - Configure user input</li> <li>Database Mapping - Map data to core tables</li> <li>Debugging Plugins - Troubleshoot display issues</li> </ul>"},{"location":"PUID_PGID_SECURITY/","title":"PUID/PGID Security \u2014 Why the entrypoint requires numeric IDs","text":""},{"location":"PUID_PGID_SECURITY/#purpose","title":"Purpose","text":"<p>This short document explains the security rationale behind the root-priming entrypoint's validation of runtime user IDs (<code>PUID</code>) and group IDs (<code>PGID</code>). The validation is intentionally strict and is a safety measure to prevent environment-variable-based command injection when running as root during the initial priming stage.</p>"},{"location":"PUID_PGID_SECURITY/#key-points","title":"Key points","text":"<ul> <li>The entrypoint accepts only values that are strictly numeric (digits only). Non-numeric values are treated as malformed and are a fatal error.</li> <li>The fatal check exists to prevent injection or accidental shell interpretation of environment values while the container runs as root (e.g., <code>PUID=\"20211 &amp;&amp; rm -rf /\"</code>).</li> <li>There is no artificial upper bound enforced by the validation \u2014 any numeric UID/GID is valid (for example, <code>100000</code> is acceptable).</li> </ul>"},{"location":"PUID_PGID_SECURITY/#behavior-on-malformed-input","title":"Behavior on malformed input","text":"<ul> <li>If <code>PUID</code> or <code>PGID</code> cannot be parsed as numeric (digits-only), the entrypoint prints an explicit security message to stderr and exits with a non-zero status.</li> <li>This is a deliberate, conservative safety measure \u2014 we prefer failing fast on potentially dangerous input rather than continuing with root-privileged operations.</li> </ul>"},{"location":"PUID_PGID_SECURITY/#operator-guidance","title":"Operator guidance","text":"<ul> <li>Always supply numeric values for <code>PUID</code> and <code>PGID</code> in your environment (via <code>docker-compose.yml</code>, <code>docker run -e</code>, or equivalent). Example: <code>PUID=20211</code>.</li> <li>If you need to run with a high-numbered UID/GID (e.g., <code>100000</code>), that is fine \u2014 the entrypoint allows it as long as the value is numeric.</li> <li>Don\u2019t pass shell meta-characters, spaces, or compound commands in <code>PUID</code> or <code>PGID</code> \u2014 those will be rejected as malformed and cause the container to exit.</li> </ul>"},{"location":"PUID_PGID_SECURITY/#related-docs","title":"Related docs","text":"<ul> <li>See <code>docs/docker-troubleshooting/file-permissions.md</code> for general permission troubleshooting and guidance about setting <code>PUID</code>/<code>PGID</code>.</li> </ul> <p>Document created to clarify the security behavior of the root-priming entrypoint (PUID/PGID validation).</p>"},{"location":"RANDOM_MAC/","title":"Privacy &amp; Random MAC's","text":"<p>Some operating systems incorporate randomize MAC addresses to improve privacy.</p> <p>This functionality allows you to hide the real MAC of the device and assign a random MAC when we connect to WIFI networks.</p> <p>This behavior is especially useful when connecting to WIFI's that we do not know, but it is totally useless when connecting to our own WIFI's or known networks.</p> <p>I recommend disabling this on-device functionality when connecting our devices to our own WIFI's, this way, NetAlertX will be able to identify the device, and it will not identify it as a new device every so often (every time iOS or Android randomizes the MAC).</p> <p>Random MACs are recognized by the characters \"2\", \"6\", \"A\", or \"E\" as the 2nd character in the Mac address. You can disable specific prefixes to be detected as random MAC addresses by specifying the <code>UI_NOT_RANDOM_MAC</code> setting.</p>"},{"location":"RANDOM_MAC/#windows","title":"Windows","text":"<ul> <li>How to Disable MAC Randomization on Windows</li> </ul>"},{"location":"RANDOM_MAC/#ios","title":"IOS","text":"<ul> <li>Use private Wi-Fi addresses in iOS 14</li> </ul>"},{"location":"RANDOM_MAC/#android","title":"Android","text":"<ul> <li>How to Disable MAC Randomization in Android 10</li> <li>How do I disable random Wi-Fi MAC address on Android 10</li> </ul>"},{"location":"REMOTE_NETWORKS/","title":"Scanning Remote or Inaccessible Networks","text":"<p>By design, local network scanners such as <code>arp-scan</code> use ARP (Address Resolution Protocol) to map IP addresses to MAC addresses on the local network. Since ARP operates at Layer 2 (Data Link Layer), it typically works only within a single broadcast domain, usually limited to a single router or network segment.</p> <p>Note</p> <p>Ping and <code>ARPSCAN</code> use different protocols so even if you can ping devices it doesn't mean <code>ARPSCAN</code> can detect them.</p> <p>To scan multiple locally accessible network segments, add them as subnets according to the subnets documentation. If <code>ARPSCAN</code> is not suitable for your setup, read on.</p>"},{"location":"REMOTE_NETWORKS/#complex-use-cases","title":"Complex Use Cases","text":"<p>The following network setups might make some devices undetectable with <code>ARPSCAN</code>. Check the specific setup to understand the cause and find potential workarounds to report on these devices.</p>"},{"location":"REMOTE_NETWORKS/#wi-fi-extenders","title":"Wi-Fi Extenders","text":"<p>Wi-Fi extenders often block or proxy Layer-2 broadcast traffic, which can prevent network scanning tools like <code>arp-scan</code> from detecting devices behind the extender. This can happen even when the extender uses the same SSID and the same IP subnet as the main network.</p> <p>Please note that being able to <code>ping</code> a device does not mean it is discoverable via <code>arp-scan</code>.</p> <ul> <li><code>arp-scan</code> relies on Layer 2 (ARP broadcast)</li> <li>ICMP (<code>ping</code>) operates at Layer 3 (routed traffic)</li> </ul> <p>That\u2019s why devices behind extenders may respond to ping but remain undiscoverable via <code>arp-scan</code>.</p> <p>Possible workaround: If the extender uses a separate subnet, scan that subnet directly. Otherwise, use DHCP-based discovery plugins or router integration instead of ARP. See the Other Workarounds section below for more details.</p>"},{"location":"REMOTE_NETWORKS/#vpns","title":"VPNs","text":"<p>ARP operates at Layer 2 (Data Link Layer) and works only within a local area network (LAN). VPNs, which operate at Layer 3 (Network Layer), route traffic between networks, preventing ARP requests from discovering devices outside the local network.</p> <p>VPNs use virtual interfaces (e.g., <code>tun0</code>, <code>tap0</code>) to encapsulate traffic, bypassing ARP-based discovery. Additionally, many VPNs use NAT, which masks individual devices behind a shared IP address.</p> <p>Possible workaround: Configure the VPN to bridge networks instead of routing to enable ARP, though this depends on the VPN setup and security requirements.</p>"},{"location":"REMOTE_NETWORKS/#other-workarounds","title":"Other Workarounds","text":"<p>The following workarounds should work for most complex network setups.</p>"},{"location":"REMOTE_NETWORKS/#supplementing-plugins","title":"Supplementing Plugins","text":"<p>You can use supplementary plugins that employ alternate methods. Protocols used by the <code>SNMPDSC</code> or <code>DHCPLSS</code> plugins are widely supported on different routers and can be effective as workarounds. Check the plugins list to find a plugin that works with your router and network setup.</p>"},{"location":"REMOTE_NETWORKS/#multiple-netalertx-instances","title":"Multiple NetAlertX Instances","text":"<p>If you have servers in different networks, you can set up separate NetAlertX instances on those subnets and synchronize the results into one instance using the <code>SYNC</code> plugin.</p>"},{"location":"REMOTE_NETWORKS/#manual-entry","title":"Manual Entry","text":"<p>If you don't need to discover new devices and only need to report on their status (<code>online</code>, <code>offline</code>, <code>down</code>), you can manually enter devices and check their status using the <code>ICMP</code> plugin, which uses the <code>ping</code> command internally.</p> <p>For more information on how to add devices manually (or dummy devices), refer to the Device Management documentation.</p> <p>To create truly dummy devices, you can use a loopback IP address (e.g., <code>0.0.0.0</code> or <code>127.0.0.1</code>) or the <code>Force Status</code> field so they appear online.</p>"},{"location":"REMOTE_NETWORKS/#nmap-and-fake-mac-addresses","title":"NMAP and Fake MAC Addresses","text":"<p>Scanning remote networks with NMAP is possible (via the <code>NMAPDEV</code> plugin), but since it cannot retrieve the MAC address, you need to enable the <code>NMAPDEV_FAKE_MAC</code> setting. This will generate a fake MAC address based on the IP address, allowing you to track devices. However, this can lead to inconsistencies, especially if the IP address changes or a previously logged device is rediscovered. If this setting is disabled, only the IP address will be discovered, and devices with missing MAC addresses will be skipped.</p> <p>Check the NMAPDEV plugin for details</p>"},{"location":"REVERSE_DNS/","title":"Reverse DNS","text":""},{"location":"REVERSE_DNS/#setting-up-better-name-discovery-with-reverse-dns","title":"Setting up better name discovery with Reverse DNS","text":"<p>If you are running a DNS server, such as AdGuard, set up Private reverse DNS servers for a better name resolution on your network. Enabling this setting will enable NetAlertX to execute dig and nslookup commands to automatically resolve device names based on their IP addresses.</p> <p>Tip</p> <p>Before proceeding, ensure that name resolution plugins are enabled. You can customize how names are cleaned using the <code>NEWDEV_NAME_CLEANUP_REGEX</code> setting. To auto-update Fully Qualified Domain Names (FQDN), enable the <code>REFRESH_FQDN</code> setting.</p> <p>Example 1: Reverse DNS <code>disabled</code></p> <pre><code>jokob@Synology-NAS:/$ nslookup 192.168.1.58\n** server can't find 58.1.168.192.in-addr.arpa: NXDOMAIN\n</code></pre> <p>Example 2: Reverse DNS <code>enabled</code></p> <pre><code>jokob@Synology-NAS:/$ nslookup 192.168.1.58\n45.1.168.192.in-addr.arpa name = jokob-NUC.localdomain.\n</code></pre>"},{"location":"REVERSE_DNS/#enabling-reverse-dns-in-adguard","title":"Enabling reverse DNS in AdGuard","text":"<ol> <li>Navigate to Settings -&gt; DNS Settings</li> <li>Locate Private reverse DNS servers</li> <li>Enter your router IP address, such as <code>192.168.1.1</code></li> <li>Make sure you have Use private reverse DNS resolvers ticked.</li> <li>Click Apply to save your settings.</li> </ol>"},{"location":"REVERSE_DNS/#specifying-the-dns-in-the-container","title":"Specifying the DNS in the container","text":"<p>You can specify the DNS server in the docker-compose to improve name resolution on your network.</p> <pre><code>services:\n netalertx:\n container_name: netalertx\n image: \"ghcr.io/netalertx/netalertx:latest\"\n...\n dns: # specifying the DNS servers used for the container\n - 10.8.0.1\n - 10.8.0.17\n</code></pre>"},{"location":"REVERSE_DNS/#using-a-custom-resolvconf-file","title":"Using a custom resolv.conf file","text":"<p>You can configure a custom /etc/resolv.conf file in docker-compose.yml and set the nameserver to your LAN DNS server (e.g.: Pi-Hole). See the relevant resolv.conf man entry for details.</p>"},{"location":"REVERSE_DNS/#docker-composeyml","title":"docker-compose.yml:","text":"<pre><code>services:\n netalertx:\n container_name: netalertx\n volumes:\n...\n - /local_data_dir/config/resolv.conf:/etc/resolv.conf # \u26a0 Mapping the /resolv.conf file for better name resolution\n...\n</code></pre>"},{"location":"REVERSE_DNS/#local_data_dirconfigresolvconf","title":"/local_data_dir/config/resolv.conf:","text":"<p>The most important below is the <code>nameserver</code> entry (you can add multiple):</p> <pre><code>nameserver 192.168.178.11\noptions edns0 trust-ad\nsearch example.com\n</code></pre>"},{"location":"REVERSE_PROXY/","title":"Reverse Proxy Configuration","text":"<p>A reverse proxy is a server that sits between users and your NetAlertX instance. It allows you to: - Access NetAlertX via a domain name (e.g., <code>https://netalertx.example.com</code>). - Add HTTPS/SSL encryption. - Enforce authentication (like SSO).</p> <pre><code>flowchart LR\n Browser --HTTPS--&gt; Proxy[Reverse Proxy] --HTTP--&gt; Container[NetAlertX Container]</code></pre>"},{"location":"REVERSE_PROXY/#netalertx-ports","title":"NetAlertX Ports","text":"<p>NetAlertX exposes two ports that serve different purposes. Your reverse proxy can target one or both, depending on your needs.</p> Port Service Purpose 20211 Nginx (Web UI) The main interface. 20212 Backend API Direct access to the API and GraphQL. Includes API docs you can view with a browser. <p>Warning</p> <p>Do not document or use <code>/server</code> as an external API endpoint. It is an internal route used by the Nginx frontend to communicate with the backend.</p>"},{"location":"REVERSE_PROXY/#connection-patterns","title":"Connection Patterns","text":""},{"location":"REVERSE_PROXY/#1-default-no-proxy","title":"1. Default (No Proxy)","text":"<p>For local testing or LAN access. The browser accesses the UI on port 20211. Code and API docs are accessible on 20212.</p> <pre><code>flowchart LR\n B[Browser]\n subgraph NAC[NetAlertX Container]\n N[Nginx listening on port 20211]\n A[Service on port 20212]\n N --&gt;|Proxy /server to localhost:20212| A\n end\n B --&gt;|port 20211| NAC\n B --&gt;|port 20212| NAC</code></pre>"},{"location":"REVERSE_PROXY/#2-direct-api-consumer-not-recommended","title":"2. Direct API Consumer (Not Recommended)","text":"<p>Connecting directly to the backend API port (20212).</p> <p>Caution</p> <p>This exposes the API directly to the network without additional protection. Avoid this on untrusted networks.</p> <pre><code>flowchart LR\n B[Browser] --&gt;|HTTPS| S[Any API Consumer app]\n subgraph NAC[NetAlertX Container]\n N[Nginx listening on port 20211]\n N --&gt;|Proxy /server to localhost:20212| A[Service on port 20212]\n end\n S --&gt;|Port 20212| NAC</code></pre>"},{"location":"REVERSE_PROXY/#3-recommended-reverse-proxy-to-web-ui","title":"3. Recommended: Reverse Proxy to Web UI","text":"<p>Using a reverse proxy (Nginx, Traefik, Caddy, etc.) to handle HTTPS and Auth in front of the main UI.</p> <pre><code>flowchart LR\n B[Browser] --&gt;|HTTPS| S[Any Auth/SSL proxy]\n subgraph NAC[NetAlertX Container]\n N[Nginx listening on port 20211]\n N --&gt;|Proxy /server to localhost:20212| A[Service on port 20212]\n end\n S --&gt;|port 20211| NAC</code></pre>"},{"location":"REVERSE_PROXY/#4-recommended-proxied-api-consumer","title":"4. Recommended: Proxied API Consumer","text":"<p>Using a proxy to secure API access with TLS or IP limiting.</p> <p>Why is this important? The backend API (<code>:20212</code>) is powerful\u2014more so than the Web UI, which is a safer, password-protectable interface. By using a reverse proxy to limit sources (e.g., allowing only your Home Assistant server's IP), you ensure that only trusted devices can talk to your backend.</p> <pre><code>flowchart LR\n B[Browser] --&gt;|HTTPS| S[Any API Consumer app]\n C[HTTPS/source-limiting Proxy]\n subgraph NAC[NetAlertX Container]\n N[Nginx listening on port 20211]\n N --&gt;|Proxy /server to localhost:20212| A[Service on port 20212]\n end\n S --&gt;|HTTPS| C\n C --&gt;|Port 20212| NAC</code></pre>"},{"location":"REVERSE_PROXY/#getting-started-nginx-proxy-manager","title":"Getting Started: Nginx Proxy Manager","text":"<p>For beginners, we recommend Nginx Proxy Manager. It provides a user-friendly interface to manage proxy hosts and free SSL certificates via Let's Encrypt.</p> <ol> <li>Install Nginx Proxy Manager alongside NetAlertX.</li> <li>Create a Proxy Host pointing to your NetAlertX IP and Port <code>20211</code> for the Web UI.</li> <li>(Optional) Create a second host for the API on Port <code>20212</code>.</li> </ol> <p></p>"},{"location":"REVERSE_PROXY/#configuration-settings","title":"Configuration Settings","text":"<p>When using a reverse proxy, you should verify two settings in Settings &gt; Core &gt; General:</p> <ol> <li>BACKEND_API_URL: This should be set to <code>/server</code>.</li> <li> <p>Reason: The frontend should communicate with the backend via the internal Nginx proxy rather than routing out to the internet and back.</p> </li> <li> <p>REPORT_DASHBOARD_URL: Set this to your external proxy URL (e.g., <code>https://netalertx.example.com</code>).</p> </li> <li>Reason: This URL is used to generate proper clickable links in emails and HTML reports.</li> </ol> <p></p>"},{"location":"REVERSE_PROXY/#other-reverse-proxies","title":"Other Reverse Proxies","text":"<p>NetAlertX uses standard HTTP. Any reverse proxy will work. Simply forward traffic to the appropriate port (<code>20211</code> or <code>20212</code>).</p> <p>For configuration details, consult the documentation for your preferred proxy:</p> <ul> <li>NGINX</li> <li>Apache (mod_proxy)</li> <li>Caddy</li> <li>Traefik</li> </ul>"},{"location":"REVERSE_PROXY/#authentication","title":"Authentication","text":"<p>If you wish to add Single Sign-On (SSO) or other authentication in front of NetAlertX, refer to the documentation for your identity provider:</p> <ul> <li>Authentik</li> <li>Authelia</li> </ul>"},{"location":"REVERSE_PROXY/#further-reading","title":"Further Reading","text":"<p>If you want to understand more about reverse proxies and networking concepts:</p> <ul> <li>What is a Reverse Proxy? (Cloudflare)</li> <li>Proxy vs Reverse Proxy (StrongDM)</li> <li>Nginx Reverse Proxy Glossary</li> </ul>"},{"location":"SECURITY/","title":"Security Considerations","text":""},{"location":"SECURITY/#responsibility-disclaimer","title":"\ud83e\udded Responsibility Disclaimer","text":"<p>NetAlertX provides powerful tools for network scanning, presence detection, and automation. However, it is up to you\u2014the deployer\u2014to ensure that your instance is properly secured.</p> <p>This includes (but is not limited to): - Controlling who has access to the UI and API - Following network and container security best practices - Running NetAlertX only on networks where you have legal authorization - Keeping your deployment up to date with the latest patches</p> <p>NetAlertX is not responsible for misuse, misconfiguration, or unsecure deployments. Always test and secure your setup before exposing it to the outside world.</p>"},{"location":"SECURITY/#securing-your-netalertx-instance","title":"\ud83d\udd10 Securing Your NetAlertX Instance","text":"<p>NetAlertX is a powerful network scanning and automation framework. With that power comes responsibility. It is your responsibility to secure your deployment, especially if you're running it outside a trusted local environment.</p>"},{"location":"SECURITY/#tldr-key-security-recommendations","title":"\u26a0\ufe0f TL;DR \u2013 Key Security Recommendations","text":"<ul> <li>\u2705 NEVER expose NetAlertX directly to the internet without protection</li> <li>\u2705 Use a VPN or Tailscale to access remotely</li> <li>\u2705 Enable password protection for the web UI</li> <li>\u2705 Harden your container environment (e.g., no unnecessary privileges)</li> <li>\u2705 Use firewalls and IP whitelisting</li> <li>\u2705 Keep the software updated</li> <li>\u2705 Limit the scope of plugins and API keys</li> </ul>"},{"location":"SECURITY/#access-control-with-vpn-or-tailscale","title":"\ud83d\udd17 Access Control with VPN (or Tailscale)","text":"<p>NetAlertX is designed to be run on private LANs, not the open internet.</p> <p>Recommended: Use a VPN to access NetAlertX from remote locations.</p>"},{"location":"SECURITY/#tailscale-easy-vpn-alternative","title":"\u2705 Tailscale (Easy VPN Alternative)","text":"<p>Tailscale sets up a private mesh network between your devices. It's fast to configure and ideal for NetAlertX. \ud83d\udc49 Get started with Tailscale</p>"},{"location":"SECURITY/#web-ui-password-protection","title":"\ud83d\udd11 Web UI Password Protection","text":"<p>By default, NetAlertX does not require login. Before exposing the UI in any way:</p> <ol> <li> <p>Enable password protection: <pre><code>SETPWD_enable_password=true\nSETPWD_password=your_secure_password\n</code></pre></p> </li> <li> <p>Passwords are stored as SHA256 hashes</p> </li> <li> <p>Default password (if not changed): 123456 \u2014 change it ASAP!</p> </li> </ol> <p>To disable authenticated login, set <code>SETPWD_enable_password=false</code> in <code>app.conf</code></p>"},{"location":"SECURITY/#additional-security-measures","title":"\ud83d\udd25 Additional Security Measures","text":"<ul> <li> <p>Firewall / Network Rules Restrict UI/API access to trusted IPs only.</p> </li> <li> <p>Limit Docker Capabilities Avoid <code>--privileged</code>. Use <code>--cap-add=NET_RAW</code> and others only if required by your scan method.</p> </li> <li> <p>Keep NetAlertX Updated Regular updates contain bug fixes and security patches.</p> </li> <li> <p>Plugin Permissions Disable unused plugins. Only install from trusted sources.</p> </li> <li> <p>Use Read-Only API Keys When integrating NetAlertX with other tools, scope keys tightly.</p> </li> </ul>"},{"location":"SECURITY/#docker-hardening-tips","title":"\ud83e\uddf1 Docker Hardening Tips","text":"<ul> <li>Use <code>read-only</code> mount options where possible (<code>:ro</code>)</li> <li>Avoid running as <code>root</code> unless absolutely necessary</li> <li>Consider using <code>docker scan</code> or other container image vulnerability scanners</li> <li>Run with <code>--network host</code> only on trusted networks and only if needed for ARP-based scans</li> </ul>"},{"location":"SECURITY/#responsible-disclosure","title":"\ud83d\udce3 Responsible Disclosure","text":"<p>If you discover a vulnerability or security concern, please report it privately to:</p> <p>\ud83d\udce7 jokob@duck.com</p> <p>We take security seriously and will work to patch confirmed issues promptly. Your help in responsible disclosure is appreciated!</p> <p>By following these recommendations, you can ensure your NetAlertX deployment is both powerful and secure.</p>"},{"location":"SECURITY_FEATURES/","title":"NetAlertX Security: A Layered Defense","text":"<p>Your network security monitor has the \"keys to the kingdom,\" making it a prime target for attackers. If it gets compromised, the game is over.</p> <p>NetAlertX is engineered from the ground up to prevent this. It's not just an app; it's a purpose-built security appliance. Its core design is built on a zero-trust philosophy, which is a modern way of saying we assume a breach will happen and plan for it. This isn't a single \"lock on the door\"; it's a \"defense-in-depth\" strategy, more like a medieval castle with a moat, high walls, and guards at every door.</p> <p>Here\u2019s a breakdown of the defensive layers you get, right out of the box using the default configuration.</p>"},{"location":"SECURITY_FEATURES/#feature-1-the-digital-concrete-filesystem","title":"Feature 1: The \"Digital Concrete\" Filesystem","text":"<p>Methodology: The core application and its system files are treated as immutable. Once built, the app's code is \"set in concrete,\" preventing attackers from modifying it or planting malware.</p> <ul> <li> <p>Immutable Filesystem: At runtime, the container's entire filesystem is set to <code>read_only: true</code>. The application code, system libraries, and all other files are literally frozen. This single control neutralizes a massive range of common attacks.</p> </li> <li> <p>\"Ownership-as-a-Lock\" Pattern: During the build, all system files are assigned to a special <code>readonly</code> user. This user has no login shell and no power to write to any files, even its own. It\u2019s a clever, defense-in-depth locking mechanism.</p> </li> <li> <p>Data Segregation: All user-specific data (like configurations and the device database) is stored completely outside the container in Docker volumes. The application is disposable; the data is persistent.</p> </li> </ul> <p>What's this mean to you: Even if an attacker gets in, they cannot modify the application code or plant malware. It's like the app is set in digital concrete.</p>"},{"location":"SECURITY_FEATURES/#feature-2-surgical-keycard-only-access","title":"Feature 2: Surgical, \"Keycard-Only\" Access","text":"<p>Methodology: The principle of least privilege is strictly enforced. Every process gets only the absolute minimum set of permissions needed for its specific job.</p> <ul> <li> <p>Non-Privileged Execution: The entire NetAlertX stack runs as a dedicated, low-power, non-root user (<code>netalertx</code>). No \"god mode\" privileges are available to the application.</p> </li> <li> <p>Kernel-Level Capability Revocation: The container is launched with <code>cap_drop: - ALL</code>, which tells the Linux kernel to revoke all \"root-like\" special powers.</p> </li> <li> <p>Binary-Specific Privileges (setcap): This is the \"keycard\" metaphor in action. After revoking all powers, the system uses <code>setcap</code> to grant specific, necessary permissions only to the binaries that absolutely require them (like <code>nmap</code> and <code>arp-scan</code>). This means that even if an attacker compromises the web server, they can't start scanning the network. The web server's \"keycard\" doesn't open the \"scanning\" door.</p> </li> </ul> <p>What's this mean to you: A security breach is firewalled. An attacker who gets into the web UI does not have the \"keycard\" to start scanning your network or take over the system. The breach is contained.</p>"},{"location":"SECURITY_FEATURES/#feature-3-attack-surface-amputation","title":"Feature 3: Attack Surface \"Amputation\"","text":"<p>Methodology: The potential attack surface is aggressively minimized by removing every non-essential tool an attacker would want to use.</p> <ul> <li> <p>Package Manager Removal: The <code>hardened</code> build stage explicitly deletes the Alpine package manager (<code>apk del apk-tools</code>). This makes it impossible for an attacker to simply <code>apk add</code> their malicious toolkit.</p> </li> <li> <p><code>sudo</code> Neutralization: All <code>sudo</code> configurations are removed, and the <code>/usr/bin/sudo</code> command is replaced with a non-functional shim. Any attempt to escalate privileges this way will fail.</p> </li> <li> <p>Build Toolchain Elimination: The <code>Dockerfile</code> uses a multi-stage build. The initial \"builder\" stage, which contains all the powerful compilers (<code>gcc</code>) and development tools, is completely discarded. The final production image is lean and contains no build tools.</p> </li> <li> <p>Minimal User &amp; Group Files: The <code>hardened</code> stage scrubs the system's <code>passwd</code> and <code>group</code> files, removing all default system users to minimize potential avenues for privilege escalation.</p> </li> </ul> <p>What's this mean to you: An attacker who breaks in finds themselves in an empty room with no tools. They have no <code>sudo</code> to get more power, no package manager to download weapons, and no compilers to build new ones.</p>"},{"location":"SECURITY_FEATURES/#feature-4-self-cleaning-writable-areas","title":"Feature 4: \"Self-Cleaning\" Writable Areas","text":"<p>Methodology: All writable locations are treated as untrusted, temporary, and non-executable by default.</p> <ul> <li> <p>In-Memory Volatile Storage: The <code>docker-compose.yml</code> configuration maps all temporary directories (e.g., <code>/tmp/log</code>, <code>/tmp/api</code>, <code>/tmp</code>) to in-memory <code>tmpfs</code> filesystems. They do not exist on the host's disk.</p> </li> <li> <p>Volatile Data: Because these locations exist only in RAM, their contents are instantly and irrevocably erased when the container is stopped. This provides a \"self-cleaning\" mechanism that purges any attacker-dropped files or payloads on every single restart.</p> </li> <li> <p>Secure Mount Flags: These in-memory mounts are configured with the <code>noexec</code> flag. This is a critical security control: it prohibits the execution of any binary or script from a location that is writable.</p> </li> </ul> <p>What's this mean to you: Any malicious file an attacker does manage to drop is written in invisible, non-permanent ink. The file is written to RAM, not disk, so it vaporizes the instant you restart the container. Even worse for them, the <code>noexec</code> flag means they can't even run the file in the first place.</p>"},{"location":"SECURITY_FEATURES/#feature-5-built-in-resource-guardrails","title":"Feature 5: Built-in Resource Guardrails","text":"<p>Methodology: The container is constrained by resource limits to function as a \"good citizen\" on the host system. This prevents a compromised or runaway process from consuming excessive resources, a common vector for Denial of Service (DoS) attacks.</p> <ul> <li> <p>Process Limiting: The <code>docker-compose.yml</code> defines a <code>pids_limit: 512</code>. This directly mitigates \"fork bomb\" attacks, where a process attempts to crash the host by recursively spawning thousands of new processes.</p> </li> <li> <p>Memory &amp; CPU Limits: The configuration file defines strict resource limits to prevent any single process from exhausting the host's available system resources.</p> </li> </ul> <p>What's this mean to you: NetAlertX is a \"good neighbor\" and can't be used to crash your host machine. Even if a process is compromised, it's in a digital straitjacket and cannot pull a \"denial of service\" attack by hogging all your CPU or memory.</p>"},{"location":"SECURITY_FEATURES/#feature-6-the-pre-flight-self-check","title":"Feature 6: The \"Pre-Flight\" Self-Check","text":"<p>Methodology: Before any services start, NetAlertX runs a comprehensive \"pre-flight\" check to ensure its own security and configuration are sound. It's like a built-in auditor who verifies its own defenses.</p> <ul> <li> <p>Active Self-Diagnosis: On every single boot, NetAlertX runs a series of startup pre-checks\u2014and it's fast. The entire self-check process typically completes in less than a second, letting you get to the web UI in about three seconds from startup.</p> </li> <li> <p>Validates Its Own Security: These checks actively inspect the other security features. For example, <code>check-0-permissions.sh</code> validates that all the \"Digital Concrete\" files are locked down and all the \"Self-Cleaning\" areas are writable, just as they should be. It also checks that the correct <code>netalertx</code> user is running the show, not <code>root</code>.</p> </li> <li> <p>Catches Misconfigurations: This system acts as a \"safety inspector\" that catches misconfigurations before they can become security holes. If you've made a mistake in your configuration (like a bad folder permission or incorrect network mode), NetAlertX will tell you in the logs why it can't start, rather than just failing silently.</p> </li> </ul> <p>What's this mean to you: The system is self-aware and checks its own work. You get instant feedback if a setting is wrong, and you get peace of mind on every single boot knowing all these security layers are active and verified, all in about one second.</p>"},{"location":"SECURITY_FEATURES/#conclusion-security-by-default","title":"Conclusion: Security by Default","text":"<p>No single security control is a silver bullet. The robust security posture of NetAlertX is achieved through defense in depth, layering these methodologies.</p> <p>An adversary must not only gain initial access but must also find a way to write a payload to a non-executable, in-memory location, without access to any standard system tools, <code>sudo</code>, or a package manager. And they must do this while operating as an unprivileged user in a resource-limited environment where the application code is immutable and actively checks its own integrity on every boot.</p>"},{"location":"SESSION_INFO/","title":"Sessions Section \u2013 Device View","text":"<p>The Sessions Section shows a device\u2019s connection history. All data is automatically detected and cannot be edited.</p> <p></p>"},{"location":"SESSION_INFO/#key-fields","title":"Key Fields","text":"Field Description Editable? First Connection The first time the device was detected on the network. \u274c Auto-detected Last Connection The most recent time the device was online. \u274c Auto-detected"},{"location":"SESSION_INFO/#how-session-information-works","title":"How Session Information Works","text":""},{"location":"SESSION_INFO/#1-detecting-new-devices","title":"1. Detecting New Devices","text":"<ul> <li>New devices are automatically detected when they first appear on the network.</li> <li>A New Device record is created, capturing the MAC, IP, vendor, and detection time.</li> </ul>"},{"location":"SESSION_INFO/#2-recording-connection-sessions","title":"2. Recording Connection Sessions","text":"<ul> <li>Every time a device connects, a session entry is created.</li> <li> <p>Captured details include:</p> </li> <li> <p>Connection type (wired or wireless)</p> </li> <li>Connection time</li> <li>Device details (MAC, IP, vendor)</li> </ul>"},{"location":"SESSION_INFO/#3-handling-missing-or-conflicting-data","title":"3. Handling Missing or Conflicting Data","text":"<ul> <li> <p>Triggers: Devices are flagged when session data is incomplete, inconsistent, or conflicting. Examples include:</p> </li> <li> <p>Missing first or last connection timestamps</p> </li> <li>Overlapping session records</li> <li> <p>Sessions showing a device as connected and disconnected at the same time</p> </li> <li> <p>System response:</p> </li> <li> <p>Automatically highlights affected devices in the Sessions Section.</p> </li> <li> <p>Attempts to infer missing information from available data, such as:</p> <ul> <li>Estimating first or last connection times from nearby session events</li> <li>Correcting overlapping session periods</li> <li>Reconciling conflicting connection statuses</li> </ul> </li> <li> <p>User impact:</p> </li> <li> <p>Users do not need to manually fix session data.</p> </li> <li>The system ensures the device\u2019s connection history remains as accurate as possible for monitoring and reporting.</li> </ul>"},{"location":"SESSION_INFO/#4-updating-sessions","title":"4. Updating Sessions","text":"<ul> <li>Reconnect: Updates session with the new connection timestamp.</li> <li>Disconnect: Records disconnection time and marks the device as offline.</li> </ul> <p>This session information feeds directly into Monitoring \u2192 Presence, providing a live view of which devices are currently online.</p> <p></p>"},{"location":"SETTINGS_SYSTEM/","title":"Settings","text":""},{"location":"SETTINGS_SYSTEM/#setting-system","title":"\u2699 Setting system","text":"<p>This is an explanation how settings are handled intended for anyone thinking about writing their own plugin or contributing to the project.</p> <p>If you are a user of the app, settings have a detailed description in the Settings section of the app. Open an issue if you'd like to clarify any of the settings.</p>"},{"location":"SETTINGS_SYSTEM/#data-storage","title":"\ud83d\udee2 Data storage","text":"<p>The source of truth for user-defined values is the <code>app.conf</code> file. Editing the file makes the App overwrite values in the <code>Settings</code> database table and in the <code>table_settings.json</code> file.</p>"},{"location":"SETTINGS_SYSTEM/#settings-database-table","title":"Settings database table","text":"<p>The <code>Settings</code> database table contains settings for App run purposes. The table is recreated every time the App restarts. The settings are loaded from the source-of-truth, that is the <code>app.conf</code> file. A high-level overview on the database structure can be found in the database documentation.</p>"},{"location":"SETTINGS_SYSTEM/#table_settingsjson","title":"table_settings.json","text":"<p>This is the API endpoint that reflects the state of the <code>Settings</code> database table. Settings can be accessed with the:</p> <ul> <li><code>getSetting(key)</code> JavaScript method</li> </ul> <p>The json file is also cached on the client-side local storage of the browser.</p>"},{"location":"SETTINGS_SYSTEM/#appconf","title":"app.conf","text":"<p>Note</p> <p>This is the source of truth for settings. User-defined values in this files always override default values specified in the Plugin definition.</p> <p>The App generates two <code>app.conf</code> entries for every setting (Since version 23.8+). One entry is the setting value, the second is the <code>__metadata</code> associated with the setting. This <code>__metadata</code> entry contains the full setting definition in JSON format. Currently unused, but intended to be used in future to extend the Settings system.</p>"},{"location":"SETTINGS_SYSTEM/#plugin-settings","title":"Plugin settings","text":"<p>Note</p> <p>This is the preferred way adding settings going forward. I'll be likely migrating all app settings into plugin-based settings.</p> <p>Plugin settings are loaded dynamically from the <code>config.json</code> of individual plugins. If a setting isn't defined in the <code>app.conf</code> file, it is initialized via the <code>default_value</code> property of a setting from the <code>config.json</code> file. Check the Plugins documentation, section <code>\u2699 Setting object structure</code> for details on the structure of the setting.</p> <p></p>"},{"location":"SETTINGS_SYSTEM/#settings-process-flow","title":"Settings Process flow","text":"<p>The process flow is mostly managed by the initialise.py file.</p> <p>The script is responsible for reading user-defined values from a configuration file (<code>app.conf</code>), initializing settings, and importing them into a database. It also handles plugins and their configurations.</p> <p>Here's a high-level description of the code:</p> <ol> <li>Function Definitions:</li> <li> <p><code>ccd</code>: This function is used to handle user-defined settings and configurations. It takes several parameters related to the setting's name, default value, input type, options, group, and more. It saves the settings and their metadata in different lists (<code>conf.mySettingsSQLsafe</code> and <code>conf.mySettings</code>).</p> </li> <li> <p><code>importConfigs</code>: This function is the main entry point of the script. It imports user settings from a configuration file, processes them, and saves them to the database.</p> </li> <li> <p><code>read_config_file</code>: This function reads the configuration file (<code>app.conf</code>) and returns a dictionary containing the key-value pairs from the file.</p> </li> <li> <p>Importing Configuration and Initializing Settings:</p> </li> <li> <p>The <code>importConfigs</code> function starts by checking the modification time of the configuration file to determine if it needs to be re-imported. If the file has not been modified since the last import, the function skips the import process.</p> </li> <li> <p>The function reads the configuration file using the <code>read_config_file</code> function, which returns a dictionary of settings.</p> </li> <li> <p>The script then initializes various user-defined settings using the <code>ccd</code> function, based on the values read from the configuration file. These settings are categorized into groups such as \"General,\" \"Email,\" \"Webhooks,\" \"Apprise,\" and more.</p> </li> <li> <p>Plugin Handling:</p> </li> <li>The script loads and handles plugins dynamically. It retrieves plugin configurations and iterates through each plugin.</li> <li>For each plugin, it extracts the prefix and settings related to that plugin and processes them similarly to other user-defined settings.</li> <li> <p>It also handles scheduling for plugins with specific <code>RUN_SCHD</code> settings.</p> </li> <li> <p>Saving Settings to the Database:</p> </li> <li> <p>The script clears the existing settings in the database and inserts the updated settings into the database using SQL queries.</p> </li> <li> <p>Updating the API and Performing Cleanup:</p> </li> <li>After importing the configurations, the script updates the API to reflect the changes in the settings.</li> <li>It saves the current timestamp to determine the next import time.</li> <li>Finally, it logs the successful import of the new configuration.</li> </ol>"},{"location":"SMTP/","title":"\ud83d\udce7 SMTP server guides","text":"<p>The SMTP plugin supports any SMTP server. Here are some commonly used services to help speed up your configuration.</p> <p>Note</p> <p>If you are using a self hosted SMTP server ssh into the container and verify (e.g. via ping) that your server is reachable from within the NetAlertX container. See also how to ssh into the container if you are running it as a Home Assistant addon.</p>"},{"location":"SMTP/#gmail","title":"Gmail","text":"<ol> <li> <p>Create an app password by following the instructions from Google, you need to Enable 2FA for this to work. https://support.google.com/accounts/answer/185833</p> </li> <li> <p>Specify the following settings:</p> </li> </ol> <pre><code> SMTP_RUN='on_notification'\n SMTP_SKIP_TLS=True\n SMTP_FORCE_SSL=True \n SMTP_PORT=465\n SMTP_SERVER='smtp.gmail.com'\n SMTP_PASS='16-digit passcode from google'\n SMTP_REPORT_TO='some_target_email@gmail.com'\n</code></pre>"},{"location":"SMTP/#brevo","title":"Brevo","text":"<p>Brevo allows for 300 free emails per day as of time of writing. </p> <ol> <li>Create an account on Brevo: https://www.brevo.com/free-smtp-server/</li> <li>Click your name -&gt; SMTP &amp; API</li> <li>Click Generate a new SMTP key</li> <li>Save the details and fill in the NetAlertX settings as below.</li> </ol> <pre><code>SMTP_SERVER='smtp-relay.brevo.com'\nSMTP_PORT=587\nSMTP_SKIP_LOGIN=False\nSMTP_USER='user@email.com'\nSMTP_PASS='xsmtpsib-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxx'\nSMTP_SKIP_TLS=False\nSMTP_FORCE_SSL=False\nSMTP_REPORT_TO='some_target_email@gmail.com'\nSMTP_REPORT_FROM='NetAlertX &lt;user@email.com&gt;'\n</code></pre>"},{"location":"SMTP/#gmx","title":"GMX","text":"<ol> <li>Go to your GMX account https://account.gmx.com</li> <li>Under Security Options enable 2FA (Two-factor authentication)</li> <li>Under Security Options generate an Application-specific password</li> <li>Home -&gt; Email Settings -&gt; POP3 &amp; IMAP -&gt; Enable access to this account via POP3 and IMAP</li> <li>In NetAlertX specify these settings:</li> </ol> <pre><code> SMTP_RUN='on_notification'\n SMTP_SERVER='mail.gmx.com'\n SMTP_PORT=465\n SMTP_USER='gmx_email@gmx.com'\n SMTP_PASS='&lt;your Application-specific password&gt;'\n SMTP_SKIP_TLS=True\n SMTP_FORCE_SSL=True\n SMTP_SKIP_LOGIN=False\n SMTP_REPORT_FROM='gmx_email@gmx.com' # this has to be the same email as in SMTP_USER\n SMTP_REPORT_TO='some_target_email@gmail.com'\n</code></pre>"},{"location":"SUBNETS/","title":"Subnets Configuration","text":"<p>You need to specify the network interface and the network mask. You can also configure multiple subnets and specify VLANs (see VLAN exceptions below).</p> <p><code>ARPSCAN</code> can scan multiple networks if the network allows it. To scan networks directly, the subnets must be accessible from the network where NetAlertX is running. This means NetAlertX needs to have access to the interface attached to that subnet.</p> <p>Warning</p> <p>If you don't see all expected devices run the following command in the NetAlertX container (replace the interface and ip mask): <code>sudo arp-scan --interface=eth0 192.168.1.0/24</code></p> <p>If this command returns no results, the network is not accessible due to your network or firewall restrictions (Wi-Fi Extenders, VPNs and inaccessible networks). If direct scans are not possible, check the remote networks documentation for workarounds.</p>"},{"location":"SUBNETS/#example-values","title":"Example Values","text":"<p>Note</p> <p>Please use the UI to configure settings as it ensures the config file is in the correct format. Edit <code>app.conf</code> directly only when really necessary. </p> <ul> <li>Examples for one and two subnets:</li> <li>One subnet: <code>SCAN_SUBNETS = ['192.168.1.0/24 --interface=eth0']</code></li> <li>Two subnets: <code>SCAN_SUBNETS = ['192.168.1.0/24 --interface=eth0','192.168.1.0/24 --interface=eth1 --vlan=107']</code></li> </ul> <p>Tip</p> <p>When adding more subnets, you may need to increase both the scan interval (<code>ARPSCAN_RUN_SCHD</code>) and the timeout (<code>ARPSCAN_RUN_TIMEOUT</code>)\u2014as well as similar settings for related plugins.</p> <p>If the timeout is too short, you may see timeout errors in the log. To prevent the application from hanging due to unresponsive plugins, scans are canceled when they exceed the timeout limit.</p> <p>To fix this: - Reduce the subnet size (e.g., change <code>/16</code> to <code>/24</code>). - Increase the timeout (e.g., set <code>ARPSCAN_RUN_TIMEOUT</code> to <code>300</code> for a 5-minute timeout). - Extend the scan interval (e.g., set <code>ARPSCAN_RUN_SCHD</code> to <code>*/10 * * * *</code> to scan every 10 minutes).</p> <p>For more troubleshooting tips, see Debugging Plugins.</p>"},{"location":"SUBNETS/#explanation","title":"Explanation","text":""},{"location":"SUBNETS/#network-mask","title":"Network Mask","text":"<p>Example value: <code>192.168.1.0/24</code></p> <p>The <code>arp-scan</code> time itself depends on the number of IP addresses to check.</p> <p>The number of IPs to check depends on the network mask you set in the <code>SCAN_SUBNETS</code> setting. For example, a <code>/24</code> mask results in 256 IPs to check, whereas a <code>/16</code> mask checks around 65,536 IPs. Each IP takes a couple of seconds, so an incorrect configuration could make <code>arp-scan</code> take hours instead of seconds.</p> <p>Specify the network filter, which significantly speeds up the scan process. For example, the filter <code>192.168.1.0/24</code> covers IP ranges from <code>192.168.1.0</code> to <code>192.168.1.255</code>.</p>"},{"location":"SUBNETS/#network-interface-adapter","title":"Network Interface (Adapter)","text":"<p>Example value: <code>--interface=eth0</code></p> <p>The adapter will probably be <code>eth0</code> or <code>eth1</code>. (Check <code>System Info</code> &gt; <code>Network Hardware</code>, or run <code>iwconfig</code> in the container to find your interface name(s)).</p> <p></p> <p>Tip</p> <p>As an alternative to <code>iwconfig</code>, run <code>ip -o link show | awk -F': ' '!/lo|vir|docker/ {print $2}'</code> in your container to find your interface name(s) (e.g.: <code>eth0</code>, <code>eth1</code>): <pre><code>Synology-NAS:/# ip -o link show | awk -F': ' '!/lo|vir|docker/ {print $2}'\nsit0@NONE\neth1\neth0\n</code></pre></p>"},{"location":"SUBNETS/#vlans","title":"VLANs","text":"<p>Example value: <code>--vlan=107</code></p> <ul> <li>Append <code>--vlan=107</code> to the <code>SCAN_SUBNETS</code> field (e.g.: <code>192.168.1.0/24 --interface=vmbr0 --vlan=107</code>) for multiple VLANs.</li> </ul>"},{"location":"SUBNETS/#vlans-on-a-hyper-v-setup","title":"VLANs on a Hyper-V Setup","text":"<p>Community-sourced content by mscreations from this discussion.</p> <p>Tested Setup: Bare Metal \u2192 Hyper-V on Win Server 2019 \u2192 Ubuntu 22.04 VM \u2192 Docker \u2192 NetAlertX.</p> <p>Approach 1 (may cause issues): Configure multiple network adapters in Hyper-V with distinct VLANs connected to each one using Hyper-V's network setup. However, this action can potentially lead to the Docker host's inability to handle network traffic correctly. This might interfere with other applications such as Authentik.</p> <p>Approach 2 (working example):</p> <p>Network connections to switches are configured as trunk and allow all VLANs access to the server.</p> <p>By default, Hyper-V only allows untagged packets through to the VM interface, blocking VLAN-tagged packets. To fix this, follow these steps:</p> <ol> <li>Run the following command in PowerShell on the Hyper-V machine:</li> </ol> <pre><code>Set-VMNetworkAdapterVlan -VMName &lt;Docker VM Name&gt; -Trunk -NativeVlanId 0 -AllowedVlanIdList \"&lt;comma separated list of vlans&gt;\"\n</code></pre> <ol> <li>Within the VM, set up sub-interfaces for each VLAN to enable scanning. On Ubuntu 22.04, Netplan can be used. In /etc/netplan/00-installer-config.yaml, add VLAN definitions:</li> </ol> <pre><code> network:\n ethernets:\n eth0:\n dhcp4: yes\n vlans:\n eth0.2:\n id: 2\n link: eth0\n addresses: [ \"192.168.2.2/24\" ]\n routes:\n - to: 192.168.2.0/24\n via: 192.168.1.1\n</code></pre> <ol> <li>Run <code>sudo netplan apply</code> to activate the interfaces for scanning in NetAlertX.</li> </ol> <p>In this case, use <code>192.168.2.0/24 --interface=eth0.2</code> in NetAlertX.</p>"},{"location":"SUBNETS/#vlan-support-exceptions","title":"VLAN Support &amp; Exceptions","text":"<p>Please note the accessibility of macvlans when configured on the same computer. This is a general networking behavior, but feel free to clarify via a PR/issue.</p> <ul> <li>NetAlertX does not detect the macvlan container when it is running on the same computer.</li> <li>NetAlertX recognizes the macvlan container when it is running on a different computer.</li> </ul>"},{"location":"SYNOLOGY_GUIDE/","title":"Installation on a Synology NAS","text":"<p>There are different ways to install NetAlertX on a Synology, including SSH-ing into the machine and using the command line. For this guide, we will use the Project option in Container manager.</p>"},{"location":"SYNOLOGY_GUIDE/#create-the-folder-structure","title":"Create the folder structure","text":"<p>The folders you are creating below will contain the configuration and the database. Back them up regularly.</p> <ol> <li>Create a parent folder named <code>netalertx</code></li> <li>Create a <code>db</code> sub-folder</li> </ol> <p> </p> <ol> <li>Create a <code>config</code> sub-folder</li> </ol> <p></p> <ol> <li>Note down the folders Locations:</li> </ol> <p> </p>"},{"location":"SYNOLOGY_GUIDE/#creating-the-project","title":"Creating the Project","text":"<ol> <li>Open Container manager -&gt; Project and click Create.</li> <li> <p>Fill in the details:</p> </li> <li> <p>Project name: <code>netalertx</code></p> </li> <li>Path: <code>/app_storage/netalertx</code> (will differ from yours)</li> <li>Paste in the following template:</li> </ol> <pre><code>services:\n netalertx:\n container_name: netalertx\n # use the below line if you want to test the latest dev image\n # image: \"ghcr.io/netalertx/netalertx-dev:latest\"\n image: \"ghcr.io/netalertx/netalertx:latest\"\n network_mode: \"host\"\n restart: unless-stopped\n cap_drop: # Drop all capabilities for enhanced security\n - ALL\n cap_add: # Re-add necessary capabilities\n - NET_RAW\n - NET_ADMIN\n - NET_BIND_SERVICE\n - CHOWN\n - SETUID\n - SETGID\n volumes:\n - /app_storage/netalertx:/data\n # to sync with system time\n - /etc/localtime:/etc/localtime:ro\n tmpfs:\n # All writable runtime state resides under /tmp; comment out to persist logs between restarts\n - \"/tmp:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime\"\n environment:\n - PORT=20211\n</code></pre> <p></p> <ol> <li>Replace the paths to your volume and comment out unnecessary line(s).</li> </ol> <p>This is only an example, your paths will differ.</p> <pre><code>volumes:\n - /volume1/app_storage/netalertx:/data\n</code></pre> <p></p> <ol> <li>(optional) Change the port number from <code>20211</code> to an unused port if this port is already used.</li> <li>Build the project:</li> </ol> <p></p> <ol> <li>Navigate to <code>&lt;Synology URL&gt;:20211</code> (or your custom port).</li> <li>Read the Subnets and Plugins docs to complete your setup.</li> </ol>"},{"location":"SYNOLOGY_GUIDE/#solving-permission-issues","title":"Solving permission issues","text":"<p>See also the Permission overview guide.</p>"},{"location":"SYNOLOGY_GUIDE/#configuring-the-permissions-via-ssh","title":"Configuring the permissions via SSH","text":"<p>Tip</p> <p>If you are facing permissions issues run the following commands on your server. This will change the owner and assure sufficient access to the database and config files that are stored in the <code>/local_data_dir/db</code> and <code>/local_data_dir/config</code> folders (replace <code>local_data_dir</code> with the location where your <code>/db</code> and <code>/config</code> folders are located).</p> <p><code>sudo chown -R 20211:20211 /local_data_dir</code></p> <p><code>sudo chmod -R a+rwx /local_data_dir</code></p>"},{"location":"SYNOLOGY_GUIDE/#configuring-the-permissions-via-the-synology-ui","title":"Configuring the permissions via the Synology UI","text":"<p>You can also execute the above bash commands via the UI by creating a one-off scheduled task.</p> <ol> <li>Control panel -&gt; Task Scheduler</li> <li>Create -&gt; Scheduled Task -&gt; User-defined Script</li> </ol> <p></p> <ol> <li>Give your task a name.</li> </ol> <p></p> <ol> <li>Specify one-off execution time (e.g. 5 minutes from now).</li> </ol> <p></p> <ol> <li>Paste the commands from the above SSH section and replace the <code>/local_data_dir</code> with the parent fodler of your <code>/db</code> and <code>/config</code> folders.</li> </ol> <p></p> <ol> <li>Wait until the execution time passes and verify the new ownership.</li> </ol> <p></p> <p>In case of issues, double-check the Permission overview guide.</p>"},{"location":"UPDATES/","title":"Docker Update Strategies to upgrade NetAlertX","text":"<p>Warning</p> <p>For versions prior to <code>v25.6.7</code> upgrade to version <code>v25.5.24</code> first (<code>docker pull ghcr.io/jokob-sk/netalertx:25.5.24</code>) as later versions don't support a full upgrade. Alternatively, devices and settings can be migrated manually, e.g. via CSV import. See the Migration guide for details.</p> <p>This guide outlines approaches for updating Docker containers, usually when upgrading to a newer version of NetAlertX. Each method offers different benefits depending on the situation. Here are the methods:</p> <ul> <li>Manual: Direct commands to stop, remove, and rebuild containers.</li> <li>Dockcheck: Semi-automated with more control, suited for bulk updates.</li> <li>Watchtower: Fully automated, runs continuously to check and update containers.</li> <li>Portainer: Manual with UI.</li> </ul> <p>You can choose any approach that fits your workflow.</p> <p>In the examples I assume that the container name is <code>netalertx</code> and the image name is <code>netalertx</code> as well.</p> <p>Note</p> <p>See also Backup strategies to be on the safe side.</p>"},{"location":"UPDATES/#1-manual-updates","title":"1. Manual Updates","text":"<p>Use this method when you need precise control over a single container or when dealing with a broken container that needs immediate attention. Example Commands</p> <p>To manually update the <code>netalertx</code> container, stop it, delete it, remove the old image, and start a fresh one with <code>docker-compose</code>.</p> <pre><code># Stop the container\nsudo docker container stop netalertx\n\n# Remove the container\nsudo docker container rm netalertx\n\n# Remove the old image\nsudo docker image rm netalertx\n\n# Pull and start a new container\nsudo docker-compose up -d\n</code></pre>"},{"location":"UPDATES/#alternative-force-pull-with-docker-compose","title":"Alternative: Force Pull with Docker Compose","text":"<p>You can also use <code>--pull always</code> to ensure Docker pulls the latest image before starting the container:</p> <pre><code>sudo docker-compose up --pull always -d\n</code></pre>"},{"location":"UPDATES/#2-dockcheck-for-bulk-container-updates","title":"2. Dockcheck for Bulk Container Updates","text":"<p>Always check the Dockcheck docs if encountering issues with the guide below.</p> <p>Dockcheck is a useful tool if you have multiple containers to update and some flexibility for handling potential issues that might arise during mass updates. Dockcheck allows you to inspect each container and decide when to update.</p>"},{"location":"UPDATES/#example-workflow-with-dockcheck","title":"Example Workflow with Dockcheck","text":"<p>You might use Dockcheck to:</p> <ul> <li>Inspect container versions.</li> <li>Pull the latest images in bulk.</li> <li>Apply updates selectively.</li> </ul> <p>Dockcheck can help streamline bulk updates, especially if you\u2019re managing multiple containers.</p> <p>Below is a script I use to run an update of the Dockcheck script and start a check for new containers:</p> <pre><code>cd /path/to/Docker &amp;&amp;\nrm dockcheck.sh &amp;&amp;\nwget https://raw.githubusercontent.com/mag37/dockcheck/main/dockcheck.sh &amp;&amp;\nsudo chmod +x dockcheck.sh &amp;&amp;\nsudo ./dockcheck.sh\n</code></pre>"},{"location":"UPDATES/#3-automated-updates-with-watchtower","title":"3. Automated Updates with Watchtower","text":"<p>Always check the watchtower docs if encountering issues with the guide below.</p> <p>Watchtower monitors your Docker containers and automatically updates them when new images are available. This is ideal for ongoing updates without manual intervention.</p>"},{"location":"UPDATES/#setting-up-watchtower","title":"Setting Up Watchtower","text":""},{"location":"UPDATES/#1-pull-the-watchtower-image","title":"1. Pull the Watchtower Image:","text":"<pre><code>docker pull containrrr/watchtower\n</code></pre>"},{"location":"UPDATES/#2-run-watchtower-to-update-all-images","title":"2. Run Watchtower to update all images:","text":"<pre><code>docker run -d \\\n --name watchtower \\\n -v /var/run/docker.sock:/var/run/docker.sock \\\n containrrr/watchtower \\\n --interval 300 # Check for updates every 5 minutes\n</code></pre>"},{"location":"UPDATES/#3-run-watchtower-to-update-only-netalertx","title":"3. Run Watchtower to update only NetAlertX:","text":"<p>You can specify which containers to monitor by listing them. For example, to monitor netalertx only:</p> <pre><code>docker run -d \\\n --name watchtower \\\n -v /var/run/docker.sock:/var/run/docker.sock \\\n containrrr/watchtower netalertx\n</code></pre>"},{"location":"UPDATES/#4-portainer-controlled-image","title":"4. Portainer controlled image","text":"<p>This assumes you're using Portainer to manage Docker (or Docker Swarm) and want to pull the latest version of an image and redeploy the container.</p> <p>Note</p> <ul> <li>Portainer does not auto-update containers. For automation, use Watchtower or similar tools.</li> <li>Make sure you have the persistent volumes mounted or backups ready before recreating.</li> </ul>"},{"location":"UPDATES/#41-steps-to-update-an-image-in-portainer-standalone-docker","title":"4.1 Steps to Update an Image in Portainer (Standalone Docker)","text":"<ol> <li>Login to Portainer.</li> <li>Go to \"Containers\" in the left sidebar.</li> <li>Find the container you want to update, click its name.</li> <li>Click \"Recreate\" (top right).</li> <li>Tick: Pull latest image (this ensures Portainer fetches the newest version from Docker Hub or your registry).</li> <li>Click \"Recreate\" again.</li> <li>Wait for the container to be stopped, removed, and recreated with the updated image.</li> </ol>"},{"location":"UPDATES/#42-for-docker-swarm-services","title":"4.2 For Docker Swarm Services","text":"<p>If you're using Docker Swarm (under \"Stacks\" or \"Services\"):</p> <ol> <li>Go to \"Stacks\".</li> <li>Select the stack managing the container.</li> <li>Click \"Editor\" (or \"Update the Stack\").</li> <li>Add a version tag or use <code>:latest</code> if your image tag is <code>latest</code> (not recommended for production).</li> <li>Click \"Update the Stack\". \u26a0 Portainer will not pull the new image unless the tag changes OR the stack is forced to recreate.</li> <li>If image tag hasn't changed, go to \"Services\", find the service, and click \"Force Update\".</li> </ol>"},{"location":"UPDATES/#summary","title":"Summary","text":"Method Type Pros Cons Manual CLI Full control, no dependencies Tedious for many containers Dockcheck CLI Script Great for batch updates Needs setup, semi-automated Watchtower Daemonized Fully automated updates Less control, version drift Portainer UI Easy via web interface No auto-updates <p>These approaches allow you to maintain flexibility in how you update Docker containers, depending on the urgency and scale of the update.</p>"},{"location":"VERSIONS/","title":"Versions","text":""},{"location":"VERSIONS/#am-i-running-the-latest-released-version","title":"Am I running the latest released version?","text":"<p>Since version 23.01.14 NetAlertX uses a simple timestamp-based version check to verify if a new version is available. You can check the current and past releases here, or have a look at what I'm currently working on.</p> <p>If you are not on the latest version, the app will notify you, that a new released version is avialable the following way:</p>"},{"location":"VERSIONS/#via-email-on-a-notification-event","title":"\ud83d\udce7 Via email on a notification event","text":"<p>If any notification occurs and an email is sent, the email will contain a note that a new version is available. See the sample email below:</p> <p></p>"},{"location":"VERSIONS/#in-the-ui","title":"\ud83c\udd95 In the UI","text":"<p>In the UI via a notification Icon and via a custom message in the Maintenance section.</p> <p></p> <p>For a comparison, this is how the UI looks like if you are on the latest stable image:</p> <p></p>"},{"location":"VERSIONS/#implementation-details","title":"Implementation details","text":"<p>During build a /app/front/buildtimestamp.txt file is created. The app then periodically checks if a new release is available with a newer timestamp in GitHub's rest-based JSON endpoint (check the <code>def isNewVersion:</code> method for details).</p>"},{"location":"WEBHOOK_N8N/","title":"Webhooks (n8n)","text":""},{"location":"WEBHOOK_N8N/#create-a-simple-n8n-workflow","title":"Create a simple n8n workflow","text":"<p>Note</p> <p>You need to enable the <code>WEBHOOK</code> plugin first in order to follow this guide. See the Plugins guide for details.</p> <p>N8N can be used for more advanced conditional notification use cases. For example, you want only to get notified if two out of a specified list of devices is down. Or you can use other plugins to process the notifiations further. The below is a simple example of sending an email on a webhook.</p> <p></p>"},{"location":"WEBHOOK_N8N/#specify-your-email-template","title":"Specify your email template","text":"<p>See sample JSON if you want to see the JSON paths used in the email template below </p> <pre><code>Events count: {{ $json[\"body\"][\"attachments\"][0][\"text\"][\"events\"].length }}\nNew devices count: {{ $json[\"body\"][\"attachments\"][0][\"text\"][\"new_devices\"].length }}\n</code></pre>"},{"location":"WEBHOOK_N8N/#get-your-webhook-in-n8n","title":"Get your webhook in n8n","text":""},{"location":"WEBHOOK_N8N/#configure-netalertx-to-point-to-the-above-url","title":"Configure NetAlertX to point to the above URL","text":""},{"location":"WEBHOOK_SECRET/","title":"Webhook Secrets","text":"<p>Note</p> <p>This is community-contributed. Due to environment, setup, or networking differences, results may vary. Please open a PR to improve it instead of creating an issue, as the maintainer is not actively maintaining it.</p> <p>Note</p> <p>You need to enable the <code>WEBHOOK</code> plugin first in order to follow this guide. See the Plugins guide for details.</p>"},{"location":"WEBHOOK_SECRET/#how-does-the-signing-work","title":"How does the signing work?","text":"<p>NetAlertX will use the configured secret to create a hash signature of the request body. This SHA256-HMAC signature will appear in the <code>X-Webhook-Signature</code> header of each request to the webhook target URL. You can use the value of this header to validate the request was sent by NetAlertX.</p>"},{"location":"WEBHOOK_SECRET/#activating-webhook-signatures","title":"Activating webhook signatures","text":"<p>All you need to do in order to add a signature to the request headers is to set the <code>WEBHOOK_SECRET</code> config value to a non-empty string.</p>"},{"location":"WEBHOOK_SECRET/#validating-webhook-deliveries","title":"Validating webhook deliveries","text":"<p>There are a few things to keep in mind when validating the webhook delivery:</p> <ul> <li>NetAlertX uses an HMAC hex digest to compute the hash</li> <li>The signature in the <code>X-Webhook-Signature</code> header always starts with <code>sha256=</code></li> <li>The hash signature is generated using the configured <code>WEBHOOK_SECRET</code> and the request body.</li> <li>Never use a plain <code>==</code> operator. Instead, consider using a method like <code>secure_compare</code> or <code>crypto.timingSafeEqual</code>, which performs a \"constant time\" string comparison to help mitigate certain timing attacks against regular equality operators, or regular loops in JIT-optimized languages.</li> </ul>"},{"location":"WEBHOOK_SECRET/#testing-the-webhook-payload-validation","title":"Testing the webhook payload validation","text":"<p>You can use the following secret and payload to verify that your implementation is working correctly.</p> <p><code>secret</code>: 'this is my secret'</p> <p><code>payload</code>: '{\"test\":\"this is a test body\"}'</p> <p>If your implementation is correct, the signature you generated should match the following:</p> <p><code>signature</code>: bed21fcc34f98e94fd71c7edb75e51a544b4a3b38b069ebaaeb19bf4be8147e9</p> <p><code>X-Webhook-Signature</code>: sha256=bed21fcc34f98e94fd71c7edb75e51a544b4a3b38b069ebaaeb19bf4be8147e9</p>"},{"location":"WEBHOOK_SECRET/#more-information","title":"More information","text":"<p>If you want to learn more about webhook security, take a look at GitHub's webhook documentation.</p> <p>You can find examples for validating a webhook delivery here.</p>"},{"location":"WEB_UI_PORT_DEBUG/","title":"Debugging inaccessible UI","text":"<p>The application uses the following default ports:</p> <ul> <li>Web UI: <code>20211</code></li> <li>GraphQL API: <code>20212</code></li> </ul> <p>The Web UI is served by an nginx server, while the API backend runs on a Flask (Python) server.</p>"},{"location":"WEB_UI_PORT_DEBUG/#changing-ports","title":"Changing Ports","text":"<ul> <li>To change the Web UI port, update the <code>PORT</code> environment variable in the <code>docker-compose.yml</code> file.</li> <li>To change the GraphQL API port, use the <code>GRAPHQL_PORT</code> setting, either directly or via Docker: <pre><code>APP_CONF_OVERRIDE={\"GRAPHQL_PORT\":\"20212\"}\n</code></pre></li> </ul> <p>For more information, check the Docker installation guide.</p>"},{"location":"WEB_UI_PORT_DEBUG/#possible-issues-and-troubleshooting","title":"Possible issues and troubleshooting","text":"<p>Follow all of the below in order to disqualify potential causes of issues and to troubleshoot these problems faster.</p>"},{"location":"WEB_UI_PORT_DEBUG/#1-port-conflicts","title":"1. Port conflicts","text":"<p>When opening an issue or debugging:</p> <ol> <li>Include a screenshot of what you see when accessing <code>HTTP://&lt;your_server&gt;:20211</code> (or your custom port)</li> <li>Follow steps 1, 2, 3, 4 on this page</li> <li>Execute the following in the container to see the processes and their ports and submit a screenshot of the result:</li> <li><code>sudo apk add lsof</code></li> <li><code>sudo lsof -i</code></li> <li>Try running the <code>nginx</code> command in the container:</li> <li>if you get <code>nginx: [emerg] bind() to 0.0.0.0:20211 failed (98: Address in use)</code> try using a different port number</li> </ol> <p></p>"},{"location":"WEB_UI_PORT_DEBUG/#2-javascript-issues","title":"2. JavaScript issues","text":"<p>Check for browser console (F12 browser dev console) errors + check different browsers.</p>"},{"location":"WEB_UI_PORT_DEBUG/#3-clear-the-app-cache-and-cached-javascript-files","title":"3. Clear the app cache and cached JavaScript files","text":"<p>Refresh the browser cache (usually shoft + refresh), try a private window, or different browsers. Please also refresh the app cache by clicking the \ud83d\udd03 (reload) button in the header of the application.</p>"},{"location":"WEB_UI_PORT_DEBUG/#4-disable-proxies","title":"4. Disable proxies","text":"<p>If you have any reverse proxy or similar, try disabling it.</p>"},{"location":"WEB_UI_PORT_DEBUG/#5-disable-your-firewall","title":"5. Disable your firewall","text":"<p>If you are using a firewall, try to temporarily disabling it.</p>"},{"location":"WEB_UI_PORT_DEBUG/#6-post-your-docker-start-details","title":"6. Post your docker start details","text":"<p>If you haven't, post your docker compose/run command.</p>"},{"location":"WEB_UI_PORT_DEBUG/#7-check-for-errors-in-your-phpnginx-error-logs","title":"7. Check for errors in your PHP/NGINX error logs","text":"<p>In the container execute and investigate:</p> <p><code>cat /var/log/nginx/error.log</code></p> <p><code>cat /tmp/log/app.php_errors.log</code></p>"},{"location":"WEB_UI_PORT_DEBUG/#8-make-sure-permissions-are-correct","title":"8. Make sure permissions are correct","text":"<p>Tip</p> <p>You can try to start the container without mapping the <code>/data/config</code> and <code>/data/db</code> dirs and if the UI shows up then the issue is most likely related to your file system permissions or file ownership.</p> <p>Please read the Permissions troubleshooting guide and provide a screesnhot of the permissions and ownership in the <code>/data/db</code> and <code>app/config</code> directories.</p>"},{"location":"WORKFLOWS/","title":"Workflows Overview","text":"<p>The workflows module in allows to automate repetitive tasks, making network management more efficient. Whether you need to assign newly discovered devices to a specific Network Node, auto-group devices from a given vendor, unarchive a device if detected online, or automatically delete devices, this module provides the flexibility to tailor the automations to your needs.</p> <p></p> <p>Below are a few examples that demonstrate how this module can be used to simplify network management tasks.</p>"},{"location":"WORKFLOWS/#updating-workflows","title":"Updating Workflows","text":"<p>Note</p> <p>In order to apply a workflow change, you must first Save the changes and then reload the application by clicking Restart server.</p>"},{"location":"WORKFLOWS/#workflow-components","title":"Workflow components","text":""},{"location":"WORKFLOWS/#triggers","title":"Triggers","text":"<p>Triggers define the event that activates a workflow. They monitor changes to objects within the system, such as updates to devices or the insertion of new entries. When the specified event occurs, the workflow is executed.</p> <p>Tip</p> <p>Workflows not running? Check the Workflows debugging guide how to troubleshoot triggers and conditions.</p>"},{"location":"WORKFLOWS/#example-trigger","title":"Example Trigger:","text":"<ul> <li>Object Type: <code>Devices</code></li> <li>Event Type: <code>update</code></li> </ul> <p>This trigger will activate when a <code>Device</code> object is updated.</p>"},{"location":"WORKFLOWS/#conditions","title":"Conditions","text":"<p>Conditions determine whether a workflow should proceed based on certain criteria. These criteria can be set for specific fields, such as whether a device is from a certain vendor, or whether it is new or archived. You can combine conditions using logical operators (<code>AND</code>, <code>OR</code>).</p> <p>Tip</p> <p>To better understand how to use specific Device fields, please read through the Database overview guide.</p>"},{"location":"WORKFLOWS/#example-condition","title":"Example Condition:","text":"<ul> <li>Logic: <code>AND</code></li> <li>Field: <code>devVendor</code></li> <li>Operator: <code>contains</code> (case in-sensitive)</li> <li>Value: <code>Google</code></li> </ul> <p>This condition checks if the device's vendor is <code>Google</code>. The workflow will only proceed if the condition is true.</p>"},{"location":"WORKFLOWS/#actions","title":"Actions","text":"<p>Actions define the tasks that the workflow will perform once the conditions are met. Actions can include updating fields or deleting devices.</p> <p>You can include multiple actions that should execute once the conditions are met.</p>"},{"location":"WORKFLOWS/#example-action","title":"Example Action:","text":"<ul> <li>Action Type: <code>update_field</code></li> <li>Field: <code>devIsNew</code></li> <li>Value: <code>0</code></li> </ul> <p>This action updates the <code>devIsNew</code> field to <code>0</code>, marking the device as no longer new.</p>"},{"location":"WORKFLOWS/#examples","title":"Examples","text":"<p>You can find a couple of configuration examples in Workflow Examples.</p> <p>Tip</p> <p>Share your workflows in Discord or GitHub Discussions.</p>"},{"location":"WORKFLOWS_DEBUGGING/","title":"Workflows debugging and troubleshooting","text":"<p>Tip</p> <p>Before troubleshooting, please ensure you have the right Debugging and LOG_LEVEL set.</p> <p>Workflows are triggered by various events. These events are captured and listed in the Integrations -&gt; App Events section of the application.</p>"},{"location":"WORKFLOWS_DEBUGGING/#troubleshooting-triggers","title":"Troubleshooting triggers","text":"<p>Note</p> <p>Workflow events are processed once every 5 seconds. However, if a scan or other background tasks are running, this can cause a delay up to a few minutes.</p> <p>If an event doesn't trigger a workflow as expected, check the App Events section for the event. You can filter these by the ID of the device (<code>devMAC</code> or <code>devGUID</code>).</p> <p></p> <p>Once you find the Event Guid and Object GUID, use them to find relevant debug entries.</p> <p>Navigate to Mainetenace -&gt; Logs where you can filter the logs based on the Event or Object GUID.</p> <p></p> <p>Below you can find some example <code>app.log</code> entries that will help you understand why a Workflow was or was not triggered.</p> <pre><code>16:27:03 [WF] Checking if '13f0ce26-1835-4c48-ae03-cdaf38f328fe' triggers the workflow 'Sample Device Update Workflow'\n16:27:03 [WF] self.triggered 'False' for event '[[155], ['13f0ce26-1835-4c48-ae03-cdaf38f328fe'], [0], ['2025-04-02 05:26:56'], ['Devices'], ['050b6980-7af6-4409-950d-08e9786b7b33'], ['DEVICES'], ['00:11:32:ef:a5:6c'], ['192.168.1.82'], ['050b6980-7af6-4409-950d-08e9786b7b33'], [None], [0], [0], ['devPresentLastScan'], ['online'], ['update'], [None], [None], [None], [None]] and trigger {\"object_type\": \"Devices\", \"event_type\": \"insert\"}'\n16:27:03 [WF] Checking if '13f0ce26-1835-4c48-ae03-cdaf38f328fe' triggers the workflow 'Location Change'\n16:27:03 [WF] self.triggered 'True' for event '[[155], ['13f0ce26-1835-4c48-ae03-cdaf38f328fe'], [0], ['2025-04-02 05:26:56'], ['Devices'], ['050b6980-7af6-4409-950d-08e9786b7b33'], ['DEVICES'], ['00:11:32:ef:a5:6c'], ['192.168.1.82'], ['050b6980-7af6-4409-950d-08e9786b7b33'], [None], [0], [0], ['devPresentLastScan'], ['online'], ['update'], [None], [None], [None], [None]] and trigger {\"object_type\": \"Devices\", \"event_type\": \"update\"}'\n16:27:03 [WF] Event with GUID '13f0ce26-1835-4c48-ae03-cdaf38f328fe' triggered the workflow 'Location Change'\n</code></pre> <p>Note how one trigger executed, but the other didn't based on different <code>\"event_type\"</code> values. One is <code>\"event_type\": \"insert\"</code>, the other <code>\"event_type\": \"update\"</code>.</p> <p>Given the Event is a update event (note <code>...['online'], ['update'], [None]...</code> in the event structure), the <code>\"event_type\": \"insert\"</code> trigger didn't execute.</p>"},{"location":"WORKFLOW_EXAMPLES/","title":"Workflow examples","text":"<p>Workflows in NetAlertX automate actions based on real-time events and conditions. Below are practical examples that demonstrate how to build automation using triggers, conditions, and actions.</p>"},{"location":"WORKFLOW_EXAMPLES/#example-1-un-archive-devices-if-detected-online","title":"Example 1: Un-archive devices if detected online","text":"<p>This workflow automatically unarchives a device if it was previously archived but has now been detected as online.</p>"},{"location":"WORKFLOW_EXAMPLES/#use-case","title":"\ud83d\udccb Use Case","text":"<p>Sometimes devices are manually archived (e.g., no longer expected on the network), but they reappear unexpectedly. This workflow reverses the archive status when such devices are detected during a scan.</p>"},{"location":"WORKFLOW_EXAMPLES/#workflow-configuration","title":"\u2699\ufe0f Workflow Configuration","text":"<pre><code>{\n \"name\": \"Un-archive devices if detected online\",\n \"trigger\": {\n \"object_type\": \"Devices\",\n \"event_type\": \"update\"\n },\n \"conditions\": [\n {\n \"logic\": \"AND\",\n \"conditions\": [\n {\n \"field\": \"devIsArchived\",\n \"operator\": \"equals\",\n \"value\": \"1\"\n },\n {\n \"field\": \"devPresentLastScan\",\n \"operator\": \"equals\",\n \"value\": \"1\"\n }\n ]\n }\n ],\n \"actions\": [\n {\n \"type\": \"update_field\",\n \"field\": \"devIsArchived\",\n \"value\": \"0\"\n }\n ],\n \"enabled\": \"Yes\"\n}\n</code></pre>"},{"location":"WORKFLOW_EXAMPLES/#explanation","title":"\ud83d\udd0d Explanation","text":"<pre><code>- Trigger: Listens for updates to device records.\n- Conditions:\n - `devIsArchived` is `1` (archived).\n - `devPresentLastScan` is `1` (device was detected in the latest scan).\n- Action: Updates the device to set `devIsArchived` to `0` (unarchived).\n</code></pre>"},{"location":"WORKFLOW_EXAMPLES/#result","title":"\u2705 Result","text":"<p>Whenever a previously archived device shows up during a network scan, it will be automatically unarchived \u2014 allowing it to reappear in your device lists and dashboards.</p> <p>Here is your updated version of Example 2 and Example 3, fully aligned with the format and structure of Example 1 for consistency and professionalism:</p>"},{"location":"WORKFLOW_EXAMPLES/#example-2-assign-device-to-network-node-based-on-ip","title":"Example 2: Assign Device to Network Node Based on IP","text":"<p>This workflow assigns newly added devices with IP addresses in the <code>192.168.1.*</code> range to a specific network node with MAC address <code>6c:6d:6d:6c:6c:6c</code>.</p>"},{"location":"WORKFLOW_EXAMPLES/#use-case_1","title":"\ud83d\udccb Use Case","text":"<p>When new devices join your network, assigning them to the correct network node is important for accurate topology and grouping. This workflow ensures devices in a specific subnet are automatically linked to the intended node.</p>"},{"location":"WORKFLOW_EXAMPLES/#workflow-configuration_1","title":"\u2699\ufe0f Workflow Configuration","text":"<pre><code>{\n \"name\": \"Assign Device to Network Node Based on IP\",\n \"trigger\": {\n \"object_type\": \"Devices\",\n \"event_type\": \"insert\"\n },\n \"conditions\": [\n {\n \"logic\": \"AND\",\n \"conditions\": [\n {\n \"field\": \"devLastIP\",\n \"operator\": \"contains\",\n \"value\": \"192.168.1.\"\n }\n ]\n }\n ],\n \"actions\": [\n {\n \"type\": \"update_field\",\n \"field\": \"devNetworkNode\",\n \"value\": \"6c:6d:6d:6c:6c:6c\"\n }\n ],\n \"enabled\": \"Yes\"\n}\n</code></pre>"},{"location":"WORKFLOW_EXAMPLES/#explanation_1","title":"\ud83d\udd0d Explanation","text":"<ul> <li>Trigger: Activates when a new device is added.</li> <li> <p>Condition:</p> </li> <li> <p><code>devLastIP</code> contains <code>192.168.1.</code> (matches subnet).</p> </li> <li> <p>Action:</p> </li> <li> <p>Sets <code>devNetworkNode</code> to the specified MAC address.</p> </li> </ul>"},{"location":"WORKFLOW_EXAMPLES/#result_1","title":"\u2705 Result","text":"<p>New devices with IPs in the <code>192.168.1.*</code> subnet are automatically assigned to the correct network node, streamlining device organization and reducing manual work.</p>"},{"location":"WORKFLOW_EXAMPLES/#example-3-mark-device-as-not-new-and-delete-if-from-google-vendor","title":"Example 3: Mark Device as Not New and Delete If from Google Vendor","text":"<p>This workflow automatically marks newly detected Google devices as not new and deletes them immediately.</p>"},{"location":"WORKFLOW_EXAMPLES/#use-case_2","title":"\ud83d\udccb Use Case","text":"<p>You may want to automatically clear out newly detected Google devices (such as Chromecast or Google Home) if they\u2019re not needed in your device database. This workflow handles that clean-up automatically.</p>"},{"location":"WORKFLOW_EXAMPLES/#workflow-configuration_2","title":"\u2699\ufe0f Workflow Configuration","text":"<pre><code>{\n \"name\": \"Mark Device as Not New and Delete If from Google Vendor\",\n \"trigger\": {\n \"object_type\": \"Devices\",\n \"event_type\": \"update\"\n },\n \"conditions\": [\n {\n \"logic\": \"AND\",\n \"conditions\": [\n {\n \"field\": \"devVendor\",\n \"operator\": \"contains\",\n \"value\": \"Google\"\n },\n {\n \"field\": \"devIsNew\",\n \"operator\": \"equals\",\n \"value\": \"1\"\n }\n ]\n }\n ],\n \"actions\": [\n {\n \"type\": \"update_field\",\n \"field\": \"devIsNew\",\n \"value\": \"0\"\n },\n {\n \"type\": \"delete_device\"\n }\n ],\n \"enabled\": \"Yes\"\n}\n</code></pre>"},{"location":"WORKFLOW_EXAMPLES/#explanation_2","title":"\ud83d\udd0d Explanation","text":"<ul> <li>Trigger: Runs on device updates.</li> <li> <p>Conditions:</p> </li> <li> <p>Vendor contains <code>Google</code>.</p> </li> <li>Device is marked as new (<code>devIsNew</code> is <code>1</code>).</li> <li> <p>Actions:</p> </li> <li> <p>Set <code>devIsNew</code> to <code>0</code> (mark as not new).</p> </li> <li>Delete the device.</li> </ul>"},{"location":"WORKFLOW_EXAMPLES/#result_2","title":"\u2705 Result","text":"<p>Any newly detected Google devices are cleaned up instantly \u2014 first marked as not new, then deleted \u2014 helping you avoid clutter in your device records.</p>"},{"location":"docker-troubleshooting/PUID_PGID_SECURITY/","title":"PUID/PGID Security \u2014 Why the entrypoint requires numeric IDs","text":""},{"location":"docker-troubleshooting/PUID_PGID_SECURITY/#purpose","title":"Purpose","text":"<p>This short document explains the security rationale behind the root-priming entrypoint's validation of runtime user IDs (<code>PUID</code>) and group IDs (<code>PGID</code>). The validation is intentionally strict and is a safety measure to prevent environment-variable-based command injection when running as root during the initial priming stage.</p>"},{"location":"docker-troubleshooting/PUID_PGID_SECURITY/#key-points","title":"Key points","text":"<ul> <li>The entrypoint accepts only values that are strictly numeric (digits only). Non-numeric values are treated as malformed and are a fatal error.</li> <li>The fatal check exists to prevent injection or accidental shell interpretation of environment values while the container runs as root (e.g., <code>PUID=\"20211 &amp;&amp; rm -rf /\"</code>).</li> <li>There is no artificial upper bound enforced by the validation \u2014 any numeric UID/GID is valid (for example, <code>100000</code> is acceptable).</li> </ul>"},{"location":"docker-troubleshooting/PUID_PGID_SECURITY/#behavior-on-malformed-input","title":"Behavior on malformed input","text":"<ul> <li>If <code>PUID</code> or <code>PGID</code> cannot be parsed as numeric (digits-only), the entrypoint prints an explicit security message to stderr and exits with a non-zero status.</li> <li>This is a deliberate, conservative safety measure \u2014 we prefer failing fast on potentially dangerous input rather than continuing with root-privileged operations.</li> </ul>"},{"location":"docker-troubleshooting/PUID_PGID_SECURITY/#operator-guidance","title":"Operator guidance","text":"<ul> <li>Always supply numeric values for <code>PUID</code> and <code>PGID</code> in your environment (via <code>docker-compose.yml</code>, <code>docker run -e</code>, or equivalent). Example: <code>PUID=20211</code>.</li> <li>If you need to run with a high-numbered UID/GID (e.g., <code>100000</code>), that is fine \u2014 the entrypoint allows it as long as the value is numeric.</li> <li>Don\u2019t pass shell meta-characters, spaces, or compound commands in <code>PUID</code> or <code>PGID</code> \u2014 those will be rejected as malformed and cause the container to exit.</li> </ul>"},{"location":"docker-troubleshooting/PUID_PGID_SECURITY/#required-capabilities-for-privilege-drop","title":"Required Capabilities for Privilege Drop","text":"<p>If you are hardening your container by dropping capabilities (e.g., <code>cap_drop: [ALL]</code>), you must explicitly grant the <code>SETUID</code> and <code>SETGID</code> capabilities.</p> <ul> <li>Why? The entrypoint runs as root to set permissions, then uses <code>su-exec</code> to switch to the user specified by <code>PUID</code>/<code>PGID</code>. This switch requires the kernel to allow the process to change its own UID/GID.</li> <li>Symptom: If these capabilities are missing, the container will log a warning (\"su-exec failed\") and continue running as root (UID 0), defeating the purpose of setting <code>PUID</code>/<code>PGID</code>.</li> <li>Fix: Add <code>SETUID</code> and <code>SETGID</code> to your <code>cap_add</code> list.</li> </ul> <pre><code>cap_drop:\n - ALL\ncap_add:\n - SETUID\n - SETGID\n # ... other required caps like CHOWN, NET_ADMIN, etc.\n</code></pre> <p>Document created to clarify the security behavior of the root-priming entrypoint (PUID/PGID validation).</p>"},{"location":"docker-troubleshooting/aufs-capabilities/","title":"AUFS Legacy Storage Driver Support","text":""},{"location":"docker-troubleshooting/aufs-capabilities/#issue-description","title":"Issue Description","text":"<p>NetAlertX automatically detects the legacy <code>aufs</code> storage driver, which is commonly found on older Synology NAS devices (DSM 6.x/7.0.x) or Linux systems where the underlying filesystem lacks <code>d_type</code> support. This occurs on older ext4 and other filesystems which did not support capabilites at time of last formatting. While ext4 currently support capabilities and filesystem overlays, older variants of ext4 did not and require a reformat to enable the support. Old variants result in docker choosing <code>aufs</code> and newer may use <code>overlayfs</code>.</p> <p>The Technical Limitation: AUFS (Another Union File System) does not support or preserve extended file attributes (<code>xattrs</code>) during Docker image extraction. NetAlertX relies on these attributes to grant granular privileges (<code>CAP_NET_RAW</code> and <code>CAP_NET_ADMIN</code>) to network scanning binaries like <code>arp-scan</code>, <code>nmap</code>, and <code>nbtscan</code>.</p> <p>The Result: When the container runs as a standard non-root user (default) on AUFS, these binaries are stripped of their capabilities. Consequently, layer-2 network discovery will fail silently, find zero devices, or exit with \"Operation not permitted\" errors.</p>"},{"location":"docker-troubleshooting/aufs-capabilities/#operational-logic","title":"Operational Logic","text":"<p>The container is designed to inspect the runtime environment at startup (<code>/root-entrypoint.sh</code>). It respects user configuration first, falling back to safe defaults (with warnings) where necessary.</p> <p>Behavior Matrix:</p> Filesystem PUID Config Runtime User Outcome Modern (Overlay2/Btrfs) Unset <code>20211</code> Secure. Full functionality via preserved <code>setcap</code>. Legacy (AUFS) Unset <code>20211</code> Degraded. Logs warning. L2 scans fail due to missing perms. Legacy (AUFS) <code>PUID=0</code> <code>Root</code> Functional. Root privileges bypass capability requirements. Legacy (AUFS) <code>PUID=1000</code> <code>1000</code> Degraded. Logs warning. L2 scans fail due to missing perms."},{"location":"docker-troubleshooting/aufs-capabilities/#warning-log","title":"Warning Log","text":"<p>When AUFS is detected without root privileges, the system emits the following warning during startup:</p> <p>\u26a0\ufe0f WARNING: Reduced functionality (AUFS + non-root user).</p> <p>AUFS strips Linux file capabilities, so tools like arp-scan, nmap, and nbtscan fail when NetAlertX runs as a non-root PUID.</p> <p>Action: Set <code>PUID=0</code> on AUFS hosts for full functionality.</p>"},{"location":"docker-troubleshooting/aufs-capabilities/#security-ramifications","title":"Security Ramifications","text":"<p>To mitigate the AUFS limitation, the recommended fix is to run the application as the root user (<code>PUID=0</code>).</p> <ul> <li>Least Privilege: Even when running as root, NetAlertX applies <code>cap_drop: - ALL</code> and re-adds only the strictly necessary capabilities (<code>NET_RAW</code>, <code>NET_ADMIN</code>). This maintains a \"least privilege\" posture, though it is inherently less secure than running as a specific UID.</li> <li>Attack Surface: Running as UID 0 increases the theoretical attack surface. If the container were compromised, the attacker would have root access inside the container (though still isolated from the host by the Docker runtime).</li> <li>Legacy Risks: Reliance on the deprecated AUFS driver often indicates an older OS kernel or filesystem configuration, which may carry its own unpatched vulnerabilities compared to modern <code>overlay2</code> or <code>btrfs</code> setups.</li> </ul>"},{"location":"docker-troubleshooting/aufs-capabilities/#how-to-correct-the-issue","title":"How to Correct the Issue","text":"<p>Choose the scenario that best matches your environment and security requirements.</p>"},{"location":"docker-troubleshooting/aufs-capabilities/#scenario-a-modern-systems-recommended","title":"Scenario A: Modern Systems (Recommended)","text":"<p>Context: Systems using <code>overlay2</code>, <code>btrfs</code>, or <code>zfs</code>. Action: No action required. The system auto-configures <code>PUID=20211</code>.</p> <pre><code>services:\n netalertx:\n image: netalertx/netalertx\n # No PUID/PGID needed; defaults to secure non-root\n</code></pre>"},{"location":"docker-troubleshooting/aufs-capabilities/#scenario-b-legacysynology-aufs-the-fix","title":"Scenario B: Legacy/Synology AUFS (The Fix)","text":"<p>Context: Synology DSM 6.x/7.x or Linux hosts using AUFS. Action: Explicitly elevate to root. This bypasses the need for file capabilities because Root inherits runtime capabilities directly from Docker.</p> <pre><code>services:\n netalertx:\n image: netalertx/netalertx\n environment:\n - PUID=0 # Required for arp-scan/nmap on AUFS\n - PGID=0\n</code></pre>"},{"location":"docker-troubleshooting/aufs-capabilities/#scenario-c-forced-non-root-on-aufs","title":"Scenario C: Forced Non-Root on AUFS","text":"<p>Context: Strict security compliance requires non-root, even if it breaks functionality. Action: The warning will persist. The Web UI and Database will function, but network discovery (ARP/Nmap) will be severely limited.</p> <pre><code>services:\n netalertx:\n image: netalertx/netalertx\n environment:\n - PUID=1000\n - PGID=1000\n # Note: cap_add is ineffective here due to AUFS stripping the binary's file caps\n</code></pre>"},{"location":"docker-troubleshooting/aufs-capabilities/#infrastructure-upgrades-long-term-fix","title":"Infrastructure Upgrades (Long-term Fix)","text":"<p>To solve the root cause and run securely as non-root, you must migrate off the AUFS driver.</p>"},{"location":"docker-troubleshooting/aufs-capabilities/#1-switch-to-btrfs-synology-recommended","title":"1. Switch to Btrfs (Synology Recommended)","text":"<p>If your NAS supports it, creating a new volume formatted as Btrfs allows Docker to use the native <code>btrfs</code> storage driver.</p> <ul> <li>Benefit: This driver fully supports extended attributes and Copy-on-Write (CoW), creating the most robust environment for Docker.</li> </ul>"},{"location":"docker-troubleshooting/aufs-capabilities/#2-reformat-ext4-with-d_type-support","title":"2. Reformat Ext4 with <code>d_type</code> Support","text":"<p>If you must use <code>ext4</code>, the issue is likely that your volume lacks <code>d_type</code> support (common on older volumes created before DSM 6).</p> <ul> <li>Fix: Back up your data and reformat the volume.</li> <li>Result: Modern formatting usually enables <code>d_type</code> by default. This allows Docker to automatically select the modern <code>overlay2</code> driver instead of failing back to AUFS.</li> </ul>"},{"location":"docker-troubleshooting/aufs-capabilities/#technical-implementation","title":"Technical Implementation","text":""},{"location":"docker-troubleshooting/aufs-capabilities/#detection-mechanism","title":"Detection Mechanism","text":"<p>The logic resides in <code>_detect_storage_driver()</code> within <code>/root-entrypoint.sh</code>. It parses the root mount point (<code>/</code>) to identify the underlying driver.</p> <pre><code># Modern (overlay2) - Pass\noverlay / overlay rw,relatime,lowerdir=...\n\n# Legacy (AUFS) - Triggers Warning\nnone / aufs rw,relatime,si=...\n</code></pre>"},{"location":"docker-troubleshooting/aufs-capabilities/#verification-troubleshooting","title":"Verification &amp; Troubleshooting","text":"<p>1. Confirm Storage Driver If your host is using ext4 you might be defaulting to aufs:</p> <pre><code>docker info | grep \"Storage Driver\"\n# OR inside the container:\ndocker exec netalertx grep \" / \" /proc/mounts\n</code></pre> <p>2. Verify Capability Loss If scans fail, check if the binary permissions were stripped.</p> <ul> <li>Modern FS: Returns <code>cap_net_admin,cap_net_raw+eip</code></li> <li>AUFS: Returns empty output (stripped)</li> </ul> <pre><code>docker exec netalertx getcap /usr/sbin/arp-scan\n</code></pre> <p>3. Simulating AUFS (Dev/Test) Developers can force the AUFS logic path on a modern machine by mocking the mounts file. Note: Docker often restricts direct bind-mounts of host <code>/proc</code> paths, so the test suite uses an environment-variable injection instead (see <code>test_puid_pgid.py</code>).</p> <pre><code># Create mock mounts content and encode it as base64\necho \"none / aufs rw,relatime 0 0\" | base64\n\n# Run the container passing the encoded mounts via NETALERTX_PROC_MOUNTS_B64\n# (the entrypoint decodes this and uses it instead of reading /proc/mounts directly)\ndocker run --rm -e NETALERTX_PROC_MOUNTS_B64=\"bm9uZSAvIGF1ZnMgcncs...\" netalertx/netalertx\n</code></pre>"},{"location":"docker-troubleshooting/aufs-capabilities/#additional-resources","title":"Additional Resources","text":"<ul> <li>Docker Storage Drivers: Use the OverlayFS storage driver</li> <li>Synology Docker Guide: Synology Docker Storage Drivers</li> <li>Configuration Guidance: DOCKER_COMPOSE.md</li> </ul>"},{"location":"docker-troubleshooting/excessive-capabilities/","title":"Excessive Capabilities","text":""},{"location":"docker-troubleshooting/excessive-capabilities/#issue-description","title":"Issue Description","text":"<p>Excessive Linux capabilities are detected beyond the necessary NET_ADMIN, NET_BIND_SERVICE, and NET_RAW. This may indicate overly permissive container configuration.</p>"},{"location":"docker-troubleshooting/excessive-capabilities/#security-ramifications","title":"Security Ramifications","text":"<p>While the detected capabilities might not directly harm operation, running with more privileges than necessary increases the attack surface. If the container is compromised, additional capabilities could allow broader system access or privilege escalation.</p>"},{"location":"docker-troubleshooting/excessive-capabilities/#why-youre-seeing-this-issue","title":"Why You're Seeing This Issue","text":"<p>This occurs when your Docker configuration grants more capabilities than required for network monitoring. The application only needs specific network-related capabilities for proper function.</p>"},{"location":"docker-troubleshooting/excessive-capabilities/#how-to-correct-the-issue","title":"How to Correct the Issue","text":"<p>Limit capabilities to only those required:</p> <ul> <li>In docker-compose.yml, specify only needed caps: <pre><code>cap_add:\n - NET_RAW\n - NET_ADMIN\n - NET_BIND_SERVICE\n</code></pre></li> <li>Remove any unnecessary <code>--cap-add</code> or <code>--privileged</code> flags from docker run commands</li> </ul>"},{"location":"docker-troubleshooting/excessive-capabilities/#additional-resources","title":"Additional Resources","text":"<p>Docker Compose setup can be complex. We recommend starting with the default docker-compose.yml as a base and modifying it incrementally.</p> <p>For detailed Docker Compose configuration guidance, see: DOCKER_COMPOSE.md</p>"},{"location":"docker-troubleshooting/file-permissions/","title":"File Permission Issues","text":""},{"location":"docker-troubleshooting/file-permissions/#issue-description","title":"Issue Description","text":"<p>NetAlertX cannot read from or write to critical configuration and database files. This prevents the application from saving data, logs, or configuration changes.</p>"},{"location":"docker-troubleshooting/file-permissions/#security-ramifications","title":"Security Ramifications","text":"<p>Incorrect file permissions can expose sensitive configuration data or database contents to unauthorized access. Network monitoring tools handle sensitive information about devices on your network, and improper permissions could lead to information disclosure.</p>"},{"location":"docker-troubleshooting/file-permissions/#why-youre-seeing-this-issue","title":"Why You're Seeing This Issue","text":"<p>This occurs when the mounted volumes for configuration and database files don't have proper ownership or permissions set for the netalertx user (UID 20211). The container expects these files to be accessible by the service account, not root or other users.</p>"},{"location":"docker-troubleshooting/file-permissions/#how-to-correct-the-issue","title":"How to Correct the Issue","text":"<p>Fix permissions on the host system for the mounted directories:</p> <ul> <li>Ensure the config and database directories are owned by the netalertx user: <code>chown -R 20211:20211 /path/to/config /path/to/db</code></li> <li>Set appropriate permissions: <code>chmod -R 755 /path/to/config /path/to/db</code> for directories, <code>chmod 644</code> for files</li> <li>Alternatively, restart the container with root privileges temporarily to allow automatic permission fixing, then switch back to the default user</li> </ul>"},{"location":"docker-troubleshooting/file-permissions/#additional-resources","title":"Additional Resources","text":"<p>Docker Compose setup can be complex. We recommend starting with the default docker-compose.yml as a base and modifying it incrementally.</p> <p>For detailed Docker Compose configuration guidance, see: DOCKER_COMPOSE.md</p>"},{"location":"docker-troubleshooting/incorrect-user/","title":"Incorrect Container User","text":""},{"location":"docker-troubleshooting/incorrect-user/#issue-description","title":"Issue Description","text":"<p>NetAlertX is running as a UID:GID that does not match the runtime service user configured for this container (default 20211:20211). Hardened ownership on writable paths may block writes if the UID/GID do not align with mounted volumes and tmpfs settings.</p>"},{"location":"docker-troubleshooting/incorrect-user/#security-ramifications","title":"Security Ramifications","text":"<p>The image uses a dedicated service user for writes and a readonly lock owner (UID 20211) for code/venv with 004/005 permissions. Running as an arbitrary UID is supported, but only when writable mounts (<code>/data</code>, <code>/tmp/*</code>) are owned by that UID. Misalignment can cause startup failures or unexpected permission escalation attempts.</p>"},{"location":"docker-troubleshooting/incorrect-user/#why-youre-seeing-this-issue","title":"Why You're Seeing This Issue","text":"<ul> <li>A <code>user:</code> override in docker-compose.yml or <code>--user</code> flag on <code>docker run</code> changes the runtime UID/GID without updating mount ownership.</li> <li>Tmpfs mounts still use <code>uid=20211,gid=20211</code> while the container runs as another UID.</li> <li>Host bind mounts (e.g., <code>/data</code>) are owned by a different UID.</li> </ul>"},{"location":"docker-troubleshooting/incorrect-user/#how-to-correct-the-issue","title":"How to Correct the Issue","text":"<p>Option A: Use defaults (recommended) - Remove custom <code>user:</code> overrides and <code>--user</code> flags. - Let the container run as the built-in service user (UID/GID 20211) and keep tmpfs at <code>uid=20211,gid=20211</code>.</p> <p>Option B: Run with a custom UID/GID - Set <code>user:</code> (or <code>NETALERTX_UID/NETALERTX_GID</code>) to your desired UID/GID. - Align mounts: ensure <code>/data</code> (and any <code>/tmp/*</code> tmpfs) use the same <code>uid=</code>/<code>gid=</code> and that host bind mounts are chowned to that UID/GID. - Recreate the container so ownership is consistent.</p>"},{"location":"docker-troubleshooting/incorrect-user/#additional-resources","title":"Additional Resources","text":"<ul> <li>Default compose and tmpfs guidance: DOCKER_COMPOSE.md</li> <li>General Docker install and runtime notes: DOCKER_INSTALLATION.md</li> </ul>"},{"location":"docker-troubleshooting/missing-capabilities/","title":"Missing Network Capabilities","text":""},{"location":"docker-troubleshooting/missing-capabilities/#issue-description","title":"Issue Description","text":"<p>Raw network capabilities (NET_RAW, NET_ADMIN, NET_BIND_SERVICE) are missing. Tools that rely on these capabilities (e.g., nmap -sS, arp-scan, nbtscan) will not function.</p>"},{"location":"docker-troubleshooting/missing-capabilities/#security-ramifications","title":"Security Ramifications","text":"<p>Network scanning and monitoring requires low-level network access that these capabilities provide. Without them, the application cannot perform essential functions like ARP scanning, port scanning, or passive network discovery, severely limiting its effectiveness.</p>"},{"location":"docker-troubleshooting/missing-capabilities/#why-youre-seeing-this-issue","title":"Why You're Seeing This Issue","text":"<p>This occurs when the container doesn't have the necessary Linux capabilities granted. Docker containers run with limited capabilities by default, and network monitoring tools need elevated network privileges.</p>"},{"location":"docker-troubleshooting/missing-capabilities/#how-to-correct-the-issue","title":"How to Correct the Issue","text":"<p>Add the required capabilities to your container:</p> <ul> <li>In docker-compose.yml: <pre><code>cap_add:\n - NET_RAW\n - NET_ADMIN\n - NET_BIND_SERVICE\n</code></pre></li> <li>For docker run: <code>--cap-add=NET_RAW --cap-add=NET_ADMIN --cap-add=NET_BIND_SERVICE</code></li> </ul>"},{"location":"docker-troubleshooting/missing-capabilities/#additional-resources","title":"Additional Resources","text":"<p>Docker Compose setup can be complex. We recommend starting with the default docker-compose.yml as a base and modifying it incrementally.</p> <p>For detailed Docker Compose configuration guidance, see: DOCKER_COMPOSE.md</p>"},{"location":"docker-troubleshooting/missing-capabilities/#cap_chown-required-when-cap_drop-all","title":"CAP_CHOWN required when cap_drop: [ALL]","text":"<p>When you start NetAlertX with <code>cap_drop: [ALL]</code>, the container loses <code>CAP_CHOWN</code>. The root priming step needs <code>CAP_CHOWN</code> to adjust ownership of <code>/data</code> and <code>/tmp</code> before dropping privileges to <code>PUID:PGID</code>. Without it, startup fails with a fatal <code>failed to chown</code> message and exits.</p> <p>To fix: - Add <code>CHOWN</code> back in <code>cap_add</code> when you also set <code>cap_drop: [ALL]</code>:</p> <pre><code>cap_drop:\n - ALL\ncap_add:\n - CHOWN\n</code></pre> <ul> <li>Or pre-chown the mounted host paths to your target <code>PUID:PGID</code> so the priming step does not need the capability.</li> </ul> <p>If you harden capabilities further, expect priming to fail until you restore the minimum set needed for ownership changes.</p>"},{"location":"docker-troubleshooting/mount-configuration-issues/","title":"Mount Configuration Issues","text":""},{"location":"docker-troubleshooting/mount-configuration-issues/#issue-description","title":"Issue Description","text":"<p>NetAlertX has detected configuration issues with your Docker volume mounts. These may include write permission problems, data loss risks, or performance concerns marked with \u274c in the table.</p>"},{"location":"docker-troubleshooting/mount-configuration-issues/#security-ramifications","title":"Security Ramifications","text":"<p>Improper mount configurations can lead to data loss, performance degradation, or security vulnerabilities. For persistent data (database and configuration), using non-persistent storage like tmpfs can result in complete data loss on container restart. For temporary data, using persistent storage may unnecessarily expose sensitive logs or cache data.</p>"},{"location":"docker-troubleshooting/mount-configuration-issues/#why-youre-seeing-this-issue","title":"Why You're Seeing This Issue","text":"<p>This occurs when your Docker Compose or run configuration doesn't properly map host directories to container paths, or when the mounted volumes have incorrect permissions. The application requires specific paths to be writable for operation, and some paths should use persistent storage while others should be temporary.</p>"},{"location":"docker-troubleshooting/mount-configuration-issues/#how-to-correct-the-issue","title":"How to Correct the Issue","text":"<p>Review and correct your volume mounts in docker-compose.yml:</p> <ul> <li>Ensure <code>${NETALERTX_DB}</code> and <code>${NETALERTX_CONFIG}</code> use persistent host directories</li> <li>Ensure <code>${NETALERTX_API}</code>, <code>${NETALERTX_LOG}</code> have appropriate permissions</li> <li>Avoid mounting sensitive paths to non-persistent filesystems like tmpfs for critical data</li> <li>Use bind mounts with proper ownership (netalertx user: 20211:20211)</li> </ul> <p>Example volume configuration: <pre><code>volumes:\n - ./data/db:/data/db\n - ./data/config:/data/config\n - ./data/log:/tmp/log\n</code></pre></p>"},{"location":"docker-troubleshooting/mount-configuration-issues/#additional-resources","title":"Additional Resources","text":"<p>Docker Compose setup can be complex. We recommend starting with the default docker-compose.yml as a base and modifying it incrementally.</p> <p>For detailed Docker Compose configuration guidance, see: DOCKER_COMPOSE.md</p>"},{"location":"docker-troubleshooting/network-mode/","title":"Network Mode Configuration","text":""},{"location":"docker-troubleshooting/network-mode/#issue-description","title":"Issue Description","text":"<p>NetAlertX is not running with <code>--network=host</code>. Bridge networking blocks passive discovery (ARP, NBNS, mDNS) and active scanning accuracy.</p>"},{"location":"docker-troubleshooting/network-mode/#security-ramifications","title":"Security Ramifications","text":"<p>Host networking is required for comprehensive network monitoring. Bridge mode isolates the container from raw network access needed for ARP scanning, passive discovery protocols, and accurate device detection. Without host networking, the application cannot fully monitor your network.</p>"},{"location":"docker-troubleshooting/network-mode/#why-youre-seeing-this-issue","title":"Why You're Seeing This Issue","text":"<p>This occurs when your Docker configuration uses bridge networking instead of host networking. Network monitoring requires direct access to the host's network interfaces to perform passive discovery and active scanning.</p>"},{"location":"docker-troubleshooting/network-mode/#how-to-correct-the-issue","title":"How to Correct the Issue","text":"<p>Enable host networking mode:</p> <ul> <li>In docker-compose.yml, add: <code>network_mode: host</code></li> <li>For docker run, use: <code>--network=host</code></li> <li>Ensure the container has required capabilities: <code>--cap-add=NET_RAW --cap-add=NET_ADMIN --cap-add=NET_BIND_SERVICE</code></li> </ul>"},{"location":"docker-troubleshooting/network-mode/#additional-resources","title":"Additional Resources","text":"<p>Docker Compose setup can be complex. We recommend starting with the default docker-compose.yml as a base and modifying it incrementally.</p> <p>For detailed Docker Compose configuration guidance, see: DOCKER_COMPOSE.md</p>"},{"location":"docker-troubleshooting/nginx-configuration-mount/","title":"Nginx Configuration Mount Issues","text":""},{"location":"docker-troubleshooting/nginx-configuration-mount/#issue-description","title":"Issue Description","text":"<p>You've configured a custom port for NetAlertX, but the required nginx configuration mount is missing or not writable. Without this mount, the container cannot apply your port changes and will fall back to the default port 20211.</p>"},{"location":"docker-troubleshooting/nginx-configuration-mount/#security-ramifications","title":"Security Ramifications","text":"<p>Running in read-only mode (as recommended) prevents the container from modifying its own nginx configuration. Without a writable mount, custom port configurations cannot be applied, potentially exposing the service on unintended ports or requiring fallback to defaults.</p>"},{"location":"docker-troubleshooting/nginx-configuration-mount/#why-youre-seeing-this-issue","title":"Why You're Seeing This Issue","text":"<p>This occurs when you set a custom PORT environment variable (other than 20211) but haven't provided a writable mount for nginx configuration. The container needs to write custom nginx config files when running in read-only mode.</p>"},{"location":"docker-troubleshooting/nginx-configuration-mount/#how-to-correct-the-issue","title":"How to Correct the Issue","text":"<p>If you want to use a custom port, create a bind mount for the nginx configuration:</p> <ul> <li>Create a directory on your host: <code>mkdir -p /path/to/nginx-config</code></li> <li>Add to your docker-compose.yml: <pre><code>volumes:\n - /path/to/nginx-config:/tmp/nginx/active-config\nenvironment:\n - PORT=your_custom_port\n</code></pre></li> <li>Ensure it's owned by the netalertx user: <code>chown -R 20211:20211 /path/to/nginx-config</code></li> <li>Set permissions: <code>chmod -R 700 /path/to/nginx-config</code></li> </ul> <p>If you don't need a custom port, simply omit the PORT environment variable and the container will use 20211 by default.</p>"},{"location":"docker-troubleshooting/nginx-configuration-mount/#additional-resources","title":"Additional Resources","text":"<p>Docker Compose setup can be complex. We recommend starting with the default docker-compose.yml as a base and modifying it incrementally.</p> <p>For detailed Docker Compose configuration guidance, see: DOCKER_COMPOSE.md</p>"},{"location":"docker-troubleshooting/port-conflicts/","title":"Port Conflicts","text":""},{"location":"docker-troubleshooting/port-conflicts/#issue-description","title":"Issue Description","text":"<p>The configured application port (default 20211) or GraphQL API port (default 20212) is already in use by another service. This commonly occurs when you already have another NetAlertX instance running.</p>"},{"location":"docker-troubleshooting/port-conflicts/#security-ramifications","title":"Security Ramifications","text":"<p>Port conflicts prevent the application from starting properly, leaving network monitoring services unavailable. Running multiple instances on the same ports can also create configuration confusion and potential security issues if services are inadvertently exposed.</p>"},{"location":"docker-troubleshooting/port-conflicts/#why-youre-seeing-this-issue","title":"Why You're Seeing This Issue","text":"<p>This error typically occurs when:</p> <ul> <li>You already have NetAlertX running - Another Docker container or devcontainer instance is using the default ports 20211 and 20212</li> <li>Port conflicts with other services - Other applications on your system are using these ports</li> <li>Configuration error - Both PORT and GRAPHQL_PORT environment variables are set to the same value</li> </ul>"},{"location":"docker-troubleshooting/port-conflicts/#how-to-correct-the-issue","title":"How to Correct the Issue","text":""},{"location":"docker-troubleshooting/port-conflicts/#check-for-existing-netalertx-instances","title":"Check for Existing NetAlertX Instances","text":"<p>First, check if you already have NetAlertX running:</p> <pre><code># Check for running NetAlertX containers\ndocker ps | grep netalertx\n\n# Check for devcontainer processes\nps aux | grep netalertx\n\n# Check what services are using the ports\nnetstat -tlnp | grep :20211\nnetstat -tlnp | grep :20212\n</code></pre>"},{"location":"docker-troubleshooting/port-conflicts/#stop-conflicting-instances","title":"Stop Conflicting Instances","text":"<p>If you find another NetAlertX instance:</p> <pre><code># Stop specific container\ndocker stop &lt;container_name&gt;\n\n# Stop all NetAlertX containers\ndocker stop $(docker ps -q --filter ancestor=jokob-sk/netalertx)\n\n# Stop devcontainer services\n# Use VS Code command palette: \"Dev Containers: Rebuild Container\"\n</code></pre>"},{"location":"docker-troubleshooting/port-conflicts/#configure-different-ports","title":"Configure Different Ports","text":"<p>If you need multiple instances, configure unique ports:</p> <pre><code>environment:\n - PORT=20211 # Main application port\n - GRAPHQL_PORT=20212 # GraphQL API port\n</code></pre> <p>For a second instance, use different ports:</p> <pre><code>environment:\n - PORT=20213 # Different main port\n - GRAPHQL_PORT=20214 # Different API port\n</code></pre>"},{"location":"docker-troubleshooting/port-conflicts/#alternative-use-different-container-names","title":"Alternative: Use Different Container Names","text":"<p>When running multiple instances, use unique container names:</p> <pre><code>services:\n netalertx-primary:\n # ... existing config\n netalertx-secondary:\n # ... config with different ports\n</code></pre>"},{"location":"docker-troubleshooting/port-conflicts/#additional-resources","title":"Additional Resources","text":"<p>Docker Compose setup can be complex. We recommend starting with the default docker-compose.yml as a base and modifying it incrementally.</p> <p>For detailed Docker Compose configuration guidance, see: DOCKER_COMPOSE.md</p>"},{"location":"docker-troubleshooting/read-only-filesystem/","title":"Read-Only Filesystem Mode","text":""},{"location":"docker-troubleshooting/read-only-filesystem/#issue-description","title":"Issue Description","text":"<p>The container is running as read-write instead of read-only mode. This reduces the security hardening of the appliance.</p>"},{"location":"docker-troubleshooting/read-only-filesystem/#security-ramifications","title":"Security Ramifications","text":"<p>Read-only root filesystem is a security best practice that prevents malicious modifications to the container's filesystem. Running read-write allows potential attackers to modify system files or persist malware within the container.</p>"},{"location":"docker-troubleshooting/read-only-filesystem/#why-youre-seeing-this-issue","title":"Why You're Seeing This Issue","text":"<p>This occurs when the Docker configuration doesn't mount the root filesystem as read-only. The application is designed as a security appliance that should prevent filesystem modifications.</p>"},{"location":"docker-troubleshooting/read-only-filesystem/#how-to-correct-the-issue","title":"How to Correct the Issue","text":"<p>Enable read-only mode:</p> <ul> <li>In docker-compose.yml, add: <code>read_only: true</code></li> <li>For docker run, use: <code>--read-only</code></li> <li>Ensure necessary directories are mounted as writable volumes (tmp, logs, etc.)</li> </ul>"},{"location":"docker-troubleshooting/read-only-filesystem/#additional-resources","title":"Additional Resources","text":"<p>Docker Compose setup can be complex. We recommend starting with the default docker-compose.yml as a base and modifying it incrementally.</p> <p>For detailed Docker Compose configuration guidance, see: DOCKER_COMPOSE.md</p>"},{"location":"docker-troubleshooting/running-as-root/","title":"Running as Root User","text":"<p>Tip</p> <p>Looking for how to run the container as root? See the File permissions documentation for details.</p>"},{"location":"docker-troubleshooting/running-as-root/#issue-description","title":"Issue Description","text":"<p>NetAlertX has detected that the container is running with root privileges (UID 0). This configuration bypasses all built-in security hardening measures designed to protect your system.</p>"},{"location":"docker-troubleshooting/running-as-root/#security-ramifications","title":"Security Ramifications","text":"<p>Running security-critical applications like network monitoring tools as root grants unrestricted access to your host system. A successful compromise here could jeopardize your entire infrastructure, including other containers, host services, and potentially your network.</p>"},{"location":"docker-troubleshooting/running-as-root/#why-youre-seeing-this-issue","title":"Why You're Seeing This Issue","text":"<p>This typically occurs when you've explicitly overridden the container's default user in your Docker configuration, such as using <code>user: root</code> or <code>--user 0:0</code> in docker-compose.yml or docker run commands. The application is designed to run under a dedicated, non-privileged service account for security.</p>"},{"location":"docker-troubleshooting/running-as-root/#how-to-correct-the-issue","title":"How to Correct the Issue","text":"<p>Switch to the dedicated 'netalertx' user by removing any custom user directives:</p> <ul> <li>Remove <code>user:</code> entries from your docker-compose.yml</li> <li>Avoid <code>--user</code> flags in docker run commands</li> <li>Ensure the container runs with the default UID 20211:20211</li> </ul> <p>After making these changes, restart the container. The application will automatically adjust ownership of required directories.</p>"},{"location":"docker-troubleshooting/running-as-root/#additional-resources","title":"Additional Resources","text":"<p>Docker Compose setup can be complex. We recommend starting with the default docker-compose.yml as a base and modifying it incrementally.</p> <p>For detailed Docker Compose configuration guidance, see: DOCKER_COMPOSE.md</p>"}]}