Compare commits

...

35 Commits

Author SHA1 Message Date
Jokob-sk
71c20e159d Docs 2023-10-02 16:54:54 +11:00
Jokob-sk
205c143782 Docs 2023-10-02 16:51:51 +11:00
Jokob-sk
ed7c919201 Docs + on HW install v0.2 2023-10-02 16:25:15 +11:00
Jokob-sk
842014160b Docs + on HW install v0.1 2023-10-02 14:29:45 +11:00
Jokob-sk
2a0f464c63 docs + #457 work 2023-10-01 16:09:17 +11:00
Jokob-sk
c412f025ca docs 2023-10-01 09:20:21 +11:00
jokob-sk
299371b38c Merge pull request #459 from nimec01/webhook-signatures
Add webhook signatures with amazing docs - thanks to @nimec01 🙏
2023-09-30 12:47:23 +00:00
Nick
d57c38bb3d add WEBHOOK_SECRET name and description 2023-09-30 13:27:38 +02:00
Nick
59231739a2 fix spelling 2023-09-30 13:23:42 +02:00
Nick
7ba6941aed Merge remote-tracking branch 'origin/main' into main 2023-09-30 11:52:44 +02:00
Nick
95f9b348cd secure webhooks using signatures 2023-09-30 11:48:49 +02:00
Jokob-sk
ebeeb6c3a5 Better version handling #458 + docs 2023-09-30 09:53:15 +10:00
Jokob-sk
07367a2ca3 NETWORK_DEVICE_TYPES #452 2023-09-22 08:22:28 +10:00
Jokob-sk
3d848a70c7 ddns plugin 0.1 + internet_ip 0.4 2023-09-21 07:52:52 +10:00
Jokob-sk
c5d1cd919a internet_ip plugin 0.3 2023-09-20 22:20:41 +10:00
Jokob-sk
c08b70a38d internet_ip plugin 0.2 2023-09-20 21:53:22 +10:00
Jokob-sk
add9800f42 internet_ip plugin 2023-09-19 07:48:53 +10:00
Jokob-sk
1395dd9fb5 vendor_update plugin 2023-09-18 14:59:49 +10:00
Jokob-sk
12b89f7e24 vendor_update plugin 2023-09-18 14:29:44 +10:00
Jokob-sk
0ab5ca32a6 vendor_update plugin 2023-09-18 08:36:11 +10:00
Jokob-sk
19f42e60ba vendor_update plugin 2023-09-18 08:31:41 +10:00
Jokob-sk
a5b952f18c db_cleanup plugin 2023-09-17 21:46:05 +10:00
Jokob-sk
80de181827 Docs, Readme, Donations 2023-09-17 11:31:41 +10:00
Jokob-sk
44856f9c04 Loading spinner + app_state.json, settings work 2023-09-16 21:14:34 +10:00
Jokob-sk
40c6c65ee5 Loading spinner 2023-09-16 09:57:38 +10:00
Jokob-sk
6cb4439d59 pialert_app_state.json 2023-09-16 08:34:14 +10:00
Jokob-sk
03970f985e better statuses, unifi setting type change 2023-09-15 20:55:39 +10:00
Jokob-sk
31a94b779f DHCPSRVS bugfix - past results 2023-09-15 20:29:50 +10:00
Jokob-sk
f9a400c34c Optimize init, code clenup 2023-09-15 20:25:48 +10:00
jokob-sk
5cea39c9c0 Merge pull request #442 from tuhriel/unifi_full_import
Added functionality to perform a full import
2023-09-15 10:09:41 +00:00
Jokob-sk
99cd07658e SMTP docs 2023-09-14 22:23:59 +10:00
jokob-sk
11fcaa2538 Update pialert.conf
pialert.conf cleanup
2023-09-14 07:28:04 +10:00
pi@skippy
8af961ff3f unifi full import debugged 2023-09-13 00:09:06 +02:00
pi@skippy
10ee4a17a5 resolved merge conflict in unifi import config file 2023-09-13 00:08:25 +02:00
stefan@pc
bbe0ba0389 added full import functionality 2023-09-12 23:30:41 +02:00
86 changed files with 5387 additions and 2007 deletions

129
README.md
View File

@@ -1,99 +1,90 @@
# Pi.Alert
<!--- --------------------------------------------------------------------- --->
# 💻🔍 Network security scanner
💻🔍 WIFI / LAN intruder detector.
Scans for devices, port changes on your WIFI/LAN and alerts you if unknown devices or changes are found.
Scans for devices connected to your WIFI / LAN and alerts you if new and unknown devices are found.
![Main screen][main]
# 🐳 Docker image
[![Docker](https://img.shields.io/github/actions/workflow/status/jokob-sk/Pi.Alert/docker_prod.yml?label=Build&logo=GitHub)](https://github.com/jokob-sk/Pi.Alert/actions/workflows/docker_prod.yml)
[![GitHub Committed](https://img.shields.io/github/last-commit/jokob-sk/Pi.Alert?color=40ba12&label=Committed&logo=GitHub&logoColor=fff)](https://github.com/jokob-sk/Pi.Alert)
[![Docker Size](https://img.shields.io/docker/image-size/jokobsk/pi.alert?label=Size&logo=Docker&color=0aa8d2&logoColor=fff)](https://hub.docker.com/r/jokobsk/pi.alert)
[![Docker Pulls](https://img.shields.io/docker/pulls/jokobsk/pi.alert?label=Pulls&logo=docker&color=0aa8d2&logoColor=fff)](https://hub.docker.com/r/jokobsk/pi.alert)
[![Docker Pushed](https://img.shields.io/badge/dynamic/json?color=0aa8d2&logoColor=fff&label=Pushed&query=last_updated&url=https%3A%2F%2Fhub.docker.com%2Fv2%2Frepositories%2Fjokobsk%2Fpi.alert%2F&logo=docker&link=http://left&link=https://hub.docker.com/repository/docker/jokobsk/pi.alert)](https://hub.docker.com/r/jokobsk/pi.alert)
🐳 [Docker hub](https://registry.hub.docker.com/r/jokobsk/pi.alert) | 📑 [Docker guide](https://github.com/jokob-sk/Pi.Alert/blob/main/dockerfiles/README.md) | 🆕 [Release notes](https://github.com/jokob-sk/Pi.Alert/releases) | 📚 [All Docs](https://github.com/jokob-sk/Pi.Alert/tree/main/docs)
| 🐳 [Docker hub](https://registry.hub.docker.com/r/jokobsk/pi.alert) | 📑 [Docker guide](https://github.com/jokob-sk/Pi.Alert/blob/main/dockerfiles/README.md) |🆕 [Release notes](https://github.com/jokob-sk/Pi.Alert/releases) | 📚 [All Docs](https://github.com/jokob-sk/Pi.Alert/tree/main/docs) |
|----------------------|----------------------| ----------------------| ----------------------|
## 🔍 Scan Methods
The system continuously scans the network for, **New devices**, **New connections** (re-connections), **Disconnections**, **"Always Connected" devices down**, Devices **IP changes** and **Internet IP address changes**. Discovery & scan methods include:
- **arp-scan**. The arp-scan system utility is used to search for devices on the network using arp frames.
- **Pi-hole - DB import**. The PiHole database is used as a source for events for devices
- **Pi-hole - DHCP leases**. Import of devices from the PiHole dhcp.leases file
- **Generic DHCP leases**. Import of devices from the generic dhcp.leases file
- **UNIFI import**. Import of devices from the UNIFI controller
- **SNMP-enabled router import**. Import of devices from an SNMP-enabled router
## Why PiAlert❓ Isn't this scary 👻...
## 🧩 Integrations
- [Apprise](https://hub.docker.com/r/caronc/apprise), [Pushsafer](https://www.pushsafer.com/), [NTFY](https://ntfy.sh/)
- [Webhooks](https://github.com/jokob-sk/Pi.Alert/blob/main/docs/WEBHOOK_N8N.md)
- [Home Assistant](https://github.com/jokob-sk/Pi.Alert/blob/main/docs/HOME_ASSISTANT.md)
- [API endpoint](https://github.com/jokob-sk/Pi.Alert/blob/main/docs/API.md)
- [Plugin system](https://github.com/jokob-sk/Pi.Alert/tree/main/front/plugins) for custom scripts monitoring and framework for extending the app
...most of us don't know what's going on on our home network, but we want our family and data to _be safe_. _Command-line tools_ are great, but the output can be _hard to understand_ and action if you are not a network specialist 😖.
# 📥 Installation
<!--- --------------------------------------------------------------------- --->
PiAlert gives you peace of mind. _Visualize and immediately report 📬_ what is going on in your network - this is the first step to enhance your _network security 🔐_.
⚠ Only tested as a [docker container - follow the guide here](dockerfiles/README.md).
> Check out [leiweibau's fork](https://github.com/leiweibau/Pi.Alert/) if you want to install Pi.Alert on the server directly or check instructions for [pucherot's original code](https://github.com/pucherot/Pi.Alert/)
_PiAlert combines several network and other scanning tools 🔍 with notifications 📧 into one user-friendly package 📦_. You get an overview of network device Sessions, Connected devices, Favorites, Events, Presence, Down alerts, and IPs. You can schedule Nmap scans to detect changes in device ports and visualize your Network topology (even with undetectable, dummy devices).
# 📑 Features
- Display:
- Sessions, Connected devices, Favorites, Events, Presence, Concurrent devices, Down alerts, IP's
- Manual Nmap scans, Optional speedtest for Device "Internet"
- Simple Network relationship display
- Maintenance tasks and Settings like:
- Theme Selection (blue, red, green, yellow, black, purple) and Light/Dark-Mode Switch
- DB maintenance, Backup, Restore tools and CSV Export / Import
- Simple login Support
- 🌟[Plugin system](https://github.com/jokob-sk/Pi.Alert/tree/main/front/plugins)
- Create custom plugins with automatically generated settings and UI.
- Monitor anything for changes
- Check the [instructions](https://github.com/jokob-sk/Pi.Alert/tree/main/front/plugins) carefully if you are up for a challenge! Current plugins include:
- Detecting Rogue DHCP servers via NMAP
- Monitoring HTTP status changes of domains/URLs
- Import devices from DHCP.leases files, a UniFi controller, or an SNMP enabled router
- Creation of dummy devices to visualize your [network map](https://github.com/jokob-sk/Pi.Alert/blob/main/docs/NETWORK_TREE.md)
Setup a _kill switch ☠_ for your network via a smart plug with the available [Home Assistant](https://github.com/jokob-sk/Pi.Alert/blob/main/docs/HOME_ASSISTANT.md) integration. Implement custom automations with the [CSV device Exports 📤](https://github.com/jokob-sk/Pi.Alert/tree/main/front/plugins/csv_backup), [Webhooks](https://github.com/jokob-sk/Pi.Alert/blob/main/docs/WEBHOOK_N8N.md), or [API endpoints](https://github.com/jokob-sk/Pi.Alert/blob/main/docs/API.md) features.
| ![Screen 1][screen1] | ![Screen 2][screen2] | ![Screen 5][screen5] |
Extend the app if you want to create your own scanner and handle the results and notifications in PiAlert. Check available [Plugins & Instructions](https://github.com/jokob-sk/Pi.Alert/tree/main/front/plugins). Looking forward to your contributions if you decide to share your work with the community ❤.
| ![Main screen][main] | ![Screen 1][screen1] | ![Screen 5][screen5] |
|----------------------|----------------------| ----------------------|
| ![Screen 3][screen3] | ![Screen 4][screen4] | ![Screen 6][screen6] |
| ![Screen 8][screen8] | ![Report 2][report2] | ![Screen 9][screen9] |
## Scan Methods, Notifications, Integration, Extension system
### 🔗 Other Alternatives
| Features | Details |
|-------------|-------------|
| 🔍 | The app scans your network for, **New devices**, **New connections** (re-connections), **Disconnections**, **"Always Connected" devices down**, Devices **IP changes** and **Internet IP address changes**. Discovery & scan methods include: **arp-scan**. **Pi-hole - DB import**, **Pi-hole - DHCP leases import**, **Generic DHCP leases import**. **UNIFI controller import**, **SNMP-enabled router import**. Check the [Plugins](https://github.com/jokob-sk/Pi.Alert/tree/main/front/plugins) docs for more info on individual scans. |
|📧 | Send notifications to more than 80+ services, including Telegram via [Apprise](https://hub.docker.com/r/caronc/apprise), or use [Pushsafer](https://www.pushsafer.com/), or [NTFY](https://ntfy.sh/). |
|🧩 | Feed your data and device changes into [Home Assistant](https://github.com/jokob-sk/Pi.Alert/blob/main/docs/HOME_ASSISTANT.md), read [API endpoints](https://github.com/jokob-sk/Pi.Alert/blob/main/docs/API.md), or use [Webhooks](https://github.com/jokob-sk/Pi.Alert/blob/main/docs/WEBHOOK_N8N.md) to setup custom automation flows. |
| | Build your own scanners with the [Plugin system](https://github.com/jokob-sk/Pi.Alert/tree/main/front/plugins) |
- [WatchYourLAN](https://github.com/aceberg/WatchYourLAN) - Lightweight network IP scanner with web GUI (Open source)
- [Fing](https://www.fing.com/) - Network scanner app for your Internet security (Commercial, Phone App, Proprietary hardware)
### 📚 Documentation
- Initial Docker Setup: [Docker instructions](https://github.com/jokob-sk/Pi.Alert/blob/main/dockerfiles/README.md)
- App Usage and Configuration: [All Documentation](https://github.com/jokob-sk/Pi.Alert/blob/main/docs/README.md)
## Installation & Documentation
<!--- --------------------------------------------------------------------- --->
| Docs | Link |
|-------------|-------------|
| 📥🐳 | [Docker instructions](https://github.com/jokob-sk/Pi.Alert/blob/main/dockerfiles/README.md)
| 📥💻 | [HW install (experimental 🧪)](https://github.com/jokob-sk/Pi.Alert/blob/main/docs/HW_INSTALL.md) |
| 📚 | [All Documentation](https://github.com/jokob-sk/Pi.Alert/blob/main/docs/README.md) (App Usage and Configuration) |
> Other Alternatives
>
> - Check out [leiweibau's on HW installed fork](https://github.com/leiweibau/Pi.Alert/) (maintained)
> - Check instructions for [pucherot's original code](https://github.com/pucherot/Pi.Alert/) (unmaintained)
> - [WatchYourLAN](https://github.com/aceberg/WatchYourLAN) - Lightweight network IP scanner with web GUI (Open source)
> - [Fing](https://www.fing.com/) - Network scanner app for your Internet security (Commercial, Phone App, Proprietary hardware)
## ❤ Support me
Get:
- Regular updates to keep your data and family safe 🔄
- Better and more functionality
- I don't get burned out and the app survives longer🔥🤯
- Quicker and better support with issues 🆘
- Less grumpy me 😄
| [![GitHub](https://i.imgur.com/emsRCPh.png)](https://github.com/sponsors/jokob-sk) | [![Buy Me A Coffee](https://i.imgur.com/pIM6YXL.png)](https://www.buymeacoffee.com/jokobsk) | [![Patreon](https://i.imgur.com/MuYsrq1.png)](https://www.patreon.com/user?u=84385063) |
| --- | --- | --- |
- Bitcoin: `1N8tupjeCK12qRVU2XrV17WvKK7LCawyZM`
- Ethereum: `0x6e2749Cb42F4411bc98501406BdcD82244e3f9C7`
> 📧 Email me at [jokob@duck.com](mailto:jokob@duck.com?subject=PiAlert) if you want to get in touch or if I should add other sponsorship platforms.
## Everything else
<!--- --------------------------------------------------------------------- --->
### License
GPL 3.0 | [Read more here](LICENSE.txt) | Source of the [animated GIF (Loading Animation)](https://commons.wikimedia.org/wiki/File:Loading_Animation.gif) | Source of the [selfhosted Fonts](https://github.com/adobe-fonts/source-sans)
> GPL 3.0 | [Read more here](LICENSE.txt) | Source of the [animated GIF (Loading Animation)](https://commons.wikimedia.org/wiki/File:Loading_Animation.gif) | Source of the [selfhosted Fonts](https://github.com/adobe-fonts/source-sans)
### 🥇 Special thanks
### Special thanks
This code is a collaborative body of work, with special thanks to:
This code is a collaborative body of work, with special thanks to:
- 🏆 [pucherot/Pi.Alert](https://github.com/pucherot/Pi.Alert) is the original creator od PiAlert
- [leiweibau](https://github.com/leiweibau/Pi.Alert): Dark mode (and much more)
- [Macleykun](https://github.com/Macleykun): Help with Dockerfile clean-up
- [Final-Hawk](https://github.com/Final-Hawk): Help with NTFY, styling and other fixes
- [TeroRERO](https://github.com/terorero): Spanish translation
- [Data-Monkey](https://github.com/Data-Monkey): Split-up of the python.py file and more
- Please see the [Git contributors](https://github.com/jokob-sk/Pi.Alert/graphs/contributors) for a full list of people and their contributions to the project
## ☕ Support me
<a href="https://github.com/sponsors/jokob-sk" target="_blank"><img src="https://i.imgur.com/X6p5ACK.png" alt="Sponsor Me on GitHub" style="height: 30px !important;width: 117px !important;" width="150px" ></a>
<a href="https://www.buymeacoffee.com/jokobsk" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 30px !important;width: 117px !important;" width="117px" height="30px" ></a>
<a href="https://www.patreon.com/user?u=84385063" target="_blank"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/8/82/Patreon_logo_with_wordmark.svg/512px-Patreon_logo_with_wordmark.svg.png" alt="Support me on patreon" style="height: 30px !important;width: 117px !important;" width="117px" ></a>
BTC: 1N8tupjeCK12qRVU2XrV17WvKK7LCawyZM
> [pucherot/Pi.Alert](https://github.com/pucherot/Pi.Alert) (the original creator of PiAlert), [leiweibau](https://github.com/leiweibau/Pi.Alert): Dark mode (and much more), [Macleykun](https://github.com/Macleykun) (Help with Dockerfile clean-up) [Final-Hawk](https://github.com/Final-Hawk) (Help with NTFY, styling and other fixes), [TeroRERO](https://github.com/terorero) (Spanish translations), [Data-Monkey](https://github.com/Data-Monkey), (Split-up of the python.py file and more), [cvc90](https://github.com/cvc90) (Spanish translation and various UI work) to name a few...
>
> Please see the [Git contributors](https://github.com/jokob-sk/Pi.Alert/graphs/contributors) for a full list of people and their contributions to the project
<!--- --------------------------------------------------------------------- --->
[main]: ./docs/img/devices_split.png "Main screen"

View File

@@ -38,7 +38,7 @@
<tr>
<td height=200 valign=top style="padding: 10px">
<INTERNET_TABLE>
<NEW_DEVICES_TABLE>
<DOWN_DEVICES_TABLE>
<EVENTS_TABLE>

View File

@@ -4,5 +4,4 @@ Server: <SERVER_NAME>
<SECTION_NEW_DEVICES>
<SECTION_DEVICES_DOWN>
<SECTION_EVENTS>
<SECTION_INTERNET>
<PLUGINS_TABLE>

View File

@@ -43,7 +43,6 @@
<tr>
<td height=200 valign=top style="padding: 10px">
<INTERNET_TABLE>
<NEW_DEVICES_TABLE>
<DOWN_DEVICES_TABLE>
<EVENTS_TABLE>

View File

@@ -15,13 +15,13 @@
#
# Scan multiple interfaces (eth1 and eth0):
# SCAN_SUBNETS = [ '192.168.1.0/24 --interface=eth1', '192.168.1.0/24 --interface=eth0' ]
SCAN_SUBNETS=['192.168.1.0/24 --interface=eth1']
PRINT_LOG=False
TIMEZONE='Europe/Berlin'
PIALERT_WEB_PROTECTION=False
PIALERT_WEB_PASSWORD='8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92'
INCLUDED_SECTIONS=['internet','new_devices','down_devices','events']
SCAN_CYCLE_MINUTES=5
DAYS_TO_KEEP_EVENTS=90
# Used for generating links in emails. Make sure not to add a trailing slash!
REPORT_DASHBOARD_URL='http://pi.alert'
@@ -93,25 +93,6 @@ DDNS_PASSWORD='A0000000B0000000C0000000D0000000'
DDNS_UPDATE_URL='https://api.dynu.com/nic/update?'
# PiHole
#---------------------------
# if enabled you need to map '/etc/pihole/pihole-FTL.db' in docker-compose.yml
PIHOLE_ACTIVE=False
# if enabled you need to map '/etc/pihole/dhcp.leases' in docker-compose.yml
DHCP_ACTIVE=False
# Pholus
#---------------------------
PHOLUS_ACTIVE=False
PHOLUS_TIMEOUT=120
PHOLUS_FORCE=False
PHOLUS_DAYS_DATA=7
PHOLUS_RUN='once'
PHOLUS_RUN_TIMEOUT=600
PHOLUS_RUN_SCHD='0 4 * * *'
#-------------------IMPORTANT INFO-------------------#
# This file is ingested by a python script, so if #
# modified it needs to use python syntax #

View File

@@ -4,9 +4,10 @@
[![Docker Pulls](https://img.shields.io/docker/pulls/jokobsk/pi.alert?label=Pulls&logo=docker&color=0aa8d2&logoColor=fff)](https://hub.docker.com/r/jokobsk/pi.alert)
[![Docker Pushed](https://img.shields.io/badge/dynamic/json?color=0aa8d2&logoColor=fff&label=Pushed&query=last_updated&url=https%3A%2F%2Fhub.docker.com%2Fv2%2Frepositories%2Fjokobsk%2Fpi.alert%2F&logo=docker&link=http://left&link=https://hub.docker.com/repository/docker/jokobsk/pi.alert)](https://hub.docker.com/r/jokobsk/pi.alert)
# 🐳 A docker image for Pi.Alert
# PiAlert 💻🔍 Network security scanner
🐳 [Docker hub](https://registry.hub.docker.com/r/jokobsk/pi.alert) | 📑 [Docker instructions](https://github.com/jokob-sk/Pi.Alert/blob/main/dockerfiles/README.md) | 🆕 [Release notes](https://github.com/jokob-sk/Pi.Alert/releases) | 📚 [All Docs](https://github.com/jokob-sk/Pi.Alert/tree/main/docs)
| 🐳 [Docker hub](https://registry.hub.docker.com/r/jokobsk/pi.alert) | 📑 [Docker guide](https://github.com/jokob-sk/Pi.Alert/blob/main/dockerfiles/README.md) |🆕 [Release notes](https://github.com/jokob-sk/Pi.Alert/releases) | 📚 [All Docs](https://github.com/jokob-sk/Pi.Alert/tree/main/docs) |
|----------------------|----------------------| ----------------------| ----------------------|
<a href="https://raw.githubusercontent.com/jokob-sk/Pi.Alert/main/docs/img/devices_split.png" target="_blank">
<img src="https://raw.githubusercontent.com/jokob-sk/Pi.Alert/main/docs/img/devices_split.png" width="300px" />
@@ -15,7 +16,6 @@
<img src="https://raw.githubusercontent.com/jokob-sk/Pi.Alert/main/docs/img/network.png" width="300px" />
</a>
## 📕 Basic Usage
- You will have to run the container on the host network, e.g:
@@ -79,6 +79,7 @@ There are 2 approaches how to get PiHole devices imported. Via the PiHole import
* `DHCPLSS_RUN`: You need to map `:/etc/pihole/dhcp.leases` in the `docker-compose.yml` file if you enable this setting.
* The above setting has to be matched with a corresponding `DHCPLSS_paths_to_check` setting entry (the path in the container must contain `pihole` as PiHole uses a different format of the `dhcp.leases` file).
> [!NOTE]
> It's recommended to use the same schedule interval for all plugins responsible for discovering new devices.
### **Common issues**
@@ -217,10 +218,19 @@ Big thanks to <a href="https://github.com/Macleykun">@Macleykun</a> for help and
<img src="https://avatars.githubusercontent.com/u/26381427?size=50">
</a>
## Support me
## Support me
<a href="https://github.com/sponsors/jokob-sk" target="_blank"><img src="https://i.imgur.com/X6p5ACK.png" alt="Sponsor Me on GitHub" style="height: 30px !important;width: 117px !important;" width="150px" ></a>
<a href="https://www.buymeacoffee.com/jokobsk" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 30px !important;width: 117px !important;" width="117px" height="30px" ></a>
<a href="https://www.patreon.com/user?u=84385063" target="_blank"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/8/82/Patreon_logo_with_wordmark.svg/512px-Patreon_logo_with_wordmark.svg.png" alt="Support me on patreon" style="height: 30px !important;width: 117px !important;" width="117px" ></a>
Get:
- Regular updates to keep your data and family safe 🔄
- Better and more functionality
- I don't get burned out and the app survives longer🔥🤯
- Quicker and better support with issues 🆘
- Less grumpy me 😄
BTC: 1N8tupjeCK12qRVU2XrV17WvKK7LCawyZM
| [![GitHub](https://i.imgur.com/emsRCPh.png)](https://github.com/sponsors/jokob-sk) | [![Buy Me A Coffee](https://i.imgur.com/pIM6YXL.png)](https://www.buymeacoffee.com/jokobsk) | [![Patreon](https://i.imgur.com/MuYsrq1.png)](https://www.patreon.com/user?u=84385063) |
| --- | --- | --- |
- Bitcoin: `1N8tupjeCK12qRVU2XrV17WvKK7LCawyZM`
- Ethereum: `0x6e2749Cb42F4411bc98501406BdcD82244e3f9C7`
> 📧 Email me at [jokob@duck.com](mailto:jokob@duck.com?subject=PiAlert) if you want to get in touch or if I should add other sponsorship platforms.

View File

@@ -28,6 +28,7 @@ You can access the following files:
| `language_strings.json` | The content of the language_strings table, which in turn is loaded from the plugins `config.json` definitions. |
| `table_custom_endpoint.json` | A custom endpoint generated by the SQL query specified by the `API_CUSTOM_SQL` setting. |
| `table_settings.json` | The content of the settings table. |
| `app_state.json` | Contains the current application state. |
Current/latest state of the aforementioned files depends on your settings.

24
docs/DEVICES_BULK_EDITING.md Executable file
View File

@@ -0,0 +1,24 @@
# 📝Bulk-edit devices via CSV Export/Import
> [!NOTE]
> As always, backup everything, just in case.
1. In `Maintenance` > `Backup / Restore` click the `CSV Export` button.
2. A `devices.csv` is generated in the `/config` folder
3. Edit the `devices.csv` file however you like.
![Maintenance > CSV Export](/docs/img/DEVICES_BULK_EDITING/MAINTENANCE_CSV_EXPORT.png)
> [!NOTE]
> The file containing a list of Devices including the Network relationships between Network Nodes and connected devices. You can also trigger this by acessing this URL: `<your pialert url>/php/server/devices.php?action=ExportCSV` or via the `CSV Backup` plugin. (💡 You can schedule this)
![Settings > CSV Backup](/docs/img/DEVICES_BULK_EDITING/CSV_BACKUP_SETTINGS.png)
> [!NOTE]
> Keep Linux line endings (sugegsted editors: Nano, Notepad++)
![Nodepad++ line endings](/docs/img/DEVICES_BULK_EDITING/NOTEPAD++.png)

View File

@@ -9,6 +9,11 @@ To edit device information:
- Press the "Save" button
> [!NOTE]
>
> [Bulk-edit devices](/docs/DEVICES_BULK_EDITING.md) by using the `CSV Export` functionality in the `Maintenance` section.
![Device Details][screen1]

25
docs/HW_INSTALL.md Normal file
View File

@@ -0,0 +1,25 @@
# How to install PiAlert on the server hardware
To download and install PiAlert on the hardware/server directly use `curl` or `wget` commands.
> [!NOTE]
> This is an Experimental feature 🧪 and it relies on community support.
PiAlert will be installed in `home/pi/pialert/` and run on port number `20211`.
## CURL
```bash
curl -o install.sh https://raw.githubusercontent.com/jokob-sk/Pi.Alert/main/install/install.sh && sudo chmod +x install.sh && sudo ./install.sh
```
## WGET
```bash
wget https://raw.githubusercontent.com/jokob-sk/Pi.Alert/main/install/install.sh -O install.sh && sudo chmod +x install.sh && sudo ./install.sh
```
These commands will download the `install.sh` script from the GitHub repository, make it executable with `chmod`, and then run it using `./install.sh`.
Make sure you have the necessary permissions to execute the script.

View File

@@ -16,11 +16,15 @@ Make sure you have a root device with the MAC `Internet` (No other MAC addresses
* If port is empty or 0 a wifi icon is rendered, otherwise a ethernet port icon
> [!NOTE]
>
> [Bulk-edit devices](/docs/DEVICES_BULK_EDITING.md) by using the `CSV Export` functionality in the `Maintenance` section. You can use this to fix `Internet` node assignment issues.
## 🔍Detailed example:
In this example you will setup a device named `rapberrypi` as a `Switch` in our network.
### 1) Device details page
### 1. Device details page
- Go to the `Devices` (1) page:
@@ -29,13 +33,13 @@ In this example you will setup a device named `rapberrypi` as a `Switch` in our
- In the (2) `Details` tab navigate to the the `Type` (3) dropdown and select the type `Switch` (4).
> Note: Only the following device types will show up as selectable Network nodes ( = devices you can connect other devices to):
> AP, Firewall, Gateway, Hypervisor, PLC, Powerline, Router, Switch, USB LAN Adapter, USB WIFI Adapter and WLAN.
> AP, Firewall, Gateway, Hypervisor, PLC, Powerline, Router, Switch, USB LAN Adapter, USB WIFI Adapter and WLAN. Custom types can be added via the `NETWORK_DEVICE_TYPES` setting.
- Assign a device to your root device from the `Node` (5) dropdown which has the MAC `Internet` (6) (Your name may differ, but the MAC needs to be set to `Internet` - this is done by default).
- Save your changes (7)
### 1) Network page
### 2. Network page
- Navigate to your `Network` (1) page:
@@ -45,7 +49,7 @@ In this example you will setup a device named `rapberrypi` as a `Switch` in our
- As we asssigned the `raspberrypi` in the previous 1) Device details page section to the `Internet` parent network node in step (6), the link is also showing up in the tree diagram (4)
- We can now assign the device `(AppleTV)` (5) to this `raspberrypi` node, representing a network Switch in this example
### 1) Network page with 2 levels
### 3. Network page with 2 levels
- After clicking the `Assign` button in the previous section, the `(AppleTV)` (1) device is now connected to our `raspberrypi` (2).
@@ -57,3 +61,4 @@ In this example you will setup a device named `rapberrypi` as a `Switch` in our

View File

@@ -22,9 +22,10 @@ There is also an in-app Help / FAQ section that should be answering frequently a
#### 🔝 Popular/Suggested
- [Network treemap configuration](/docs/NETWORK_TREE.md)
- [Gmail as SMTP server for sending emails](/docs/SMTP_GMAIL.md)
- [SMTP server config](/docs/SMTP.md)
- [Subnets and VLANs configuration for arp-scan](/docs/SUBNETS.md)
- [Home Assistant](/docs/HOME_ASSISTANT.md)
- [Bulk edit devices](/docs/DEVICES_BULK_EDITING.md)
#### ⚙ System Management
@@ -50,6 +51,7 @@ There is also an in-app Help / FAQ section that should be answering frequently a
- [Settings system](/docs/SETTINGS_SYSTEM.md)
- [New Version notifications](/docs/VERSIONS.md)
- [Frontend development tips](/docs/FRONTEND_DEVELOPMENT.md)
- [Webhook secrets](/docs/WEBHOOK_SECRET.md)
Feel free to suggest or submit new docs via a PR.

41
docs/SMTP.md Executable file
View File

@@ -0,0 +1,41 @@
# 📧 SMTP guides
## Using the GMX SMTP server
1. Go to your GMX account https://account.gmx.com
2. Under Security Options enable 2FA (Two-factor authentication)
3. Under Security Options generate an Application-specific password
4. Home -> Email Settings -> POP3 & IMAP -> Enable access to this account via POP3 and IMAP
5. In PiAlert specify these settings:
```python
REPORT_MAIL=True
SMTP_SERVER='mail.gmx.com'
SMTP_PORT=465
SMTP_USER='gmx_email@gmx.com'
SMTP_PASS='<your Application-specific password>'
SMTP_SKIP_TLS=True
SMTP_FORCE_SSL=True
SMTP_SKIP_LOGIN=False
REPORT_FROM='gmx_email@gmx.com' # this has to be the same email as in SMTP_USER
REPORT_TO='some_target_email@gmail.com'
```
## Using the Gmail SMTP server
1. 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](https://support.google.com/accounts/answer/185833)
2. Specify the following settings:
```python
REPORT_MAIL=True
SMTP_SKIP_TLS=True
SMTP_FORCE_SSL=True
SMTP_PORT=465
SMTP_SERVER='smtp.gmail.com'
SMTP_PASS='16-digit passcode from google'
REPORT_TO='some_target_email@gmail.com'
```

View File

@@ -1,15 +0,0 @@
## Use the Gmail SMTP server
1) 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](https://support.google.com/accounts/answer/185833)
2) Specify the following settings:
```python
SMTP_SKIP_TLS=True
SMTP_FORCE_SSL=True
SMTP_PORT=465
SMTP_SERVER='smtp.gmail.com'
SMTP_PASS='16-digit passcode from google'
```

38
docs/WEBHOOK_SECRET.md Executable file
View File

@@ -0,0 +1,38 @@
# Webhook Secrets
## How does the signing work?
Pi.Alert will use the configured secret to create a hash signature of the request body. This SHA256-HMAC signature will appear in the `X-Webhook-Signature` header of each request to the webhook target URL. You can use the value of this header to validate the request was sent by Pi.Alert.
## Activating webhook signatures
All you need to do in order to add a signature to the request headers is to set the `WEBHOOK_SECRET` config value to a non-empty string.
## Validating webhook deliveries
There are a few things to keep in mind when validating the webhook delivery:
- Pi.Alert uses an HMAC hex digest to compute the hash
- The signature in the `X-Webhook-Signature` header always starts with `sha256=`
- The hash signature is generated using the configured `WEBHOOK_SECRET` and the request body.
- Never use a plain `==` operator. Instead, consider using a method like [`secure_compare`](https://www.rubydoc.info/gems/rack/Rack%2FUtils:secure_compare) or [`crypto.timingSafeEqual`](https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b), which performs a "constant time" string comparison to help mitigate certain timing attacks against regular equality operators, or regular loops in JIT-optimized languages.
## Testing the webhook payload validation
You can use the following secret and payload to verify that your implementation is working correctly.
`secret`: 'this is my secret'
`payload`: '{"test":"this is a test body"}'
If your implementation is correct, the signature you generated should match the following:
`signature`: bed21fcc34f98e94fd71c7edb75e51a544b4a3b38b069ebaaeb19bf4be8147e9
`X-Webhook-Signature`: sha256=bed21fcc34f98e94fd71c7edb75e51a544b4a3b38b069ebaaeb19bf4be8147e9
## More information
If you want to learn more about webhook security, take a look at [GitHub's webhook documentation](https://docs.github.com/en/webhooks/about-webhooks).
You can find examples for validating a webhook delivery [here](https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries#examples).

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -459,35 +459,7 @@
border-right: 5px solid #606060;
}
/* -----------------------------------------------------------------------------
Spin
----------------------------------------------------------------------------- */
.pa_semitransparent-panel {
position: absolute;
width: 100%; /*calc (100% -40px);*/
height: 100%;
left: 0;
top: 0;
display: block;
opacity: 0.8;
background-color: #fff;
z-index: 99;
}
.pa_spinner {
position: absolute;
left: 0;
right: 0;
top: 20px;
margin-left: auto;
margin-right: auto;
padding: 15px;
width: 200px;
background-color: #fff;
z-index: 100;
}
/* -----------------------------------------------------------------------------
Notification float banner
@@ -934,3 +906,54 @@ input[readonly] {
display: none;
}
}
/* -----------------------------------------------------------------------------
Spin
----------------------------------------------------------------------------- */
.pa_semitransparent-panel {
position: absolute;
width: 100%; /*calc (100% -40px);*/
height: 100%;
left: 0;
top: 0;
display: block;
opacity: 0.8;
background-color: #fff;
z-index: 99;
}
.pa_spinner {
position: fixed;
left: 0;
right: 0;
top: 100px;
margin-left: auto;
margin-right: auto;
padding: 15px;
width: 200px;
background-color: #fff;
z-index: 100;
}
#loadingSpinner
{
z-index: 100;
}
/* -----------------------------------------------------------------------------
Donations
----------------------------------------------------------------------------- */
.donations .box
{
padding:15px;
margin-bottom: 0px;
}
.donations .box-header
{
color:15px;
}
.donations h3
{
margin-top: 10px;
}

View File

@@ -542,14 +542,6 @@
<!-- tab page 3 ------------------------------------------------------------ -->
<div class="tab-pane fade table-responsive" id="panPresence">
<!-- spinner -->
<div id="loading" style="display: none">
<div class="pa_semitransparent-panel"></div>
<div class="panel panel-default pa_spinner">
<table><td width="130px" align="middle"><?= lang("DevDetail_Loading");?></td><td><i class="ion ion-ios-loop-strong fa-spin fa-2x fa-fw"></td></table>
</div>
</div>
<!-- Calendar -->
<div id="calendar">
</div>
@@ -1136,9 +1128,9 @@ function initializeCalendar () {
loading: function( isLoading, view ) {
if (isLoading) {
$('#loading').show();
showSpinner()
} else {
$('#loading').hide();
hideSpinner()
}
}

View File

@@ -209,6 +209,8 @@
// -----------------------------------------------------------------------------
function main () {
handleLoadingDialog()
// get from cookie if available (need to use decodeURI as saved as part of URI in PHP)
cookieColumnsVisibleStr = decodeURI(getCookie("Front_Devices_Columns_Visible")).replaceAll('%2C',',')
@@ -542,6 +544,26 @@ function getDevicesList (status) {
'php/server/devices.php?action=getDevicesList&status=' + deviceStatus).load();
};
function handleLoadingDialog()
{
$.get('api/app_state.json?nocache=' + Date.now(), function(appState) {
console.log(appState["showSpinner"])
if(appState["showSpinner"])
{
showSpinner("settings_old")
setTimeout("handleLoadingDialog()", 1000);
} else
{
hideSpinner()
}
})
}
</script>
<script src="js/pialert_common.js"></script>

View File

@@ -2,21 +2,69 @@
require 'php/templates/header.php';
?>
<script src="js/pialert_common.js"></script>
<div id="donationsPage" class="content-wrapper">
<!-- Content header--------------------------------------------------------- -->
<section class="content-header">
<h1 id="pageTitle">
<i class="fa fa-heart"></i>
</h1>
</section>
<!-- Main content ---------------------------------------------------------- -->
<section class="content donations">
<div id="donationsText" class="box box-solid"></div>
<div class="content-header">
<h3 class="box-title " id="donationsPlatforms"></h3>
</div>
<div class="box box-solid">
<div class="box-body">
<div class="col-sm-2">
<a target="_blank" href="https://github.com/sponsors/jokob-sk">
<img alt="Sponsor Me on GitHub" src="https://i.imgur.com/X6p5ACK.png" width="150px">
</a>
</div>
<div class="col-sm-2">
<a target="_blank" href="https://www.buymeacoffee.com/jokobsk">
<img alt="Buy Me A Coffee" src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" width="117px" height="30px">
</a>
</div>
<div class="col-sm-2">
<a target="_blank" href="https://www.patreon.com/user?u=84385063">
<img alt="Support me on patreon" src="https://upload.wikimedia.org/wikipedia/commons/thumb/8/82/Patreon_logo_with_wordmark.svg/512px-Patreon_logo_with_wordmark.svg.png" width="117px">
</a>
</div>
</div>
</div>
<div class="content-header">
<h3 class="box-title " id="donationsOthers"></h3>
</div>
<div class="box box-solid">
<div class="box-body">
<div class="col-sm-12">
<ul>
<li>Bitcoin: <code>1N8tupjeCK12qRVU2XrV17WvKK7LCawyZM</code></li>
<li>Ethereum: <code>0x6e2749Cb42F4411bc98501406BdcD82244e3f9C7</code></li>
</ul>
</div>
</div>
</div>
<div>
</section>
<div id="settingsPage" class="content-wrapper">
<p>
<a target="_blank" href="https://github.com/sponsors/jokob-sk">
<img alt="Sponsor Me on GitHub" src="https://i.imgur.com/X6p5ACK.png" width="150px">
</a>
<a target="_blank" href="https://www.buymeacoffee.com/jokobsk">
<img alt="Buy Me A Coffee" src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" width="117px" height="30px">
</a>
<a target="_blank" href="https://www.patreon.com/user?u=84385063">
<img alt="Support me on patreon" src="https://upload.wikimedia.org/wikipedia/commons/thumb/8/82/Patreon_logo_with_wordmark.svg/512px-Patreon_logo_with_wordmark.svg.png" width="117px">
</a>
</p>
<p>
BTC: 1N8tupjeCK12qRVU2XrV17WvKK7LCawyZM
</p>
</div>
</div> <!-- End of class="content-wrapper" -->
<script>
function init()
{
$("#donationsText").html(getString("Donations_Text"))
$("#pageTitle").append(getString("Donations_Title"))
$("#donationsPlatforms").append(getString("Donations_Platforms"))
$("#donationsOthers").append(getString("Donations_Others"))
}
init();
</script>
<?php
require 'php/templates/footer.php';
?>

View File

@@ -1,80 +1,51 @@
function handleVersion(){
//--------------------------------------------------------------
// Handle the UI changes to show or hide notifications about a new version
function versionUpdateUI(){
release_timestamp = getCookie("release_timestamp")
isNewVersion = getCookie("isNewVersion")
if(release_timestamp != "")
{
console.log(isNewVersion)
build_timestamp = parseInt($('#version').attr("data-build-time").match( /\d+/g ).join(''))
// if the release_timestamp is older by 10 min or more as the build timestamp then there is a new release available
if(release_timestamp > build_timestamp + 600 )
{
console.log("New release!")
// handling the navigation menu icon
$('#version').attr("class", $('#version').attr("class").replace("myhidden", ""))
maintenanceDiv = $('#new-version-text')
}
else{
console.log("All up-to-date!")
maintenanceDiv = $('#current-version-text')
}
// handling the maintenance section message
if(emptyArr.includes(maintenanceDiv) == false && $(maintenanceDiv).length != 0)
{
$(maintenanceDiv).attr("class", $(maintenanceDiv).attr("class").replace("myhidden", ""))
}
}
}
//--------------------------------------------------------------
function getVersion()
// if the release_timestamp is older by 10 min or more as the build timestamp then there is a new release available
if(isNewVersion != "false")
{
release_timestamp = getCookie("release_timestamp")
console.log("New release!")
// handling the navigation menu icon
$('#version').attr("class", $('#version').attr("class").replace("myhidden", ""))
release_timestampNum = Number(release_timestamp)
maintenanceDiv = $('#new-version-text')
}
else{
console.log("All up-to-date!")
// logging
console.log(`Latest release in cookie: ${new Date(release_timestampNum*1000)}`)
// no cached value available
if(release_timestamp == "")
{
$.get('https://api.github.com/repos/jokob-sk/Pi.Alert/releases').done(function(response) {
// Handle successful response
var releases = response;
console.log(response)
if(releases.length > 0)
{
release_datetime = releases[0].published_at; // get latest release
release_timestamp = new Date(release_datetime).getTime() / 1000;
// cache value
setCookie("release_timestamp", release_timestamp, 30);
handleVersion();
}
}).fail(function(jqXHR, textStatus, errorThrown) {
$('.version').append(`<p>Github API: ${errorThrown} (${jqXHR.status}), ${jqXHR.responseJSON.message}</p>`)
});
} else
{
// cache is available, just call the handler
handleVersion()
}
maintenanceDiv = $('#current-version-text')
}
// handling the maintenance section message
if(emptyArr.includes(maintenanceDiv) == false && $(maintenanceDiv).length != 0)
{
$(maintenanceDiv).attr("class", $(maintenanceDiv).attr("class").replace("myhidden", ""))
}
}
//--------------------------------------------------------------
// Checks if a new version is available via the global app_state.json
function checkIfNewVersionAvailable()
{
$.get('api/app_state.json?nocache=' + Date.now(), function(appState) {
console.log(appState["isNewVersionChecked"])
console.log(appState["isNewVersion"])
// cache value
setCookie("isNewVersion", appState["isNewVersion"], 30);
setCookie("isNewVersionChecked", appState["isNewVersionChecked"], 30);
versionUpdateUI();
})
}
// handle the dispaly of the NEW icon
getVersion()
checkIfNewVersionAvailable()

View File

@@ -173,6 +173,7 @@ function cacheStrings()
}
// Get translated language string
function getString (key) {
UI_LANG = getSetting("UI_LANG");
@@ -196,11 +197,7 @@ function getString (key) {
if(isEmpty(result))
{
console.log(`pia_lang_${key}_${lang_code}`)
console.log(key)
result = getCache(`pia_lang_${key}_en_us`, true);
console.log(result)
}
return result;
@@ -537,7 +534,42 @@ function isEmpty(value)
return emptyArr.includes(value)
}
// -----------------------------------------------------------------------------
// Loading Spinner overlay
// -----------------------------------------------------------------------------
function showSpinner(stringKey='Loading')
{
if($("#loadingSpinner").length)
{
$("#loadingSpinner").show();
}
else{
html = `
<!-- spinner -->
<div id="loadingSpinner" style="display: block">
<div class="pa_semitransparent-panel"></div>
<div class="panel panel-default pa_spinner">
<table>
<td width="130px" align="middle">${getString(stringKey)}</td>
<td><i class="ion ion-ios-loop-strong fa-spin fa-2x fa-fw"></td>
</table>
</div>
</div>
`
$(".wrapper").append(html)
}
}
// -----------------------------------------------------------------------------
function hideSpinner()
{
$("#loadingSpinner").hide()
}
// -----------------------------------------------------------------------------
// initialize
// -----------------------------------------------------------------------------
cacheSettings()
cacheStrings()
initDeviceListAll_JSON()

View File

@@ -174,6 +174,12 @@ $db->close();
<?php echo date("Y-m-d", ((int)file_get_contents( "buildtimestamp.txt")));?>
</div>
</div>
<div class="db_info_table_row">
<div class="db_info_table_cell" style="min-width: 140px"><?= lang('Maintenance_Running_Version');?></div>
<div class="db_info_table_cell">
<?php include 'php/templates/version.php'; ?>
</div>
</div>
<div class="db_info_table_row">
<div class="db_info_table_cell" style="min-width: 140px"><?= lang('Maintenance_database_path');?></div>
<div class="db_info_table_cell">

View File

@@ -8,7 +8,6 @@
define('circle_online', '<div class="badge bg-green text-white" style="width: 10px; height: 10px; padding:2px; margin-top: -25px;">&nbsp;</div>');
define('circle_offline', '<div class="badge bg-red text-white" style="width: 10px; height: 10px; padding:2px; margin-top: -25px;">&nbsp;</div>');
$NETWORKTYPES = getNetworkTypes();
?>
<!-- Page ------------------------------------------------------------------ -->
@@ -268,6 +267,8 @@
// \
// PC (leaf) <------- leafs are not included in this SQL query
$networkDeviceTypes = str_replace("]", "",(str_replace("[", "", getSettingValue("NETWORK_DEVICE_TYPES"))));
$sql = "SELECT node_name, node_mac, online, node_type, node_ports_count, parent_mac, node_icon
FROM
(
@@ -278,7 +279,7 @@
a.dev_Network_Node_MAC_ADDR as parent_mac,
a.dev_Icon as node_icon
FROM Devices a
WHERE a.dev_DeviceType in ('AP', 'Gateway', 'Firewall', 'Hypervisor', 'Powerline', 'Switch', 'WLAN', 'PLC', 'Router','USB LAN Adapter', 'USB WIFI Adapter', 'Internet')
WHERE a.dev_DeviceType in (".$networkDeviceTypes.")
) t1
LEFT JOIN
(
@@ -766,10 +767,18 @@
// ---------------------------------------------------------------------------
function updateLeaf(leafMac,nodeMac)
{
console.log(leafMac)
console.log(nodeMac)
saveData('updateNetworkLeaf', leafMac, nodeMac);
setTimeout("location.reload();", 1000); // refresh page after 1s
console.log(leafMac) // child
console.log(nodeMac) // parent
// prevent the assignment of the Internet root node avoiding recursion when generating the network tree topology
if(leafMac.toLowerCase().includes('internet'))
{
showMessage(getString('Network_Cant_Assign'))
}
else{
saveData('updateNetworkLeaf', leafMac, nodeMac);
setTimeout("location.reload();", 500); // refresh page
}
}

View File

@@ -755,7 +755,12 @@ function getNetworkNodes() {
global $db;
// Device Data
$sql = 'SELECT * FROM Devices WHERE dev_DeviceType in ( "AP", "Gateway", "Firewall", "Hypervisor", "Powerline", "Switch", "WLAN", "PLC", "Router","USB LAN Adapter", "USB WIFI Adapter")';
$networkDeviceTypes = str_replace("]", "",(str_replace("[", "", getSettingValue("NETWORK_DEVICE_TYPES"))));
$sql = 'SELECT * FROM Devices WHERE dev_DeviceType in ( '. $networkDeviceTypes .' )';
// echo $sql;
$result = $db->query($sql);
@@ -1001,8 +1006,8 @@ function getLocations() {
// ----------------------------------------------------------------------------------------
function updateNetworkLeaf()
{
$nodeMac = $_REQUEST['value'];
$leafMac = $_REQUEST['id'];
$nodeMac = $_REQUEST['value']; // parent
$leafMac = $_REQUEST['id']; // child
if ((false === filter_var($nodeMac , FILTER_VALIDATE_MAC) && $nodeMac != "Internet" && $nodeMac != "") || false === filter_var($leafMac , FILTER_VALIDATE_MAC) ) {
throw new Exception('Invalid mac address');

View File

@@ -332,9 +332,7 @@ function saveSettings()
fwrite($newConfig, $txt);
fclose($newConfig);
displayMessage("<br/>Settings saved to the <code>".$config_file."</code> file.
<br/><br/>Backup of the previous ".$config_file." created here: <br/><br/><code>".$new_name."</code><br/><br/>
<b>Note:</b> Wait at least <b>5s</b> for the changes to reflect in the UI. (longer if for example a <a href='#state'>Scan is running</a>)",
displayMessage("<br/>Settings saved to the <code>pialert.conf</code> file.<br/><br/>A time-stamped backup of the previous file created. <br/><br/> Reloading...<br/>",
FALSE, TRUE, TRUE, TRUE);
}
@@ -351,6 +349,38 @@ function getString ($codeName, $default) {
return $default;
}
// -------------------------------------------------------------------------------------------
function getSettingValue($codeName) {
// Define the JSON endpoint URL
$url = dirname(__FILE__).'/../../../front/api/table_settings.json';
// Fetch the JSON data
$json = file_get_contents($url);
// Check if the JSON data was successfully fetched
if ($json === false) {
return 'Could not get json data';
}
// Decode the JSON data
$data = json_decode($json, true);
// Check if the JSON decoding was successful
if (json_last_error() !== JSON_ERROR_NONE) {
return 'Could not decode json data';
}
// Search for the setting by Code_Name
foreach ($data['data'] as $setting) {
if ($setting['Code_Name'] === $codeName) {
return $setting['Value'];
// echo $setting['Value'];
}
}
// Return false if the setting was not found
return 'Could not find setting '.$codeName;
}
// -------------------------------------------------------------------------------------------
@@ -421,16 +451,7 @@ function handleNull ($text, $default = "") {
}
// -------------------------------------------------------------------------------------------
// Currently unused - should be source of truth for network types (or define somewhere else?)
function getNetworkTypes(){
$array = array(
"AP", "Gateway", "Firewall", "Hypervisor", "Powerline", "Switch", "WLAN", "PLC", "Router","USB LAN Adapter", "USB WIFI Adapter"
);
return $array;
}
// -------------------------------------------------------------------------------------------
function getDevicesColumns(){

View File

@@ -27,9 +27,8 @@
<div class="pull-right no-hidden-xs">
<!-- Pi.Alert footer with url -->
<?php
echo '<a href="https://github.com/jokob-sk/Pi.Alert" target="_blank">Pi.Alert</a>';
?>
<a href="https://github.com/jokob-sk/Pi.Alert" target="_blank">Pi.Alert</a>
</div>
</footer>

View File

@@ -83,8 +83,13 @@ if ($ENABLED_DARKMODE === True) {
<script>
function updateState(){
getParam("state","Back_App_State", true)
setTimeout("updateState()", 5000);
$.get('api/app_state.json?nocache=' + Date.now(), function(appState) {
document.getElementById('state').innerHTML = appState["currentState"].replaceAll('"', '');
setTimeout("updateState()", 1000);
})
}
function show_pia_servertime() {
@@ -99,12 +104,6 @@ if ($ENABLED_DARKMODE === True) {
setTimeout("show_pia_servertime()", 1000);
}
// refresh page on focus - adds a lot of SQL queries overhead onto the DB - disabling for now
// document.addEventListener("visibilitychange",()=>{
// if(document.visibilityState==="visible"){
// window.location.href = window.location.href.split('#')[0];
// }
// })
</script>
@@ -262,9 +261,9 @@ if ($ENABLED_DARKMODE === True) {
<li class=" <?php if (in_array (basename($_SERVER['SCRIPT_NAME']), array('help_faq.php') ) ){ echo 'active'; } ?>">
<a href="help_faq.php"><i class="fa fa-question"></i> <span><?= lang('Navigation_HelpFAQ');?></span></a>
</li>
<!-- <li class=" <?php if (in_array (basename($_SERVER['SCRIPT_NAME']), array('donations.php') ) ){ echo 'active'; } ?>">
<li class=" <?php if (in_array (basename($_SERVER['SCRIPT_NAME']), array('donations.php') ) ){ echo 'active'; } ?>">
<a href="donations.php"><i class="fa fa-heart"></i> <span><?= lang('Navigation_Donations');?></span></a>
</li> -->
</li>
</ul>
<!-- /.sidebar-menu -->
@@ -277,26 +276,6 @@ if ($ENABLED_DARKMODE === True) {
//--------------------------------------------------------------
//--------------------------------------------------------------
function getParam(targetId, key, skipCache = false) {
skipCacheQuery = "";
if(skipCache)
{
skipCacheQuery = "&skipcache";
}
// get parameter value
$.get('php/server/parameters.php?action=get&defaultValue=NULL&parameter='+ key + skipCacheQuery, function(data) {
var result = data;
document.getElementById(targetId).innerHTML = result.replaceAll('"', '');
});
}
//--------------------------------------------------------------
function toggleFullscreen() {

View File

@@ -61,6 +61,7 @@
"Device_Table_nav_prev":"Zurück",
"Presence_Title":"Anwesenheit pro Gerät",
"Presence_Loading":"Laden...",
"Loading":"Laden...",
"Presence_Shortcut_AllDevices":"Alle Geräte",
"Presence_Shortcut_Connected":"Verbunden",
"Presence_Shortcut_Favorites":"Favoriten",

View File

@@ -84,6 +84,7 @@
"Device_Table_nav_prev" : "Previous",
"Presence_Title" : "Presence by Device",
"Presence_Loading" : "Loading...",
"Loading" : "Loading...",
"Presence_Shortcut_AllDevices" : "All Devices",
"Presence_Shortcut_Connected" : "Connected",
"Presence_Shortcut_Favorites" : "Favorites",
@@ -259,6 +260,7 @@
"Maintenance_new_version" : "🆕 A new version is available. Check out the <a href=\"https://github.com/jokob-sk/Pi.Alert/releases\" target=\"_blank\">release notes</a>.",
"Maintenance_current_version" : "You are up-to-date. Check out what <a href=\"https://github.com/jokob-sk/Pi.Alert/issues/138\" target=\"_blank\">I am working on</a>.",
"Maintenance_built_on" : "Built on",
"Maintenance_Running_Version" : "Installed version",
"Maintenance_database_path" : "Database-Path",
"Maintenance_database_size" : "Database-Size",
"Maintenance_database_rows" : "Table (Rows)",
@@ -406,6 +408,7 @@
"Network_Node" : "Network node",
"Network_Node_Name" : "Node name",
"Network_Parent" : "Parent network device",
"Network_Cant_Assign" : "Can't assign the root Internet node as a child leaf node.",
"Network_NoAssignedDevices" : "This network node does not have any assigned devices (leaf nodes). Assign one from bellow or go to the <b><i class=\"fa fa-info-circle\"></i> Details</b> tab of any device in <a href=\"devices.php\"><b> <i class=\"fa fa-laptop\"></i> Devices</b></a>, and assign it to a network <b><i class=\"fa fa-server\"></i> Node (MAC)</b> and <b><i class=\"fa fa-ethernet\"></i> Port</b> there.",
"HelpFAQ_Title" : "Help / FAQ",
"HelpFAQ_Cat_General" : "General",
@@ -425,7 +428,7 @@
"HelpFAQ_Cat_Device_200_head" : "I have devices in my list that I do not know about. After deleting them, they always reappear.",
"HelpFAQ_Cat_Device_200_text" : "If you use Pi-hole, please note that Pi.Alert retrieves information from Pi-hole. Pause Pi.Alert, go to the settings page in Pi-hole and delete the DHCP lease if necessary. Then, also in Pi-hole, look under Tools -> Network to see if you can find the recurring hosts there. If yes, delete them there as well. Now you can start Pi.Alert again. Now the device(s) should not show up anymore.",
"HelpFAQ_Cat_Detail_300_head" : "What means ",
"HelpFAQ_Cat_Detail_300_text_a" : "means a network device (a device of the type AP, Gateway, Firewall, Hypervisor, Powerline, Switch, WLAN, PLC, Router,USB LAN Adapter, USB WIFI Adapter, or Internet).",
"HelpFAQ_Cat_Detail_300_text_a" : "means a network device (a device of the type AP, Gateway, Firewall, Hypervisor, Powerline, Switch, WLAN, PLC, Router,USB LAN Adapter, USB WIFI Adapter, or Internet). Custom types can be added via the <code>NETWORK_DEVICE_TYPES</code> setting.",
"HelpFAQ_Cat_Detail_300_text_b" : "designates the port number where the currently edited device is connected to this network device. Read <a target=\"_blank\" href=\"https://github.com/jokob-sk/Pi.Alert/blob/main/docs/NETWORK_TREE.md\">this guide</a> for more info.",
"HelpFAQ_Cat_Detail_301_head_a" : "When is scanning now? At ",
"HelpFAQ_Cat_Detail_301_head_b" : " says 1min but the graph shows 5min intervals.",
@@ -457,9 +460,10 @@
"Plugins_Out_of" : "out of",
"Plugins_no_control" : "No form control was found to render this value.",
"Settings_Metadata_Toggle" : "Show/hide metadata for the given setting.",
"settings_missing" : "Not all settings loaded, refresh the page! This is probably caused by a high load on the database.",
"settings_missing" : "Not all settings loaded, refresh the page! This is probably caused by a high load on the database or app startup sequence.",
"settings_missing_block" : "You can not save your settings without specifying all setting keys. Refresh the page. This is probably caused by a high load on the database.",
"settings_old" : "The settings in the DB (shown on this page) are outdated. This is probably caused by a running scan. The settings were saved in the <code>pialert.conf</code> file, but the background process didn not have time to import it yet to the DB. You can wait until the settings get refreshed so you do not overwrite your old values. Feel free to save your settings either way if you do not mind losing the settings between the last save and now. There are also backup files created if you need to compare your settings later.",
"settings_old" : "Importing settings and re-initializing...",
"settings_saved" : "<br/>Settings saved to the <code>pialert.conf</code> file.<br/><br/>A time-stamped backup of the previous file created. <br/><br/> Reloading...<br/>",
"settings_imported" : "Last time settings were imported from the pialert.conf file:",
"settings_expand_all" : "Expand all",
"Setting_Override" : "Override value",
@@ -474,7 +478,7 @@
"ENABLE_PLUGINS_name" : "Enable Plugins",
"ENABLE_PLUGINS_description" : "Enables the <a target=\"_blank\" href=\"https://github.com/jokob-sk/Pi.Alert/tree/main/front/plugins\">plugins</a> functionality. Loading plugins requires more hardware resources so you might want to disable them on low-powered system.",
"PLUGINS_KEEP_HIST_name" : "Plugins History",
"PLUGINS_KEEP_HIST_description" : "How many entries of Plugins History scan results should be kept (globally, not device specific!).",
"PLUGINS_KEEP_HIST_description" : "How many entries of Plugins History scan results should be kept (per Plugin, not device specific).",
"PIALERT_WEB_PROTECTION_name" : "Enable login",
"PIALERT_WEB_PROTECTION_description" : "When enabled a login dialog is displayed. Read below carefully if you get locked out of your instance.",
"PIALERT_WEB_PASSWORD_name" : "Login password",
@@ -489,6 +493,8 @@
"REPORT_DASHBOARD_URL_description" : "This URL is used as the base for generating links in the emails. Enter full URL starting with <code>http://</code> including the port number (no trailig slash <code>/</code>).",
"DIG_GET_IP_ARG_name" : "Internet IP discovery",
"DIG_GET_IP_ARG_description" : "Change the <a href=\"https://linux.die.net/man/1/dig\" target=\"_blank\">dig utility</a> arguments if you have issues resolving your Internet IP. Arguments are added at the end of the following command: <code>dig +short </code>.",
"NETWORK_DEVICE_TYPES_name" : "Network device types",
"NETWORK_DEVICE_TYPES_description" : "Which device types are allowed to be used as network devices in the Network view. The device type has to match exactly the <code>Type</code> setting on a specific device in Device details. Do not remove existing types, only add new ones.",
"UI_LANG_name" : "UI Language",
"UI_LANG_description" : "Select the preferred UI language.",
"UI_PRESENCE_name" : "Show in presence chart",
@@ -498,7 +504,7 @@
"REPORT_MAIL_name" : "Enable email",
"REPORT_MAIL_description" : "If enabled an email is sent out with a list of changes you nove subscribed to. Please also fill out all remaining settings related to the SMTP setup below. If facing issues, set <code>LOG_LEVEL</code> to <code>debug</code> and check the <a href=\"/maintenance.php#tab_Logging\">error log</a>.",
"SMTP_SERVER_name" : "SMTP server URL",
"SMTP_SERVER_description" : "The SMTP server host URL. For example <code>smtp-relay.sendinblue.com</code>. To use Gmail as an SMTP server <a target=\"_blank\" href=\"https://github.com/jokob-sk/Pi.Alert/blob/main/docs/SMTP_GMAIL.md\">follow this guide</a>",
"SMTP_SERVER_description" : "The SMTP server host URL. For example <code>smtp-relay.sendinblue.com</code>. To use Gmail as an SMTP server <a target=\"_blank\" href=\"https://github.com/jokob-sk/Pi.Alert/blob/main/docs/SMTP.md\">follow this guide</a>",
"SMTP_PORT_name" : "SMTP server PORT",
"SMTP_PORT_description" : "Port number used for the SMTP connection. Set to <code>0</code> if you do not want to use a port when connecting to the SMTP server.",
"SMTP_SKIP_LOGIN_name" : "Skip authentication",
@@ -528,6 +534,8 @@
"WEBHOOK_REQUEST_METHOD_description" : "The HTTP request method to be used for the webhook call.",
"WEBHOOK_SIZE_name" : "Max payload size",
"WEBHOOK_SIZE_description" : "The maximum size of the webhook payload as number of characters in the passed string. If above limit, it will be truncated and a <code>(text was truncated)</code> message is appended.",
"WEBHOOK_SECRET_name": "HMAC Secret",
"WEBHOOK_SECRET_description": "When set, use this secret to generate the SHA256-HMAC hex digest value of the request body, which will be passed as the <code>X-Webhook-Signature</code> header to the request. You can find more informations <a target=\"_blank\" href=\"https://github.com/jokob-sk/Pi.Alert/blob/main/docs/WEBHOOK_SECRET.md\">here</a>.",
"Apprise_display_name" : "Apprise",
"Apprise_icon" : "<i class=\"fa fa-bullhorn\"></i>",
"REPORT_APPRISE_name" : "Enable Apprise",
@@ -576,18 +584,6 @@
"MQTT_QOS_description" : "Quality of service setting for MQTT message sending. <code>0</code> - Low quality to <code>2</code> - High quality. The higher the quality the longer the delay.",
"MQTT_DELAY_SEC_name" : "MQTT delay per device",
"MQTT_DELAY_SEC_description" : "A little hack - delay adding to the queue in case the process is restarted and previous publish processes aborted (it takes ~<code>2</code>s to update a sensor config on the broker). Tested with <code>2</code>-<code>3</code> seconds of delay. This delay is only applied when devices are created (during the first notification loop). It doesn not affect subsequent scans or notifications.",
"DynDNS_display_name" : "DynDNS",
"DynDNS_icon" : "<i class=\"fa fa-globe\"></i>",
"DDNS_ACTIVE_name" : "Enable DynDNS",
"DDNS_ACTIVE_description" : "Enable DynDNS service",
"DDNS_DOMAIN_name" : "DynDNS domain URL",
"DDNS_DOMAIN_description" : "DynDNS host URL (do not include http:// or https://).",
"DDNS_USER_name" : "DynDNS user",
"DDNS_USER_description" : "The username used to login to the DynDNS service (sometimes a full email address).",
"DDNS_PASSWORD_name" : "DynDNS password",
"DDNS_PASSWORD_description" : "The DynDNS service access password",
"DDNS_UPDATE_URL_name" : "DynDNS update URL",
"DDNS_UPDATE_URL_description" : "Update URL starting with <code>http://</code> or <code>https://</code>.",
"API_display_name" : "API",
"API_icon" : "<i class=\"fa fa-arrow-down-up-across-line\"></i>",
"API_CUSTOM_SQL_name" : "Custom endpoint",
@@ -662,6 +658,10 @@
"Systeminfo_System_System": "System:",
"Systeminfo_System_Uname": "Uname:",
"Systeminfo_System_Uptime": "Uptime:",
"Systeminfo_USB_Devices" : "USB Devices"
"Systeminfo_USB_Devices" : "USB Devices",
"Donations_Title" : "Donations",
"Donations_Text" : "Hey 👋! </br> Thanks for clicking on this menu item 😅 </br> </br> I'm trying to collect some donations to make you better software. Also, it would help me not to get burned out. Me burning out might mean end of support for this app. Any small (recurring or not) sponsorship makes me want ot put more effort into this app. I don't want to lock features (new plugins) behind paywalls 🔐. </br> Currently, I'm waking up 2h before work so I contribute to the app a bit. If I had some recurring income I could shorten my workweek and in the remaining time fully focus on PiAlert. You'd get more functionality, a more polished app and less bugs. </br> </br> Thanks for reading - I'm super grateful for any support ❤🙏 </br> </br> TL;DR: By supporting me you get: </br> </br> <ul><li>Regular updates to keep your data and family safe 🔄</li><li>Less bugs 🐛🔫</li><li>Better and more functionality</li><li>I don't get burned out 🔥🤯</li><li>Less rushed releases 💨</li><li>Better docs📚</li><li>Quicker and better support with issues 🆘</li><li>Less grumpy me 😄</li></ul> </br> 📧Email me to <a href='mailto:jokob@duck.com?subject=PiAlert'>jokob@duck.com</a> if you want to get in touch or if I should add other sponsorship platforms. </br>",
"Donations_Platforms" : "Sponsor platforms",
"Donations_Others" : "Others"
}
}

View File

@@ -83,6 +83,7 @@
"Device_Table_nav_prev" : "Anterior",
"Presence_Title" : "Historial por dispositivo",
"Presence_Loading" : "Cargando...",
"Loading" : "Cargando...",
"Presence_Shortcut_AllDevices" : "Todos",
"Presence_Shortcut_Connected" : "Conectado(s)",
"Presence_Shortcut_Favorites" : "Favorito(s)",
@@ -449,7 +450,7 @@
"Settings_Title" : "<i class=\"fa fa-cog\"> Configuración</i>",
"settings_missing" : "Actualiza la página, no todos los ajustes se han cargado. Probablemente sea por una sobrecarga de la base de datos.",
"settings_missing_block" : "No puedes guardar los ajustes sin establecer todas las claves. Actualiza la página. Problabmente esté causado por una sobrecarga de la base de datos.",
"settings_old" : "Los ajustes mostrados en esta página están desactualizados. Probablemente sea por un escaneo en proceso. Los ajustes se guardan en el archivo <code>pialert.conf</code>, pero el proceso en segundo plano no las ha importado todavía a la base de datos. Puedes esperar a que los ajustes se actualicen para evitar sobreescribirlos con los ajustes antiguos. Si te da igual perder los ajustes desde la última vez que guardaste y ahora, siéntete libre de guardarlos de nuevo. También hay copias de seguridad creadas si necesitas comparar tus ajustes más tarde.",
"settings_old" : "N/A",
"settings_imported" : "Última vez que los ajustes fueron importados desde el archivo pialert.conf:",
"settings_expand_all" : "Expandir todo",
"Setting_Override" : "Sobreescribir el valor",
@@ -489,7 +490,7 @@
"REPORT_MAIL_name" : "Habilitar email",
"REPORT_MAIL_description" : "Si está habilitado, se envía un correo electrónico con una lista de cambios a los que se ha suscrito. Complete también todas las configuraciones restantes relacionadas con la configuración de SMTP a continuación",
"SMTP_SERVER_name" : "URL del servidor SMTP",
"SMTP_SERVER_description" : "La URL del host del servidor SMTP. Por ejemplo, <code>smtp-relay.sendinblue.com</code>. Para utilizar Gmail como servidor SMTP <a target=\"_blank\" href=\"https://github.com/jokob-sk/Pi.Alert/blob/main/docs/SMTP_GMAIL.md\">siga esta guía</a >",
"SMTP_SERVER_description" : "La URL del host del servidor SMTP. Por ejemplo, <code>smtp-relay.sendinblue.com</code>. Para utilizar Gmail como servidor SMTP <a target=\"_blank\" href=\"https://github.com/jokob-sk/Pi.Alert/blob/main/docs/SMTP.md\">siga esta guía</a >",
"SMTP_PORT_name" : "Puerto del servidor SMTP",
"SMTP_PORT_description" : "Número de puerto utilizado para la conexión SMTP. Establézcalo en <code>0</code> si no desea utilizar un puerto al conectarse al servidor SMTP.",
"SMTP_SKIP_LOGIN_name" : "Omitir autenticación",
@@ -568,18 +569,6 @@
"MQTT_QOS_description" : "Configuración de calidad de servicio para el envío de mensajes MQTT. <code>0</code>: baja calidad a <code>2</code>: alta calidad. Cuanto mayor sea la calidad, mayor será el retraso.",
"MQTT_DELAY_SEC_name" : "Retraso de MQTT por dispositivo",
"MQTT_DELAY_SEC_description" : "Un pequeño truco: retrase la adición a la cola en caso de que el proceso se reinicie y los procesos de publicación anteriores se anulen (se necesitan ~<code>2</code>s para actualizar la configuración de un sensor en el intermediario). Probado con <code>2</code>-<code>3</code> segundos de retraso. Este retraso solo se aplica cuando se crean dispositivos (durante el primer bucle de notificación). No afecta los escaneos o notificaciones posteriores.",
"DynDNS_display_name" : "DynDNS",
"DynDNS_icon" : "<i class=\"fa fa-globe\"></i>",
"DDNS_ACTIVE_name" : "Habilitar DynDNS",
"DDNS_ACTIVE_description" : "Habilitar el servicio DynDNS",
"DDNS_DOMAIN_name" : "URL del dominio DynDNS",
"DDNS_DOMAIN_description" : "URL del host DynDNS (no incluya http:// o https://).",
"DDNS_USER_name" : "Usuario de DynDNS",
"DDNS_USER_description" : "El nombre de usuario utilizado para iniciar sesión en el servicio DynDNS (a veces, una dirección de correo electrónico completa).",
"DDNS_PASSWORD_name" : "Contraseña de DynDNS",
"DDNS_PASSWORD_description" : "La contraseña de acceso al servicio DynDNS.",
"DDNS_UPDATE_URL_name" : "URL de actualización de DynDNS",
"DDNS_UPDATE_URL_description" : "Actualice la URL que comienza con <code>http://</code> o <code>https://</code>.",
"API_display_name" : "API",
"API_icon" : "<i class=\"fa fa-arrow-down-up-across-line\"></i>",
"API_CUSTOM_SQL_name" : "Endpoint personalizado",

View File

@@ -2,6 +2,8 @@
### 🏴 Community translations of this file
> Please note there might be a delay between English and community translations.
* <a href="https://github.com/jokob-sk/Pi.Alert/blob/main/front/plugins/README_ES.md">
<img src="https://github.com/lipis/flag-icons/blob/main/flags/4x3/es.svg" alt="README_ES.md" style="height: 20px !important;width: 20px !important;"> Spanish (Spain)
</a>
@@ -12,23 +14,29 @@
### 🔌 Plugins & 📚 Docs
| Required | CurrentScan | Unique Prefix | Plugin Type | Link + Docs |
| Required | CurrentScan | Unique Prefix | Plugin Type | Link + Docs |
|-------------|-------------|-----------------------|------------------------|----------------------------------------------------------|
| | Yes | ARPSCAN | Script | [arp_scan](/front/plugins/arp_scan/) |
| | | CSVBCKP | Script | [csv_backup](/front/plugins/csv_backup/) |
| | Yes | DHCPLSS | Script | [dhcp_leases](/front/plugins/dhcp_leases/) |
| | | DHCPSRVS | Script | [dhcp_servers](/front/plugins/dhcp_servers/) |
| Yes | | NEWDEV | Template | [newdev_template](/front/plugins/newdev_template/) |
| | | NMAP | Script | [nmap_scan](/front/plugins/nmap_scan/) |
| | Yes | PIHOLE | External SQLite DB | [pihole_scan](/front/plugins/pihole_scan/) |
| | | SETPWD | Script | [set_password](/front/plugins/set_password/) |
| | | SNMPDSC | Script | [snmp_discovery](/front/plugins/snmp_discovery/) |
| | Yes* | UNDIS | Script | [undiscoverables](/front/plugins/undiscoverables/) |
| | Yes | UNFIMP | Script | [unifi_import](/front/plugins/unifi_import/) |
| | | WEBMON | Script | [website_monitor](/front/plugins/website_monitor/) |
| N/A | | N/A | SQL query | No example available, but the External SQLite based plugins work very similar |
| | Yes | ARPSCAN | Script | 📚[arp_scan](/front/plugins/arp_scan/) |
| | | CSVBCKP | Script | 📚[csv_backup](/front/plugins/csv_backup/) |
| Yes* | | DBCLNP | Script | 📚[db_cleanup](/front/plugins/db_cleanup/) |
| | | DDNS | Script | 📚[ddns_update](/front/plugins/ddns_update/) |
| | Yes | DHCPLSS | Script | 📚[dhcp_leases](/front/plugins/dhcp_leases/) |
| | | DHCPSRVS | Script | 📚[dhcp_servers](/front/plugins/dhcp_servers/) |
| | Yes | INTRNT | Script | 📚[internet_ip](/front/plugins/internet_ip/) |
| Yes | | NEWDEV | Template | 📚[newdev_template](/front/plugins/newdev_template/) |
| | | NMAP | Script | 📚[nmap_scan](/front/plugins/nmap_scan/) |
| | Yes | PIHOLE | External SQLite DB | 📚[pihole_scan](/front/plugins/pihole_scan/) |
| | | SETPWD | Script | 📚[set_password](/front/plugins/set_password/) |
| | | SNMPDSC | Script | 📚[snmp_discovery](/front/plugins/snmp_discovery/) |
| | Yes* | UNDIS | Script | 📚[undiscoverables](/front/plugins/undiscoverables/) |
| | Yes | UNFIMP | Script | 📚[unifi_import](/front/plugins/unifi_import/) |
| | | VNDRPDT | Script | 📚[vendor_update](/front/plugins/vendor_update/) |
| | | WEBMON | Script | 📚[website_monitor](/front/plugins/website_monitor/) |
| N/A | | N/A | SQL query | N/A, but the External SQLite DB plugins work similar |
>* The Undiscoverables plugin (`UNDIS`) inserts only user-specified dummy devices.
> \* The Undiscoverables plugin (`UNDIS`) inserts only user-specified dummy devices.
>
> \* The database cleanup plugin (`DBCLNP`) is not _required_ but the app will become unusable after a while if not executed.
> [!NOTE]
> You soft-disable plugins via Settings or completely ignore plugins by placing a `ignore_plugin` file into the plugin directory. The difference is that ignored plugins don't show up anywhere in the UI (Settings, Device details, Plugins pages). The app skips ignored plugins completely. Device-detecting plugins insert values into the `CurrentScan` database table. The plugins that are not required are safe to ignore, however it makes sense to have a least some device-detecting plugins (that insert entries into the `CurrentScan` table) enabled, such as ARPSCAN or PIHOLE.
@@ -564,7 +572,6 @@ You can have any `"function": "my_custom_name"` custom name, however, the ones l
| | - `missing-in-last-scan` - if the object is missing compared to previous scans |
> 🔎 Example:
>
> ```json

View File

@@ -352,7 +352,7 @@
"language_code":"en_us",
"string" : "Status"
},
{
{
"language_code":"es_es",
"string" : "Estado"
}]

View File

@@ -63,6 +63,7 @@
"settings": [
{
"function": "RUN",
"events": ["run"],
"type": "text.select",
"default_value":"schedule",
"options": ["disabled", "once", "schedule", "always_after_scan", "on_new_device"],
@@ -109,7 +110,7 @@
},
{
"language_code": "de_de",
"string": "Kommando"
"string": "Befehl"
}
],
"description": [

View File

@@ -0,0 +1,7 @@
## Overview
Plugin to run regular database cleanup tasks. It is strongly recommended to have an hourly or at least daily schedule running.
### Usage
- Check the Settings page for details.

View File

@@ -0,0 +1,181 @@
{
"code_name": "db_cleanup",
"unique_prefix": "DBCLNP",
"enabled": true,
"data_source": "script",
"show_ui": false,
"localized": ["display_name", "description", "icon"],
"display_name": [
{
"language_code": "en_us",
"string": "DB cleanup"
}
],
"icon": [
{
"language_code": "en_us",
"string": "<i class=\"fa-solid fa-broom\"></i>"
}
],
"description": [
{
"language_code": "en_us",
"string": "A plugin to schedule database cleanup & upkeep tasks."
}
],
"params" : [{
"name" : "pluginskeephistory",
"type" : "setting",
"value" : "PLUGINS_KEEP_HIST"
},
{
"name" : "daystokeepevents",
"type" : "setting",
"value" : "DAYS_TO_KEEP_EVENTS"
},
{
"name" : "hourstokeepnewdevice",
"type" : "setting",
"value" : "HRS_TO_KEEP_NEWDEV"
},
{
"name" : "pholuskeepdays",
"type" : "setting",
"value" : "PHOLUS_DAYS_DATA"
}
],
"settings": [
{
"function": "RUN",
"events": ["run"],
"type": "text.select",
"default_value":"schedule",
"options": ["disabled", "once", "schedule", "always_after_scan"],
"localized": ["name", "description"],
"name" :[{
"language_code":"en_us",
"string" : "When to run"
},
{
"language_code":"es_es",
"string" : "Cuándo ejecutar"
},
{
"language_code":"de_de",
"string" : "Wann laufen"
}],
"description": [{
"language_code":"en_us",
"string" : "When the cleanup should be performed. An hourly or daily <code>SCHEDULE</code> is a good option."
}]
},
{
"function": "CMD",
"type": "readonly",
"default_value": "python3 /home/pi/pialert/front/plugins/db_cleanup/script.py pluginskeephistory={pluginskeephistory} hourstokeepnewdevice={hourstokeepnewdevice} daystokeepevents={daystokeepevents} pholuskeepdays={pholuskeepdays}",
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Command"
},
{
"language_code": "es_es",
"string": "Comando"
},
{
"language_code": "de_de",
"string": "Befehl"
}
],
"description": [
{
"language_code": "en_us",
"string": "Command to run. This can not be changed"
},
{
"language_code": "es_es",
"string": "Comando a ejecutar. Esto no se puede cambiar"
},
{
"language_code": "de_de",
"string": "Befehl zum Ausführen. Dies kann nicht geändert werden"
}
]
},
{
"function": "RUN_SCHD",
"type": "text",
"default_value":"*/30 * * * *",
"options": [],
"localized": ["name", "description"],
"name" : [{
"language_code":"en_us",
"string" : "Schedule"
},
{
"language_code":"es_es",
"string" : "Schedule"
},
{
"language_code":"de_de",
"string" : "Schedule"
}],
"description": [{
"language_code":"en_us",
"string" : "Only enabled if you select <code>schedule</code> in the <a href=\"#DBCLNP_RUN\"><code>DBCLNP_RUN</code> setting</a>. Make sure you enter the schedule in the correct cron-like format (e.g. validate at <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). For example entering <code>0 4 * * *</code> will run the scan after 4 am in the <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</code> you set above</a>. Will be run NEXT time the time passes."
},
{
"language_code":"es_es",
"string" : "Solo está habilitado si selecciona <code>schedule</code> en la configuración <a href=\"#DBCLNP_RUN\"><code>DBCLNP_RUN</code></a>. Asegúrese de ingresar la programación en el formato similar a cron correcto (por ejemplo, valide en <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). Por ejemplo, ingresar <code>0 4 * * *</code> ejecutará el escaneo después de las 4 a.m. en el <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</ código> que configuró arriba</a>. Se ejecutará la PRÓXIMA vez que pase el tiempo."
},
{
"language_code":"de_de",
"string" : "Nur aktiviert, wenn Sie <code>schedule</code> in der <a href=\"#DBCLNP_RUN\"><code>DBCLNP_RUN</code>-Einstellung</a> auswählen. Stellen Sie sicher, dass Sie den Zeitplan im richtigen Cron-ähnlichen Format eingeben (z. B. validieren unter <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). Wenn Sie beispielsweise <code>0 4 * * *</code> eingeben, wird der Scan nach 4 Uhr morgens in der <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</ ausgeführt. Code> den Sie oben festgelegt haben</a>. Wird das NÄCHSTE Mal ausgeführt, wenn die Zeit vergeht."
}]
},
{
"function": "RUN_TIMEOUT",
"type": "integer",
"default_value": 30,
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Run timeout"
},
{
"language_code": "es_es",
"string": "Tiempo límite de ejecución"
},
{
"language_code": "de_de",
"string": "Zeitüberschreitung"
}
],
"description": [
{
"language_code": "en_us",
"string": "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted."
},
{
"language_code": "es_es",
"string": "Tiempo máximo en segundos para esperar a que finalice el script. Si se supera este tiempo, el script se cancela."
},
{
"language_code": "de_de",
"string": "Maximale Zeit in Sekunden, die auf den Abschluss des Skripts gewartet werden soll. Bei Überschreitung dieser Zeit wird das Skript abgebrochen."
}
]
}
],
"database_column_definitions":
[
]
}

View File

@@ -0,0 +1,147 @@
#!/usr/bin/env python
# test script by running:
# /home/pi/pialert/front/plugins/db_cleanup/script.py pluginskeephistory=250 hourstokeepnewdevice=48 daystokeepevents=90
import os
import pathlib
import argparse
import sys
import hashlib
import csv
import sqlite3
from io import StringIO
from datetime import datetime
sys.path.append("/home/pi/pialert/front/plugins")
sys.path.append('/home/pi/pialert/pialert')
from plugin_helper import Plugin_Object, Plugin_Objects, decodeBase64
from logger import mylog, append_line_to_file
from helper import timeNowTZ
from const import logPath, pialertPath
CUR_PATH = str(pathlib.Path(__file__).parent.resolve())
LOG_FILE = os.path.join(CUR_PATH, 'script.log')
RESULT_FILE = os.path.join(CUR_PATH, 'last_result.log')
def main():
parser = argparse.ArgumentParser(description='DB cleanup tasks')
parser.add_argument('pluginskeephistory', action="store", help="TBC")
parser.add_argument('hourstokeepnewdevice', action="store", help="TBC")
parser.add_argument('daystokeepevents', action="store", help="TBC")
parser.add_argument('pholuskeepdays', action="store", help="TBC")
values = parser.parse_args()
PLUGINS_KEEP_HIST = values.pluginskeephistory.split('=')[1]
HRS_TO_KEEP_NEWDEV = values.hourstokeepnewdevice.split('=')[1]
DAYS_TO_KEEP_EVENTS = values.daystokeepevents.split('=')[1]
PHOLUS_DAYS_DATA = values.pholuskeepdays.split('=')[1]
mylog('verbose', ['[DBCLNP] In script'])
# Execute cleanup/upkeep
cleanup_database('/home/pi/pialert/db/pialert.db', DAYS_TO_KEEP_EVENTS, PHOLUS_DAYS_DATA, HRS_TO_KEEP_NEWDEV, PLUGINS_KEEP_HIST)
mylog('verbose', ['[DBCLNP] Cleanup complete file '])
return 0
#===============================================================================
# Cleanup / upkeep database
#===============================================================================
def cleanup_database (dbPath, DAYS_TO_KEEP_EVENTS, PHOLUS_DAYS_DATA, HRS_TO_KEEP_NEWDEV, PLUGINS_KEEP_HIST):
"""
Cleaning out old records from the tables that don't need to keep all data.
"""
mylog('verbose', ['[DBCLNP] Upkeep Database:' ])
# Connect to the PiAlert SQLite database
conn = sqlite3.connect(dbPath)
cursor = conn.cursor()
# Cleanup Online History
mylog('verbose', ['[DBCLNP] Online_History: Delete all but keep latest 150 entries'])
cursor.execute ("""DELETE from Online_History where "Index" not in (
SELECT "Index" from Online_History
order by Scan_Date desc limit 150)""")
mylog('verbose', ['[DBCLNP] Optimize Database'])
# Cleanup Events
mylog('verbose', [f'[DBCLNP] Events: Delete all older than {str(DAYS_TO_KEEP_EVENTS)} days (DAYS_TO_KEEP_EVENTS setting)'])
cursor.execute (f"""DELETE FROM Events
WHERE eve_DateTime <= date('now', '-{str(DAYS_TO_KEEP_EVENTS)} day')""")
# Trim Plugins_History entries to less than PLUGINS_KEEP_HIST setting per unique "Plugin" column entry
mylog('verbose', [f'[DBCLNP] Plugins_History: Trim Plugins_History entries to less than {str(PLUGINS_KEEP_HIST)} per Plugin (PLUGINS_KEEP_HIST setting)'])
# Build the SQL query to delete entries that exceed the limit per unique "Plugin" column entry
delete_query = f"""DELETE FROM Plugins_History
WHERE "Index" NOT IN (
SELECT "Index"
FROM (
SELECT "Index",
ROW_NUMBER() OVER(PARTITION BY "Plugin" ORDER BY DateTimeChanged DESC) AS row_num
FROM Plugins_History
) AS ranked_objects
WHERE row_num <= {str(PLUGINS_KEEP_HIST)}
);"""
cursor.execute(delete_query)
# Cleanup Pholus_Scan
if PHOLUS_DAYS_DATA != 0:
mylog('verbose', ['[DBCLNP] Pholus_Scan: Delete all older than ' + str(PHOLUS_DAYS_DATA) + ' days (PHOLUS_DAYS_DATA setting)'])
# todo: improvement possibility: keep at least N per mac
cursor.execute (f"""DELETE FROM Pholus_Scan
WHERE Time <= date('now', '-{str(PHOLUS_DAYS_DATA)} day')""")
# Cleanup New Devices
if HRS_TO_KEEP_NEWDEV != 0:
mylog('verbose', [f'[DBCLNP] Devices: Delete all New Devices older than {str(HRS_TO_KEEP_NEWDEV)} hours (HRS_TO_KEEP_NEWDEV setting)'])
cursor.execute (f"""DELETE FROM Devices
WHERE dev_NewDevice = 1 AND dev_FirstConnection < date('now', '+{str(HRS_TO_KEEP_NEWDEV)} hour')""")
# De-dupe (de-duplicate) from the Plugins_Objects table
# TODO This shouldn't be necessary - probably a concurrency bug somewhere in the code :(
mylog('verbose', ['[DBCLNP] Plugins_Objects: Delete all duplicates'])
cursor.execute("""
DELETE FROM Plugins_Objects
WHERE rowid > (
SELECT MIN(rowid) FROM Plugins_Objects p2
WHERE Plugins_Objects.Plugin = p2.Plugin
AND Plugins_Objects.Object_PrimaryID = p2.Object_PrimaryID
AND Plugins_Objects.Object_SecondaryID = p2.Object_SecondaryID
AND Plugins_Objects.UserData = p2.UserData
)
""")
# De-Dupe (de-duplicate - remove duplicate entries) from the Pholus_Scan table
mylog('verbose', ['[DBCLNP] Pholus_Scan: Delete all duplicates'])
cursor.execute ("""DELETE FROM Pholus_Scan
WHERE rowid > (
SELECT MIN(rowid) FROM Pholus_Scan p2
WHERE Pholus_Scan.MAC = p2.MAC
AND Pholus_Scan.Value = p2.Value
AND Pholus_Scan.Record_Type = p2.Record_Type
);""")
conn.commit()
# Shrink DB
mylog('verbose', ['[DBCLNP] Shrink Database'])
cursor.execute ("VACUUM;")
# Close the database connection
conn.close()
#===============================================================================
# BEGIN
#===============================================================================
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,7 @@
## Overview
Plugin to run regular DDNS update tasks.
### Usage
- Check the Settings page for details.

View File

@@ -0,0 +1,559 @@
{
"code_name": "ddns_update",
"unique_prefix": "DDNS",
"enabled": true,
"data_filters": [
{
"compare_column": "Object_PrimaryID",
"compare_operator": "==",
"compare_field_id": "txtMacFilter",
"compare_js_template": "'{value}'.toString()",
"compare_use_quotes": true
}
],
"data_source": "script",
"show_ui": true,
"localized": [
"display_name",
"description",
"icon"
],
"display_name": [
{
"language_code": "en_us",
"string": "DDNS update"
}
],
"icon": [
{
"language_code": "en_us",
"string": "<i class=\"fa-solid fa-globe\"></i>"
}
],
"description": [
{
"language_code": "en_us",
"string": "A plugin update the DDNS record."
}
],
"params": [
{
"name": "prev_ip",
"type": "sql",
"value": "SELECT dev_LastIP FROM Devices WHERE dev_MAC = 'Internet' "
},
{
"name": "DDNS_UPDATE_URL",
"type": "setting",
"value": "DDNS_UPDATE_URL"
},
{
"name": "DDNS_USER",
"type": "setting",
"value": "DDNS_USER"
},
{
"name": "DDNS_PASSWORD",
"type": "setting",
"value": "DDNS_PASSWORD"
},
{
"name": "DDNS_DOMAIN",
"type": "setting",
"value": "DDNS_DOMAIN"
}
],
"settings": [
{
"function": "RUN",
"events": ["run"],
"type": "text.select",
"default_value": "schedule",
"options": [
"disabled",
"once",
"schedule",
"always_after_scan"
],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "When to run"
},
{
"language_code": "es_es",
"string": "Cuándo ejecutar"
},
{
"language_code": "de_de",
"string": "Wann laufen"
}
],
"description": [
{
"language_code": "en_us",
"string": "When the plugin should run. An hourly or daily <code>SCHEDULE</code> is a good option."
}
]
},
{
"function": "CMD",
"type": "readonly",
"default_value": "python3 /home/pi/pialert/front/plugins/ddns_update/script.py prev_ip={prev_ip} DDNS_UPDATE_URL={DDNS_UPDATE_URL} DDNS_USER={DDNS_USER} DDNS_PASSWORD={DDNS_PASSWORD} DDNS_DOMAIN={DDNS_DOMAIN} ",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Command"
},
{
"language_code": "es_es",
"string": "Comando"
},
{
"language_code": "de_de",
"string": "Befehl"
}
],
"description": [
{
"language_code": "en_us",
"string": "Command to run. This can not be changed"
},
{
"language_code": "es_es",
"string": "Comando a ejecutar. Esto no se puede cambiar"
},
{
"language_code": "de_de",
"string": "Befehl zum Ausführen. Dies kann nicht geändert werden"
}
]
},
{
"function": "RUN_SCHD",
"type": "text",
"default_value": "*/5 * * * *",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Schedule"
},
{
"language_code": "es_es",
"string": "Schedule"
},
{
"language_code": "de_de",
"string": "Schedule"
}
],
"description": [
{
"language_code": "en_us",
"string": "Only enabled if you select <code>schedule</code> in the <a href=\"#DDNS_RUN\"><code>DDNS_RUN</code> setting</a>. Make sure you enter the schedule in the correct cron-like format (e.g. validate at <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). For example entering <code>0 4 * * *</code> will run the scan after 4 am in the <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</code> you set above</a>. Will be run NEXT time the time passes."
},
{
"language_code": "es_es",
"string": "Solo está habilitado si selecciona <code>schedule</code> en la configuración <a href=\"#DDNS_RUN\"><code>DDNS_RUN</code></a>. Asegúrese de ingresar la programación en el formato similar a cron correcto (por ejemplo, valide en <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). Por ejemplo, ingresar <code>0 4 * * *</code> ejecutará el escaneo después de las 4 a.m. en el <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</ código> que configuró arriba</a>. Se ejecutará la PRÓXIMA vez que pase el tiempo."
},
{
"language_code": "de_de",
"string": "Nur aktiviert, wenn Sie <code>schedule</code> in der <a href=\"#DDNS_RUN\"><code>DDNS_RUN</code>-Einstellung</a> auswählen. Stellen Sie sicher, dass Sie den Zeitplan im richtigen Cron-ähnlichen Format eingeben (z. B. validieren unter <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). Wenn Sie beispielsweise <code>0 4 * * *</code> eingeben, wird der Scan nach 4 Uhr morgens in der <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</ ausgeführt. Code> den Sie oben festgelegt haben</a>. Wird das NÄCHSTE Mal ausgeführt, wenn die Zeit vergeht."
}
]
},
{
"function": "RUN_TIMEOUT",
"type": "integer",
"default_value": 30,
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Run timeout"
},
{
"language_code": "es_es",
"string": "Tiempo límite de ejecución"
},
{
"language_code": "de_de",
"string": "Zeitüberschreitung"
}
],
"description": [
{
"language_code": "en_us",
"string": "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted."
},
{
"language_code": "es_es",
"string": "Tiempo máximo en segundos para esperar a que finalice el script. Si se supera este tiempo, el script se cancela."
},
{
"language_code": "de_de",
"string": "Maximale Zeit in Sekunden, die auf den Abschluss des Skripts gewartet werden soll. Bei Überschreitung dieser Zeit wird das Skript abgebrochen."
}
]
},
{
"function": "DOMAIN",
"type": "text",
"default_value": "your_domain.freeddns.org",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "DynDNS domain URL"
},
{
"language_code": "es_es",
"string": "URL del dominio DynDNS"
}
],
"description": [
{
"language_code": "en_us",
"string": "DynDNS host URL (do not include http:// or https://)."
},
{
"language_code": "es_es",
"string": "URL del host DynDNS (no incluya http:// o https://)."
}
]
},
{
"function": "USER",
"type": "text",
"default_value": "dynu_user",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "DynDNS user"
},
{
"language_code": "es_es",
"string": "Usuario de DynDNS"
}
],
"description": [
{
"language_code": "en_us",
"string": "The username used to login to the DynDNS service (sometimes a full email address)."
},
{
"language_code": "es_es",
"string": "El nombre de usuario utilizado para iniciar sesión en el servicio DynDNS (a veces, una dirección de correo electrónico completa)."
}
]
},
{
"function": "PASSWORD",
"type": "password",
"default_value": "A0000000B0000000C0000000D0000000",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "DynDNS password"
},
{
"language_code": "es_es",
"string": "Contraseña de DynDNS"
}
],
"description": [
{
"language_code": "en_us",
"string": "The DynDNS service access password"
},
{
"language_code": "es_es",
"string": "La contraseña de acceso al servicio DynDNS."
}
]
},
{
"function": "UPDATE_URL",
"type": "text",
"default_value": "https://api.dynu.com/nic/update?",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "DynDNS update URL"
},
{
"language_code": "es_es",
"string": "URL de actualización de DynDNS"
}
],
"description": [
{
"language_code": "en_us",
"string": "Update URL starting with <code>http://</code> or <code>https://</code>."
},
{
"language_code": "es_es",
"string": "Actualice la URL que comienza con <code>http://</code> o <code>https://</code>."
}
]
},
{
"function": "WATCH",
"type": "text.multiselect",
"default_value": [
"Watched_Value1"
],
"options": [
"Watched_Value1",
"Watched_Value2",
"Watched_Value3",
"Watched_Value4"
],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Watched"
},
{
"language_code": "es_es",
"string": "Visto"
}
],
"description": [
{
"language_code": "en_us",
"string": "Send a notification if selected values change. Use <code>CTRL + Click</code> to select/deselect. <ul> <li><code>Watched_Value1</code> is Previous IP (not recommended)</li><li><code>Watched_Value2</code> unused</li><li><code>Watched_Value3</code> unused </li><li><code>Watched_Value4</code> unused </li></ul>"
}
]
},
{
"function": "REPORT_ON",
"type": "text.multiselect",
"default_value":["new","watched-changed"],
"options": ["new","watched-changed","watched-not-changed", "missing-in-last-scan"],
"localized": ["name", "description"],
"name" :[{
"language_code":"en_us",
"string" : "Report on"
},
{
"language_code":"es_es",
"string" : "Informar sobre"
} ] ,
"description":[{
"language_code":"en_us",
"string" : "Send a notification only on these statuses. <code>new</code> means a new unique (unique combination of PrimaryId and SecondaryId) object was discovered. <code>watched-changed</code> means that selected <code>Watched_ValueN</code> columns changed."
},
{
"language_code":"es_es",
"string" : "Envíe una notificación solo en estos estados. <code>new</code> significa que se descubrió un nuevo objeto único (una combinación única de PrimaryId y SecondaryId). <code>watched-changed</code> significa que las columnas <code>Watched_ValueN</code> seleccionadas cambiaron."
}]
}
],
"database_column_definitions": [
{
"column": "Object_PrimaryID",
"css_classes": "col-sm-2",
"show": true,
"type": "device_name_mac",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "MAC"
},
{
"language_code": "es_es",
"string": "MAC"
}
]
},
{
"column": "Object_SecondaryID",
"css_classes": "col-sm-2",
"show": true,
"type": "device_ip",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "IP"
},
{
"language_code": "es_es",
"string": "IP"
}
]
},
{
"column": "Watched_Value1",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Extra"
}
]
},
{
"column": "Dummy",
"mapped_to_column_data": {
"value": "DDNS"
},
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Scan method"
},
{
"language_code": "es_es",
"string": "Método de escaneo"
}
]
},
{
"column": "DateTimeCreated",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Created"
},
{
"language_code": "es_es",
"string": "Creado"
}
]
},
{
"column": "DateTimeChanged",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Changed"
},
{
"language_code": "es_es",
"string": "Cambiado"
}
]
},
{
"column": "Status",
"css_classes": "col-sm-1",
"show": true,
"type": "replace",
"default_value": "",
"options": [
{
"equals": "watched-not-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-square-check'></i><div></div>"
},
{
"equals": "watched-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-triangle-exclamation'></i></div>"
},
{
"equals": "new",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-circle-plus'></i></div>"
},
{
"equals": "missing-in-last-scan",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-question'></i></div>"
}
],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Status"
},
{
"language_code": "es_es",
"string": "Estado"
}
]
}
]
}

View File

@@ -0,0 +1,145 @@
#!/usr/bin/env python
# test script by running:
# /home/pi/pialert/front/plugins/internet_ip/script.py TBD
import os
import pathlib
import argparse
import sys
import hashlib
import csv
import subprocess
import re
import base64
import sqlite3
from io import StringIO
from datetime import datetime
sys.path.append("/home/pi/pialert/front/plugins")
sys.path.append('/home/pi/pialert/pialert')
from plugin_helper import Plugin_Object, Plugin_Objects, decodeBase64
from logger import mylog, append_line_to_file
from helper import timeNowTZ, check_IP_format
from const import logPath, pialertPath, fullDbPath
CUR_PATH = str(pathlib.Path(__file__).parent.resolve())
LOG_FILE = os.path.join(CUR_PATH, 'script.log')
RESULT_FILE = os.path.join(CUR_PATH, 'last_result.log')
def main():
mylog('verbose', ['[DDNS] In script'])
parser = argparse.ArgumentParser(description='Check internet connectivity and IP')
parser.add_argument('prev_ip', action="store", help="Previous IP address to compare against the current IP")
parser.add_argument('DDNS_UPDATE_URL', action="store", help="URL for updating Dynamic DNS (DDNS)")
parser.add_argument('DDNS_USER', action="store", help="Username for Dynamic DNS (DDNS) authentication")
parser.add_argument('DDNS_PASSWORD', action="store", help="Password for Dynamic DNS (DDNS) authentication")
parser.add_argument('DDNS_DOMAIN', action="store", help="Dynamic DNS (DDNS) domain name")
values = parser.parse_args()
PREV_IP = values.prev_ip.split('=')[1]
DDNS_UPDATE_URL = values.DDNS_UPDATE_URL.split('=')[1]
DDNS_USER = values.DDNS_USER.split('=')[1]
DDNS_PASSWORD = values.DDNS_PASSWORD.split('=')[1]
DDNS_DOMAIN = values.DDNS_DOMAIN.split('=')[1]
# perform the new IP lookup and DDNS tasks if enabled
ddns_update( DDNS_UPDATE_URL, DDNS_USER, DDNS_PASSWORD, DDNS_DOMAIN, PREV_IP)
mylog('verbose', ['[DDNS] Finished '])
return 0
#===============================================================================
# INTERNET IP CHANGE
#===============================================================================
def ddns_update ( DDNS_UPDATE_URL, DDNS_USER, DDNS_PASSWORD, DDNS_DOMAIN, PREV_IP ):
# Update DDNS record if enabled and IP is different
# Get Dynamic DNS IP
mylog('verbose', ['[DDNS] Retrieving Dynamic DNS IP'])
dns_IP = get_dynamic_DNS_IP(DDNS_DOMAIN)
# Check Dynamic DNS IP
if dns_IP == "" or dns_IP == "0.0.0.0" :
mylog('none', ['[DDNS] Error retrieving Dynamic DNS IP'])
mylog('none', ['[DDNS] ', dns_IP])
# Check DNS Change
if dns_IP != PREV_IP :
mylog('none', ['[DDNS] Updating Dynamic DNS IP'])
message = set_dynamic_DNS_IP (DDNS_UPDATE_URL, DDNS_USER, DDNS_PASSWORD, DDNS_DOMAIN)
mylog('none', ['[DDNS] ', message])
# plugin_objects = Plugin_Objects(RESULT_FILE)
# plugin_objects.add_object(
# primaryId = 'Internet', # MAC (Device Name)
# secondaryId = new_internet_IP, # IP Address
# watched1 = f'Previous IP: {PREV_IP}',
# watched2 = '',
# watched3 = '',
# watched4 = '',
# extra = f'Previous IP: {PREV_IP}',
# foreignKey = 'Internet')
# plugin_objects.write_result_file()
#-------------------------------------------------------------------------------
def get_dynamic_DNS_IP (DDNS_DOMAIN):
# Using supplied DNS server
dig_args = ['dig', '+short', DDNS_DOMAIN]
try:
# try runnning a subprocess
dig_output = subprocess.check_output (dig_args, universal_newlines=True)
except subprocess.CalledProcessError as e:
# An error occured, handle it
mylog('none', ['[DDNS] ERROR - ', e.output])
dig_output = '' # probably no internet
# Check result is an IP
IP = check_IP_format (dig_output)
# Handle invalid response
if IP == '':
IP = '0.0.0.0'
return IP
#-------------------------------------------------------------------------------
def set_dynamic_DNS_IP (DDNS_UPDATE_URL, DDNS_USER, DDNS_PASSWORD, DDNS_DOMAIN):
try:
# try runnning a subprocess
# Update Dynamic IP
curl_output = subprocess.check_output (['curl',
'-s',
DDNS_UPDATE_URL +
'username=' + DDNS_USER +
'&password=' + DDNS_PASSWORD +
'&hostname=' + DDNS_DOMAIN],
universal_newlines=True)
except subprocess.CalledProcessError as e:
# An error occured, handle it
mylog('none', ['[DDNS] ERROR - ',e.output])
curl_output = ""
return curl_output
#===============================================================================
# BEGIN
#===============================================================================
if __name__ == '__main__':
main()

View File

@@ -310,6 +310,7 @@
"settings":[
{
"function": "RUN",
"events": ["run"],
"type": "text.select",
"default_value":"disabled",
"options": ["disabled", "once", "schedule", "always_after_scan", "on_new_device"],
@@ -393,7 +394,7 @@
}],
"description": [{
"language_code":"en_us",
"string" : "Only enabled if you select <code>schedule</code> in the <a href=\"#DHCPLSS_RUN\"><code>DHCPLSS_RUN</code> setting</a>. Make sure you enter the schedule in the correct cron-like format (e.g. validate at <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). For example entering <code>0 4 * * *</code> will run the scan after 4 am in the <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</code> you set above</a>. Will be run NEXT time the time passes."
"string" : "Only enabled if you select <code>schedule</code> in the <a href=\"#DHCPLSS_RUN\"><code>DHCPLSS_RUN</code> setting</a>. Make sure you enter the schedule in the correct cron-like format (e.g. validate at <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). For example entering <code>0 4 * * *</code> will run the scan after 4 am in the <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</code> you set above</a>. Will be run NEXT time the time passes. <br/> It's recommended to use the same schedule interval for all plugins responsible for discovering new devices."
},
{
"language_code":"es_es",

View File

@@ -274,6 +274,7 @@
"settings":[
{
"function": "RUN",
"events": ["run"],
"type": "text.select",
"default_value":"disabled",
"options": ["disabled", "once", "schedule", "always_after_scan", "on_new_device"],

View File

@@ -15,8 +15,10 @@ from logger import mylog
def main():
mylog('verbose', ['[DHCPSRVS] In script'])
RESULT_FILE = 'last_result.log'
last_run_logfile = open(RESULT_FILE, 'a')
last_run_logfile.write("")
plugin_objects = Plugin_Objects(RESULT_FILE)
timeoutSec = 10

View File

@@ -0,0 +1,838 @@
{
"code_name": "events_notifications",
"plugin_type": "flow",
"template_type": "database-entry",
"unique_prefix": "EVNTNTF",
"enabled": true,
"data_source": "template",
"show_ui": false,
"localized": [
"display_name",
"description",
"icon"
],
"display_name": [
{
"language_code": "en_us",
"string": "Known Devices"
}
],
"description": [
{
"language_code": "en_us",
"string": "The template used for known devices."
}
],
"icon": [
{
"language_code": "en_us",
"string": "<i class=\"fa fa-check\"></i>"
}
],
"params": [
{
"name": "target_macs",
"type": "setting",
"value": "KNWN_target_macs"
},
{
"name": "dev_AlertDeviceDown",
"type": "setting",
"value": "KNWN_dev_AlertDeviceDown"
},
{
"name": "dev_AlertEvents",
"type": "setting",
"value": "KNWN_dev_AlertEvents"
},
{
"name": "trigger_ids",
"type": "array",
"value": "trigger.Object_PrimaryID"
},
{
"name": "trigger_objects",
"type": "array",
"value": "trigger"
}
],
"settings": [
{
"function": "FLOW",
"type": "json",
"default_value": [
{
"name": "send_notification",
"trigger": [
{
"object_event": "new",
"object_filter": "",
"object": "Events_Notifications"
}
],
"steps": [
{
"step_type": "wait",
"params": [
{
"days": 0,
"hours": 0,
"minutes": 0,
"seconds": 30
}
]
},
{
"step_type": "condition",
"params": [
{
"left": {
"value": "triggers[0].object['dev_DeviceID']",
"use_quotes": true,
"js_template": "'{value}'.toString()"
},
"operator": {
"value": "==",
"data_type": "string"
},
"right": {
"value": "device_id_param",
"use_quotes": false,
"js_template": "'{value}'.toString()"
}
}
]
},
{
"step_type": "condition",
"params": [
{
"left": {
"value": "check_event_repetition(device_id_param, event_type_param, repetition_count_param)",
"use_quotes": false,
"js_template": "{value}"
},
"operator": {
"value": "==",
"data_type": "boolean"
},
"right": {
"value": true,
"use_quotes": false,
"js_template": "{value}"
}
}
]
},
{
"step_type": "action",
"params": [
{
"type": "run_plugin",
"params": {
"unique_prefix": "webhook"
}
}
]
},
{
"step_type": "action",
"params": [
{
"type": "run_plugin",
"params": {
"unique_prefix": "mqtt"
}
}
]
}
]
}
],
"options": [
{
"name": [
{
"language_code": "en_us",
"string": "Device ID Parameter"
}
],
"type": "string",
"default_value": "1",
"localized": [
"name"
]
},
{
"name": [
{
"language_code": "en_us",
"string": "Event Type Parameter"
}
],
"type": "string",
"default_value": "device_down",
"localized": [
"name"
]
},
{
"name": [
{
"language_code": "en_us",
"string": "Repetition Count Parameter"
}
],
"type": "integer",
"default_value": 3,
"localized": [
"name"
]
}
],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Plugin flow"
}
],
"description": [
{
"language_code": "en_us",
"string": "This flow sends a notification after 30s every time a new event is logged."
}
]
},
{
"function": "target_macs",
"type": "list.readonly",
"maxLength": 50,
"default_value": [],
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Target devices"
}
],
"description": [
{
"language_code": "en_us",
"string": "The MAC address of the devices to update. Uneditable. This parameter is dynamically updated via a Flow."
}
]
},
{
"function": "CMD",
"type": "text",
"default_value": "UPDATE Devices SET dev_AlertDeviceDown = {KNWN_dev_AlertDeviceDown}, dev_AlertEvents = {KNWN_dev_AlertEvents} WHERE dev_MAC in ({target_macs})",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "UPDATE SQL"
}
],
"description": [
{
"language_code": "en_us",
"string": "This SQL query is used to update target devices."
}
]
},
{
"function": "dev_Name",
"type": "readonly",
"maxLength": 50,
"default_value": "(unknown)",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Device Name"
}
],
"description": [
{
"language_code": "en_us",
"string": "The name of the device. Uneditable as internal functionality is dependent on specific new device names."
}
]
},
{
"function": "dev_Owner",
"type": "string",
"maxLength": 30,
"default_value": "House",
"override_value": {
"override": false
},
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Device Owner"
}
],
"description": [
{
"language_code": "en_us",
"string": "The owner of the device."
}
]
},
{
"function": "dev_DeviceType",
"type": "string",
"maxLength": 30,
"default_value": "",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Device Type"
}
],
"description": [
{
"language_code": "en_us",
"string": "The type of the device."
}
]
},
{
"function": "dev_Vendor",
"type": "readonly",
"maxLength": 250,
"default_value": "",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Device Vendor"
}
],
"description": [
{
"language_code": "en_us",
"string": "The vendor of the device. Uneditable - Autodetected."
}
]
},
{
"function": "dev_Favorite",
"type": "integer.checkbox",
"default_value": 0,
"override_value": {
"override": false
},
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Favorite Device"
}
],
"description": [
{
"language_code": "en_us",
"string": "Indicates whether the device is marked as a favorite."
}
]
},
{
"function": "dev_Group",
"type": "string",
"maxLength": 10,
"default_value": "",
"override_value": {
"override": false
},
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Device Group"
}
],
"description": [
{
"language_code": "en_us",
"string": "The group to which the device belongs."
}
]
},
{
"function": "dev_Comments",
"type": "string",
"default_value": "",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Device Comments"
}
],
"description": [
{
"language_code": "en_us",
"string": "Additional comments or notes about the device."
}
]
},
{
"function": "dev_FirstConnection",
"type": "readonly",
"format": "date-time",
"default_value": "",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "First Connection"
}
],
"description": [
{
"language_code": "en_us",
"string": "The date and time of the first connection with the device. Uneditable - Autodetected."
}
]
},
{
"function": "dev_LastConnection",
"type": "readonly",
"format": "date-time",
"default_value": "",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Last Connection"
}
],
"description": [
{
"language_code": "en_us",
"string": "The date and time of the last connection with the device. Uneditable - Autodetected."
}
]
},
{
"function": "dev_LastIP",
"type": "readonly",
"maxLength": 50,
"default_value": "",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Last IP"
}
],
"description": [
{
"language_code": "en_us",
"string": "The last known IP address of the device. Uneditable - Autodetected."
}
]
},
{
"function": "dev_StaticIP",
"type": "integer.checkbox",
"default_value": 1,
"override_value": {
"override": true
},
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Static IP"
}
],
"description": [
{
"language_code": "en_us",
"string": "Indicates whether the device has a static IP address."
}
]
},
{
"function": "dev_ScanCycle",
"type": "integer.checkbox",
"default_value": 1,
"override_value": {
"override": true
},
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Scan Cycle"
}
],
"description": [
{
"language_code": "en_us",
"string": "The default value of the <code>Scan device</code> dropdown. Enable if newly discovered devices should be scanned."
}
]
},
{
"function": "dev_LogEvents",
"type": "integer.checkbox",
"default_value": 0,
"override_value": {
"override": false
},
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Log Events"
}
],
"description": [
{
"language_code": "en_us",
"string": "Indicates whether events related to the device shouldbe logged."
}
]
},
{
"function": "dev_AlertEvents",
"type": "integer.checkbox",
"default_value": 0,
"override_value": {
"override": true
},
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Alert Events"
}
],
"description": [
{
"language_code": "en_us",
"string": "Indicates whether events related to the device should trigger alerts. The default value of the <code>Alert All Events</code> checkbox."
}
]
},
{
"function": "dev_AlertDeviceDown",
"type": "integer.checkbox",
"default_value": 0,
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Alert Device Down"
}
],
"description": [
{
"language_code": "en_us",
"string": "Indicates whether an alert should be triggered when the device goes down. The default value of the <code>Alert Down</code> checkbox."
}
]
},
{
"function": "dev_SkipRepeated",
"type": "integer",
"default_value": 0,
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Skip Repeated"
}
],
"description": [
{
"language_code": "en_us",
"string": "The default value of the <code>Skip repeated notifications for</code> dropdown. Enter number of <b>hours</b> for which repeated notifications should be ignored for. If you enter <code>0</code> then you get notified on all events."
}
]
},
{
"function": "dev_LastNotification",
"type": "readonly",
"format": "date-time",
"default_value": "",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Last Notification"
}
],
"description": [
{
"language_code": "en_us",
"string": "The date and time of the last notification sent for the device. Uneditable - Autodetected."
}
]
},
{
"function": "dev_PresentLastScan",
"type": "integer.checkbox",
"default_value": 1,
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Present Last Scan"
}
],
"description": [
{
"language_code": "en_us",
"string": "Indicates whether the device should be marked as present after detected in a scan."
}
]
},
{
"function": "dev_NewDevice",
"type": "integer.checkbox",
"default_value": true,
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "New Device"
}
],
"description": [
{
"language_code": "en_us",
"string": "Indicates whether the device is considered a new device. The default value of the <code>New Device</code> checkbox."
}
]
},
{
"function": "dev_Location",
"type": "string",
"maxLength": 250,
"default_value": "",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Device Location"
}
],
"description": [
{
"language_code": "en_us",
"string": "The location of the device."
}
]
},
{
"function": "dev_Archived",
"type": "integer.checkbox",
"default_value": 0,
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Archived"
}
],
"description": [
{
"language_code": "en_us",
"string": "Indicates whether the device is archived. The default value of the <code>Archived</code> checkbox."
}
]
},
{
"function": "dev_Network_Node_MAC_ADDR",
"type": "string",
"default_value": "",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Network Node MAC Address"
}
],
"description": [
{
"language_code": "en_us",
"string": "The MAC address of the network node."
}
]
},
{
"function": "dev_Network_Node_port",
"type": "readonly",
"default_value": 0,
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Network Node Port"
}
],
"description": [
{
"language_code": "en_us",
"string": "The port number of the network node. Uneditable."
}
]
},
{
"function": "dev_Icon",
"type": "string",
"default_value": "",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Device Icon"
}
],
"description": [
{
"language_code": "en_us",
"string": "The icon associated with the device. Check the <a href=\"https://github.com/jokob-sk/Pi.Alert/blob/main/docs/ICONS.md\" target=\"_blank\">documentation on icons</a> for more details."
}
]
}
],
"required": [
"dev_MAC",
"dev_Name",
"dev_Owner",
"dev_FirstConnection",
"dev_LastConnection",
"dev_LastIP",
"dev_StaticIP",
"dev_ScanCycle",
"dev_LogEvents",
"dev_AlertEvents",
"dev_AlertDeviceDown",
"dev_SkipRepeated",
"dev_LastNotification",
"dev_PresentLastScan",
"dev_NewDevice",
"dev_Location",
"dev_Archived",
"dev_Network_Node_MAC_ADDR",
"dev_Network_Node_port",
"dev_Icon"
],
"additionalProperties": false
}

View File

@@ -0,0 +1 @@
This plugin will not be loaded

View File

@@ -0,0 +1,7 @@
## Overview
Plugin to run regular Internet connectivity and IP checks.
### Usage
- Check the Settings page for details.

View File

@@ -0,0 +1,430 @@
{
"code_name": "internet_ip",
"unique_prefix": "INTRNT",
"enabled": true,
"mapped_to_table": "CurrentScan",
"data_filters": [
{
"compare_column": "Object_PrimaryID",
"compare_operator": "==",
"compare_field_id": "txtMacFilter",
"compare_js_template": "'{value}'.toString()",
"compare_use_quotes": true
}
],
"data_source": "script",
"show_ui": true,
"localized": [
"display_name",
"description",
"icon"
],
"display_name": [
{
"language_code": "en_us",
"string": "Internet check"
}
],
"icon": [
{
"language_code": "en_us",
"string": "<i class=\"fa-solid fa-globe\"></i>"
}
],
"description": [
{
"language_code": "en_us",
"string": "A plugin to check your internet connectivity and IP."
}
],
"params": [
{
"name": "prev_ip",
"type": "sql",
"value": "SELECT dev_LastIP FROM Devices WHERE dev_MAC = 'Internet' "
},
{
"name": "DIG_GET_IP_ARG",
"type": "setting",
"value": "DIG_GET_IP_ARG",
"base64": true
}
],
"settings": [
{
"function": "RUN",
"events": ["run"],
"type": "text.select",
"default_value": "schedule",
"options": [
"disabled",
"once",
"schedule",
"always_after_scan"
],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "When to run"
},
{
"language_code": "es_es",
"string": "Cuándo ejecutar"
},
{
"language_code": "de_de",
"string": "Wann laufen"
}
],
"description": [
{
"language_code": "en_us",
"string": "When the plugin should run. An hourly or daily <code>SCHEDULE</code> is a good option."
}
]
},
{
"function": "CMD",
"type": "readonly",
"default_value": "python3 /home/pi/pialert/front/plugins/internet_ip/script.py prev_ip={prev_ip} DIG_GET_IP_ARG={DIG_GET_IP_ARG}",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Command"
},
{
"language_code": "es_es",
"string": "Comando"
},
{
"language_code": "de_de",
"string": "Befehl"
}
],
"description": [
{
"language_code": "en_us",
"string": "Command to run. This can not be changed"
},
{
"language_code": "es_es",
"string": "Comando a ejecutar. Esto no se puede cambiar"
},
{
"language_code": "de_de",
"string": "Befehl zum Ausführen. Dies kann nicht geändert werden"
}
]
},
{
"function": "RUN_SCHD",
"type": "text",
"default_value": "*/5 * * * *",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Schedule"
},
{
"language_code": "es_es",
"string": "Schedule"
},
{
"language_code": "de_de",
"string": "Schedule"
}
],
"description": [
{
"language_code": "en_us",
"string": "Only enabled if you select <code>schedule</code> in the <a href=\"#INTRNT_RUN\"><code>INTRNT_RUN</code> setting</a>. Make sure you enter the schedule in the correct cron-like format (e.g. validate at <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). For example entering <code>0 4 * * *</code> will run the scan after 4 am in the <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</code> you set above</a>. Will be run NEXT time the time passes."
},
{
"language_code": "es_es",
"string": "Solo está habilitado si selecciona <code>schedule</code> en la configuración <a href=\"#INTRNT_RUN\"><code>INTRNT_RUN</code></a>. Asegúrese de ingresar la programación en el formato similar a cron correcto (por ejemplo, valide en <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). Por ejemplo, ingresar <code>0 4 * * *</code> ejecutará el escaneo después de las 4 a.m. en el <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</ código> que configuró arriba</a>. Se ejecutará la PRÓXIMA vez que pase el tiempo."
},
{
"language_code": "de_de",
"string": "Nur aktiviert, wenn Sie <code>schedule</code> in der <a href=\"#INTRNT_RUN\"><code>INTRNT_RUN</code>-Einstellung</a> auswählen. Stellen Sie sicher, dass Sie den Zeitplan im richtigen Cron-ähnlichen Format eingeben (z. B. validieren unter <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). Wenn Sie beispielsweise <code>0 4 * * *</code> eingeben, wird der Scan nach 4 Uhr morgens in der <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</ ausgeführt. Code> den Sie oben festgelegt haben</a>. Wird das NÄCHSTE Mal ausgeführt, wenn die Zeit vergeht."
}
]
},
{
"function": "RUN_TIMEOUT",
"type": "integer",
"default_value": 30,
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Run timeout"
},
{
"language_code": "es_es",
"string": "Tiempo límite de ejecución"
},
{
"language_code": "de_de",
"string": "Zeitüberschreitung"
}
],
"description": [
{
"language_code": "en_us",
"string": "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted."
},
{
"language_code": "es_es",
"string": "Tiempo máximo en segundos para esperar a que finalice el script. Si se supera este tiempo, el script se cancela."
},
{
"language_code": "de_de",
"string": "Maximale Zeit in Sekunden, die auf den Abschluss des Skripts gewartet werden soll. Bei Überschreitung dieser Zeit wird das Skript abgebrochen."
}
]
},
{
"function": "WATCH",
"type": "text.multiselect",
"default_value": [
"Watched_Value1"
],
"options": [
"Watched_Value1",
"Watched_Value2",
"Watched_Value3",
"Watched_Value4"
],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Watched"
},
{
"language_code": "es_es",
"string": "Visto"
}
],
"description": [
{
"language_code": "en_us",
"string": "Send a notification if selected values change. Use <code>CTRL + Click</code> to select/deselect. <ul> <li><code>Watched_Value1</code> is Previous IP (not recommended)</li><li><code>Watched_Value2</code> unused</li><li><code>Watched_Value3</code> unused </li><li><code>Watched_Value4</code> unused </li></ul>"
}
]
},
{
"function": "REPORT_ON",
"type": "text.multiselect",
"default_value":["new","watched-changed"],
"options": ["new","watched-changed","watched-not-changed", "missing-in-last-scan"],
"localized": ["name", "description"],
"name" :[{
"language_code":"en_us",
"string" : "Report on"
},
{
"language_code":"es_es",
"string" : "Informar sobre"
} ] ,
"description":[{
"language_code":"en_us",
"string" : "Send a notification only on these statuses. <code>new</code> means a new unique (unique combination of PrimaryId and SecondaryId) object was discovered. <code>watched-changed</code> means that selected <code>Watched_ValueN</code> columns changed."
},
{
"language_code":"es_es",
"string" : "Envíe una notificación solo en estos estados. <code>new</code> significa que se descubrió un nuevo objeto único (una combinación única de PrimaryId y SecondaryId). <code>watched-changed</code> significa que las columnas <code>Watched_ValueN</code> seleccionadas cambiaron."
}]
}
],
"database_column_definitions": [
{
"column": "Object_PrimaryID",
"mapped_to_column": "cur_MAC",
"css_classes": "col-sm-2",
"show": true,
"type": "device_name_mac",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "MAC"
},
{
"language_code": "es_es",
"string": "MAC"
}
]
},
{
"column": "Object_SecondaryID",
"mapped_to_column": "cur_IP",
"css_classes": "col-sm-2",
"show": true,
"type": "device_ip",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "IP"
},
{
"language_code": "es_es",
"string": "IP"
}
]
},
{
"column": "Watched_Value1",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Extra"
}
]
},
{
"column": "Dummy",
"mapped_to_column": "cur_ScanMethod",
"mapped_to_column_data": {
"value": "INTRNT"
},
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Scan method"
},
{
"language_code": "es_es",
"string": "Método de escaneo"
}
]
},
{
"column": "DateTimeCreated",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Created"
},
{
"language_code": "es_es",
"string": "Creado"
}
]
},
{
"column": "DateTimeChanged",
"mapped_to_column": "cur_DateTime",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Changed"
},
{
"language_code": "es_es",
"string": "Cambiado"
}
]
},
{
"column": "Status",
"css_classes": "col-sm-1",
"show": true,
"type": "replace",
"default_value": "",
"options": [
{
"equals": "watched-not-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-square-check'></i><div></div>"
},
{
"equals": "watched-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-triangle-exclamation'></i></div>"
},
{
"equals": "new",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-circle-plus'></i></div>"
},
{
"equals": "missing-in-last-scan",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-question'></i></div>"
}
],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Status"
},
{
"language_code": "es_es",
"string": "Estado"
}
]
}
]
}

View File

@@ -0,0 +1,123 @@
#!/usr/bin/env python
# test script by running:
# /home/pi/pialert/front/plugins/internet_ip/script.py TBD
import os
import pathlib
import argparse
import sys
import hashlib
import csv
import subprocess
import re
import base64
import sqlite3
from io import StringIO
from datetime import datetime
sys.path.append("/home/pi/pialert/front/plugins")
sys.path.append('/home/pi/pialert/pialert')
from plugin_helper import Plugin_Object, Plugin_Objects, decodeBase64
from logger import mylog, append_line_to_file
from helper import timeNowTZ, check_IP_format
from const import logPath, pialertPath, fullDbPath
CUR_PATH = str(pathlib.Path(__file__).parent.resolve())
LOG_FILE = os.path.join(CUR_PATH, 'script.log')
RESULT_FILE = os.path.join(CUR_PATH, 'last_result.log')
def main():
mylog('verbose', ['[INTRNT] In script'])
parser = argparse.ArgumentParser(description='Check internet connectivity and IP')
parser.add_argument('prev_ip', action="store", help="Previous IP address to compare against the current IP")
parser.add_argument('DIG_GET_IP_ARG', action="store", help="Arguments for the 'dig' command to retrieve the IP address")
values = parser.parse_args()
PREV_IP = values.prev_ip.split('=')[1]
DIG_GET_IP_ARG = values.DIG_GET_IP_ARG.split('=b')[1] # byte64 encoded
mylog('verbose', ['[INTRNT] DIG_GET_IP_ARG: ', DIG_GET_IP_ARG])
# Decode the base64-encoded value to get the actual value in ASCII format.
DIG_GET_IP_ARG = base64.b64decode(DIG_GET_IP_ARG).decode('ascii')
mylog('verbose', [f'[INTRNT] DIG_GET_IP_ARG resolved: {DIG_GET_IP_ARG} '])
# perform the new IP lookup
new_internet_IP = check_internet_IP( PREV_IP, DIG_GET_IP_ARG)
plugin_objects = Plugin_Objects(RESULT_FILE)
plugin_objects.add_object(
primaryId = 'Internet', # MAC (Device Name)
secondaryId = new_internet_IP, # IP Address
watched1 = f'Previous IP: {PREV_IP}',
watched2 = '',
watched3 = '',
watched4 = '',
extra = f'Previous IP: {PREV_IP}',
foreignKey = 'Internet')
plugin_objects.write_result_file()
mylog('verbose', ['[INTRNT] Finished '])
return 0
#===============================================================================
# INTERNET IP CHANGE
#===============================================================================
def check_internet_IP ( PREV_IP, DIG_GET_IP_ARG ):
# Get Internet IP
mylog('verbose', ['[INTRNT] - Retrieving Internet IP'])
internet_IP = get_internet_IP(DIG_GET_IP_ARG)
mylog('verbose', [f'[INTRNT] Current internet_IP : {internet_IP}'])
# Check previously stored IP
previous_IP = '0.0.0.0'
if PREV_IP is not None and len(PREV_IP) > 0 :
previous_IP = PREV_IP
mylog('verbose', [f'[INTRNT] previous_IP : {previous_IP}'])
# logging
append_line_to_file (logPath + '/IP_changes.log', '['+str(timeNowTZ()) +']\t'+ internet_IP +'\n')
return internet_IP
#-------------------------------------------------------------------------------
def get_internet_IP (DIG_GET_IP_ARG):
# Using 'dig'
dig_args = ['dig', '+short'] + DIG_GET_IP_ARG.strip().split()
try:
cmd_output = subprocess.check_output (dig_args, universal_newlines=True)
except subprocess.CalledProcessError as e:
mylog('none', [e.output])
cmd_output = '' # no internet
# Check result is an IP
IP = check_IP_format (cmd_output)
# Handle invalid response
if IP == '':
IP = '0.0.0.0'
return IP
#===============================================================================
# BEGIN
#===============================================================================
if __name__ == '__main__':
main()

View File

@@ -1,5 +1,5 @@
{
"code_name": "Devices.known",
"code_name": "known_template",
"template_type": "database-entry",
"unique_prefix": "KNWN",
"enabled": true,
@@ -56,7 +56,8 @@
"trigger": [
{
"object_event": "new",
"object_filter": ""
"object_filter": "",
"object": "Devices"
}
],
"steps": [

View File

@@ -319,6 +319,7 @@
"settings":[
{
"function": "RUN",
"events": ["run"],
"type": "text.select",
"default_value":"disabled",
"options": ["disabled", "once", "schedule", "always_after_scan", "on_new_device"],

View File

@@ -54,15 +54,14 @@ def main():
timeoutSec = values.timeoutSec[0].split('=')[1]
# Printing the extracted base64-encoded subnet information.
print(userSubnetsParamBase64)
print(timeoutSec)
mylog('verbose', [f'[PHOLUS] { userSubnetsParamBase64 }'])
mylog('verbose', [f'[PHOLUS] { timeoutSec }'])
# Decode the base64-encoded subnet information to get the actual subnet information in ASCII format.
userSubnetsParam = base64.b64decode(userSubnetsParamBase64).decode('ascii')
# Print the decoded subnet information.
print('userSubnetsParam:')
print(userSubnetsParam)
mylog('verbose', [f'[PHOLUS] userSubnetsParam { userSubnetsParam } '])
# Check if the decoded subnet information contains multiple subnets separated by commas.
# If it does, split the string into a list of individual subnets.
@@ -101,7 +100,7 @@ def execute_pholus_scan(userSubnets, timeoutSec):
timeoutPerSubnet = float(timeoutSec) / len(userSubnets)
print(timeoutPerSubnet)
mylog('verbose', [f'[PHOLUS] { timeoutPerSubnet } '])
# scan each interface
@@ -110,7 +109,7 @@ def execute_pholus_scan(userSubnets, timeoutSec):
temp = interface.split("--interface=")
if len(temp) != 2:
mylog('none', ["[PholusScan] Skip scan (need interface in format '192.168.1.0/24 --inteface=eth0'), got: ", interface])
mylog('none', ["[PHOLUS] Skip scan (need interface in format '192.168.1.0/24 --inteface=eth0'), got: ", interface])
return
mask = temp[0].strip()
@@ -118,7 +117,7 @@ def execute_pholus_scan(userSubnets, timeoutSec):
pholus_output_list = execute_pholus_on_interface (interface, timeoutPerSubnet, mask)
print(pholus_output_list)
mylog('verbose', [f'[PHOLUS] { pholus_output_list } '])
result_list += pholus_output_list
@@ -134,8 +133,8 @@ def execute_pholus_on_interface(interface, timeoutSec, mask):
# logging & updating app state
mylog('verbose', ['[PholusScan] Scan: Pholus for ', str(timeoutSec), 's ('+ str(round(int(timeoutSec) / 60, 1)) +'min)'])
mylog('verbose', ["[PholusScan] Pholus scan on [interface] ", interface, " [mask] " , mask])
mylog('verbose', ['[PHOLUS] Scan: Pholus for ', str(timeoutSec), 's ('+ str(round(int(timeoutSec) / 60, 1)) +'min)'])
mylog('verbose', ["[PHOLUS] Pholus scan on [interface] ", interface, " [mask] " , mask])
# the scan always lasts 2x as long, so the desired user time from settings needs to be halved
adjustedTimeout = str(round(int(timeoutSec) / 2, 0))
@@ -151,15 +150,15 @@ def execute_pholus_on_interface(interface, timeoutSec, mask):
output = subprocess.check_output (pholus_args, universal_newlines=True, stderr=subprocess.STDOUT, timeout=(timeoutSec + 30))
except subprocess.CalledProcessError as e:
# An error occured, handle it
mylog('none', ['[PholusScan]', e.output])
mylog('none', ["[PholusScan] Error - Pholus Scan - check logs"])
mylog('none', ['[PHOLUS]', e.output])
mylog('none', ["[PHOLUS] Error - Pholus Scan - check logs"])
except subprocess.TimeoutExpired as timeErr:
mylog('none', ['[PholusScan] Pholus TIMEOUT - the process forcefully terminated as timeout reached'])
mylog('none', ['[PHOLUS] Pholus TIMEOUT - the process forcefully terminated as timeout reached'])
if output == "": # check if the subprocess failed
mylog('none', ['[PholusScan] Scan: Pholus FAIL - check logs'])
mylog('none', ['[PHOLUS] Scan: Pholus FAIL - check logs'])
else:
mylog('verbose', ['[PholusScan] Scan: Pholus SUCCESS'])
mylog('verbose', ['[PHOLUS] Scan: Pholus SUCCESS'])
# check the last run output
f = open(logPath + '/pialert_pholus_lastrun.log', 'r+')

View File

@@ -57,6 +57,7 @@
"settings": [
{
"function": "RUN",
"events": ["run"],
"type": "text.select",
"default_value":"disabled",
"options": ["disabled", "once", "schedule", "always_after_scan", "on_new_device"],
@@ -243,11 +244,11 @@
"localized": ["name"],
"name":[{
"language_code":"en_us",
"string" : "IP"
"string" : "Link to device"
},
{
"language_code":"es_es",
"string" : "IP"
"string" : "N/A"
}]
},
{

View File

@@ -42,6 +42,7 @@
"settings":[
{
"function": "RUN",
"events": ["run"],
"type": "text.select",
"default_value":"disabled",
"options": ["disabled", "before_config_save"],

View File

@@ -306,6 +306,7 @@
"settings":[
{
"function": "RUN",
"events": ["run"],
"type": "text.select",
"default_value":"disabled",
"options": ["disabled", "once", "schedule", "always_after_scan", "on_new_device"],
@@ -389,7 +390,7 @@
}],
"description": [{
"language_code":"en_us",
"string" : "Only enabled if you select <code>schedule</code> in the <a href=\"#SNMPDSC_RUN\"><code>SNMPDSC_RUN</code> setting</a>. Make sure you enter the schedule in the correct cron-like format (e.g. validate at <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). For example entering <code>0 4 * * *</code> will run the scan after 4 am in the <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</code> you set above</a>. Will be run NEXT time the time passes."
"string" : "Only enabled if you select <code>schedule</code> in the <a href=\"#SNMPDSC_RUN\"><code>SNMPDSC_RUN</code> setting</a>. Make sure you enter the schedule in the correct cron-like format (e.g. validate at <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). For example entering <code>0 4 * * *</code> will run the scan after 4 am in the <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</code> you set above</a>. Will be run NEXT time the time passes. <br/> It's recommended to use the same schedule interval for all plugins responsible for discovering new devices. "
},
{
"language_code":"es_es",

View File

@@ -47,6 +47,7 @@
"settings": [
{
"function": "RUN",
"events": ["run"],
"type": "text.select",
"default_value":"disabled",
"options": ["disabled", "once", "always_after_scan"],

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
# Inspired by https://github.com/stevehoek/Pi.Alert
# Example call
# python3 /home/pi/pialert/front/plugins/unifi_import/script.py username=pialert password=passw0rd host=192.168.1.1 site=default protocol=https port=8443 version='UDMP-unifiOS'
# python3 /home/pi/pialert/front/plugins/unifi_import/script.py username=pialert password=passw0rd host=192.168.1.1 site=default protocol=https port=443 verifyssl=false version='UDMP-unifiOS'
# python3 /home/pi/pialert/front/plugins/unifi_import/script.py username=pialert password=passw0rd host=192.168.1.1 sites=sdefault port=8443 verifyssl=false version=v5
from __future__ import unicode_literals
@@ -28,6 +28,7 @@ from logger import mylog
CUR_PATH = str(pathlib.Path(__file__).parent.resolve())
LOG_FILE = os.path.join(CUR_PATH, 'script.log')
RESULT_FILE = os.path.join(CUR_PATH, 'last_result.log')
LOCK_FILE = os.path.join(CUR_PATH, 'full_run.lock')
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
@@ -39,7 +40,7 @@ def main():
# init global variables
global UNIFI_USERNAME, UNIFI_PASSWORD, UNIFI_HOST, UNIFI_SITES, PORT, VERIFYSSL, VERSION
global UNIFI_USERNAME, UNIFI_PASSWORD, UNIFI_HOST, UNIFI_SITES, PORT, VERIFYSSL, VERSION, FULL_IMPORT
parser = argparse.ArgumentParser(description='Import devices from a UNIFI controller')
@@ -51,9 +52,13 @@ def main():
parser.add_argument('port', action="store", help="Usually 8443")
parser.add_argument('verifyssl', action="store", help="verify SSL certificate [true|false]")
parser.add_argument('version', action="store", help="The base version of the controller API [v4|v5|unifiOS|UDMP-unifiOS]")
parser.add_argument('fullimport', action="store", help="Defines if a full import or only online devices hould be imported [disabled|once|always]")
values = parser.parse_args()
# parse output
plugin_objects = Plugin_Objects(RESULT_FILE)
@@ -69,6 +74,7 @@ def main():
PORT = values.port.split('=')[1]
VERIFYSSL = values.verifyssl.split('=')[1]
VERSION = values.version.split('=')[1]
FULL_IMPORT = values.fullimport.split('=')[1]
plugin_objects = get_entries(plugin_objects)
@@ -79,9 +85,14 @@ def main():
# .............................................
def get_entries(plugin_objects):
def get_entries(plugin_objects: Plugin_Objects) -> Plugin_Objects:
global VERIFYSSL
# check if the full run must be run:
lock_file_value = read_lock_file()
perform_full_run = check_full_run_state(FULL_IMPORT, lock_file_value)
sites = []
if ',' in UNIFI_SITES:
@@ -148,7 +159,7 @@ def get_entries(plugin_objects):
# get_users() returns all clients known by the controller
for user in c.get_users():
mylog('verbose', [f'{json.dumps(user)}'])
#mylog('verbose', [f'{json.dumps(user)}'])
name = get_unifi_val(user, 'name')
hostName = get_unifi_val(user, 'hostname')
@@ -157,7 +168,7 @@ def get_entries(plugin_objects):
status = 1 if user['mac'] in online_macs else 0
if status == 1:
if status == 1 or perform_full_run is True:
ipTmp = get_unifi_val(user, 'last_ip')
@@ -174,11 +185,17 @@ def get_entries(plugin_objects):
extra=get_unifi_val(user, 'last_connection_network_name')
)
# check if the lockfile needs to be adapted
mylog('verbose', [f'[UNFIMP] check if Lock file needs to be modified'])
set_lock_file_value(FULL_IMPORT, lock_file_value)
mylog('verbose', [f'[UNFIMP] Found {len(plugin_objects)} Clients overall'])
return plugin_objects
# -----------------------------------------------------------------------------
def get_unifi_val(obj, key):
@@ -192,8 +209,8 @@ def get_unifi_val(obj, key):
return 'null'
# -----------------------------------------------------------------------------
# -----------------------------------------------------------------------------
def set_name(name: str, hostName: str) -> str:
if name != 'null':
@@ -205,6 +222,45 @@ def set_name(name: str, hostName: str) -> str:
else:
return 'null'
# -----------------------------------------------------------------------------
def set_lock_file_value(config_value: str, lock_file_value: bool) -> None:
mylog('verbose', [f'[UNFIMP] Lock Params: config_value={config_value}, lock_file_value={lock_file_value}'])
# set lock if 'once' is set and the lock is not set
if config_value == 'once' and lock_file_value is False:
out = 1
# reset lock if not 'once' is set and the lock is present
elif config_value != 'once' and lock_file_value is True:
out = 0
else:
mylog('verbose', [f'[UNFIMP] No change on lock file needed'])
return
mylog('verbose', [f'[UNFIMP] Setting lock value for "full import" to {out}'])
with open(LOCK_FILE, 'w') as lock_file:
lock_file.write(str(out))
# -----------------------------------------------------------------------------
def read_lock_file() -> bool:
try:
with open(LOCK_FILE, 'r') as lock_file:
return bool(int(lock_file.readline()))
except (FileNotFoundError, ValueError):
return False
# -----------------------------------------------------------------------------
def check_full_run_state(config_value: str, lock_file_value: bool) -> bool:
if config_value == 'always' or (config_value == 'once' and lock_file_value == False):
mylog('verbose', [f'[UNFIMP] Full import needs to be done: config_value: {config_value} and lock_file_value: {lock_file_value}'])
return True
else:
mylog('verbose', [f'[UNFIMP] Full import NOT needed: config_value: {config_value} and lock_file_value: {lock_file_value}'])
return False
#===============================================================================
# BEGIN
#===============================================================================

View File

@@ -0,0 +1,7 @@
## Overview
A plugin to retrieve a MAC and vendor database to identify vendors for devices. The Plugin result objects will be a list of vendors mapped to the devices where the vendor was previously unknown.
### Usage
- Check the Settings page for details.

View File

@@ -0,0 +1,560 @@
{
"code_name": "vendor_update",
"unique_prefix": "VNDRPDT",
"enabled": true,
"data_source": "script",
"show_ui": true,
"localized": [
"display_name",
"description",
"icon"
],
"display_name": [
{
"language_code": "en_us",
"string": "Vendor update"
}
],
"icon": [
{
"language_code": "en_us",
"string": "<i class=\"fa-solid fa-landmark-flag\"></i>"
}
],
"description": [
{
"language_code": "en_us",
"string": "A plugin to schedule vendor database updates for mac based vendor resolution."
}
],
"params": [],
"settings": [
{
"function": "RUN",
"events": ["run"],
"type": "text.select",
"default_value": "schedule",
"options": [
"disabled",
"once",
"schedule",
"always_after_scan"
],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "When to run"
},
{
"language_code": "es_es",
"string": "Cuándo ejecutar"
},
{
"language_code": "de_de",
"string": "Wann laufen"
}
],
"description": [
{
"language_code": "en_us",
"string": "When the plugin should run. An overnight weekly <code>SCHEDULE</code> is recommended."
}
]
},
{
"function": "CMD",
"type": "readonly",
"default_value": "python3 /home/pi/pialert/front/plugins/vendor_update/script.py",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Command"
},
{
"language_code": "es_es",
"string": "Comando"
},
{
"language_code": "de_de",
"string": "Befehl"
}
],
"description": [
{
"language_code": "en_us",
"string": "Command to run. This can not be changed"
},
{
"language_code": "es_es",
"string": "Comando a ejecutar. Esto no se puede cambiar"
},
{
"language_code": "de_de",
"string": "Befehl zum Ausführen. Dies kann nicht geändert werden"
}
]
},
{
"function": "RUN_SCHD",
"type": "text",
"default_value": "0 4 * * 3",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Schedule"
},
{
"language_code": "es_es",
"string": "Schedule"
},
{
"language_code": "de_de",
"string": "Schedule"
}
],
"description": [
{
"language_code": "en_us",
"string": "Only enabled if you select <code>schedule</code> in the <a href=\"#VNDRPDT_RUN\"><code>VNDRPDT_RUN</code> setting</a>. Make sure you enter the schedule in the correct cron-like format (e.g. validate at <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). For example entering <code>0 4 * * *</code> will run the scan after 4 am in the <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</code> you set above</a>. Will be run NEXT time the time passes."
},
{
"language_code": "es_es",
"string": "Solo está habilitado si selecciona <code>schedule</code> en la configuración <a href=\"#VNDRPDT_RUN\"><code>VNDRPDT_RUN</code></a>. Asegúrese de ingresar la programación en el formato similar a cron correcto (por ejemplo, valide en <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). Por ejemplo, ingresar <code>0 4 * * *</code> ejecutará el escaneo después de las 4 a.m. en el <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</ código> que configuró arriba</a>. Se ejecutará la PRÓXIMA vez que pase el tiempo."
},
{
"language_code": "de_de",
"string": "Nur aktiviert, wenn Sie <code>schedule</code> in der <a href=\"#VNDRPDT_RUN\"><code>VNDRPDT_RUN</code>-Einstellung</a> auswählen. Stellen Sie sicher, dass Sie den Zeitplan im richtigen Cron-ähnlichen Format eingeben (z. B. validieren unter <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). Wenn Sie beispielsweise <code>0 4 * * *</code> eingeben, wird der Scan nach 4 Uhr morgens in der <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</ ausgeführt. Code> den Sie oben festgelegt haben</a>. Wird das NÄCHSTE Mal ausgeführt, wenn die Zeit vergeht."
}
]
},
{
"function": "RUN_TIMEOUT",
"type": "integer",
"default_value": 600,
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Run timeout"
},
{
"language_code": "es_es",
"string": "Tiempo límite de ejecución"
},
{
"language_code": "de_de",
"string": "Zeitüberschreitung"
}
],
"description": [
{
"language_code": "en_us",
"string": "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted."
},
{
"language_code": "es_es",
"string": "Tiempo máximo en segundos para esperar a que finalice el script. Si se supera este tiempo, el script se cancela."
},
{
"language_code": "de_de",
"string": "Maximale Zeit in Sekunden, die auf den Abschluss des Skripts gewartet werden soll. Bei Überschreitung dieser Zeit wird das Skript abgebrochen."
}
]
},
{
"function": "WATCH",
"type": "text.multiselect",
"default_value": [
"Watched_Value1"
],
"options": [
"Watched_Value1",
"Watched_Value2",
"Watched_Value3",
"Watched_Value4"
],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Watched"
},
{
"language_code": "es_es",
"string": "Visto"
}
],
"description": [
{
"language_code": "en_us",
"string": "Send a notification if selected values change. Use <code>CTRL + Click</code> to select/deselect. <ul> <li><code>Watched_Value1</code> is vendor name</li><li><code>Watched_Value2</code> is device name</li><li><code>Watched_Value3</code> unused </li><li><code>Watched_Value4</code> unused </li></ul>"
}
]
},
{
"function": "REPORT_ON",
"type": "text.multiselect",
"default_value": [
"new",
"watched-changed"
],
"options": [
"new",
"watched-changed",
"watched-not-changed",
"missing-in-last-scan"
],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Report on"
},
{
"language_code": "es_es",
"string": "Informar sobre"
}
],
"description": [
{
"language_code": "en_us",
"string": "Send a notification only on these statuses. <code>new</code> means a new unique (unique combination of PrimaryId and SecondaryId) object was discovered. <code>watched-changed</code> means that selected <code>Watched_ValueN</code> columns changed."
},
{
"language_code": "es_es",
"string": "Envíe una notificación solo en estos estados. <code>new</code> significa que se descubrió un nuevo objeto único (combinación única de PrimaryId y SecondaryId). <code>watched-changed</code> significa que seleccionó <code>Watched_ValueN Las columnas </code> cambiaron."
}
]
}
],
"database_column_definitions": [
{
"column": "Index",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "N/A"
},
{
"language_code": "es_es",
"string": "N/A"
}
]
},
{
"column": "Plugin",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "N/A"
},
{
"language_code": "es_es",
"string": "N/A"
}
]
},
{
"column": "Object_PrimaryID",
"mapped_to_column": "cur_MAC",
"css_classes": "col-sm-2",
"show": true,
"type": "device_mac",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "MAC address"
},
{
"language_code": "es_es",
"string": "Dirección MAC"
}
]
},
{
"column": "Object_SecondaryID",
"mapped_to_column": "cur_IP",
"css_classes": "col-sm-2",
"show": true,
"type": "device_ip",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "IP"
},
{
"language_code": "es_es",
"string": "IP"
}
]
},
{
"column": "DateTimeCreated",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Created"
},
{
"language_code": "es_es",
"string": "Creado"
}
]
},
{
"column": "DateTimeChanged",
"mapped_to_column": "cur_DateTime",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Changed"
},
{
"language_code": "es_es",
"string": "Cambiado"
}
]
},
{
"column": "Dummy",
"mapped_to_column": "cur_ScanMethod",
"mapped_to_column_data": {
"value": "VNDRPDT"
},
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Scan method"
},
{
"language_code": "es_es",
"string": "Método de escaneo"
}
]
},
{
"column": "Watched_Value1",
"mapped_to_column": "cur_Vendor",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Vendor"
}
]
},
{
"column": "Watched_Value2",
"mapped_to_column": "cur_Name",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Hostname"
},
{
"language_code": "es_es",
"string": "Nombre de host"
}
]
},
{
"column": "Watched_Value3",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "N/A"
}
]
},
{
"column": "Watched_Value4",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "N/A"
}
]
},
{
"column": "UserData",
"css_classes": "col-sm-2",
"show": false,
"type": "textbox_save",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Comments"
},
{
"language_code": "es_es",
"string": "Comentarios"
}
]
},
{
"column": "Extra",
"css_classes": "col-sm-3",
"show": false,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "N/A"
}
]
},
{
"column": "Status",
"css_classes": "col-sm-1",
"show": true,
"type": "replace",
"default_value": "",
"options": [
{
"equals": "watched-not-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-square-check'></i><div></div>"
},
{
"equals": "watched-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-triangle-exclamation'></i></div>"
},
{
"equals": "new",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-circle-plus'></i></div>"
},
{
"equals": "missing-in-last-scan",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-question'></i></div>"
}
],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Status"
},
{
"language_code": "es_es",
"string": "Estado"
}
]
}
]
}

View File

@@ -0,0 +1,134 @@
#!/usr/bin/env python
# test script by running:
# /home/pi/pialert/front/plugins/db_cleanup/script.py pluginskeephistory=250 hourstokeepnewdevice=48 daystokeepevents=90
import os
import pathlib
import argparse
import sys
import hashlib
import subprocess
import csv
import sqlite3
from io import StringIO
from datetime import datetime
sys.path.append("/home/pi/pialert/front/plugins")
sys.path.append('/home/pi/pialert/pialert')
from plugin_helper import Plugin_Object, Plugin_Objects, decodeBase64
from logger import mylog, append_line_to_file
from helper import timeNowTZ
from const import logPath, pialertPath
from device import query_MAC_vendor
CUR_PATH = str(pathlib.Path(__file__).parent.resolve())
LOG_FILE = os.path.join(CUR_PATH, 'script.log')
RESULT_FILE = os.path.join(CUR_PATH, 'last_result.log')
def main():
mylog('verbose', ['[VNDRPDT] In script'])
# Get newest DB
update_vendor_database()
# Resolve missing vendors
plugin_objects = Plugin_Objects(RESULT_FILE)
plugin_objects = update_vendors('/home/pi/pialert/db/pialert.db', plugin_objects)
plugin_objects.write_result_file()
mylog('verbose', ['[VNDRPDT] Update complete'])
return 0
#===============================================================================
# Update device vendors database
#===============================================================================
def update_vendor_database():
# Update vendors DB (iab oui)
mylog('verbose', [' Updating vendors DB (iab & oui)'])
update_args = ['sh', pialertPath + '/back/update_vendors.sh']
# Execute command
try:
# try runnning a subprocess safely
update_output = subprocess.check_output (update_args)
except subprocess.CalledProcessError as e:
# An error occured, handle it
mylog('none', [' FAILED: Updating vendors DB, set LOG_LEVEL=debug for more info'])
mylog('none', [e.output])
# ------------------------------------------------------------------------------
# resolve missing vendors
def update_vendors (dbPath, plugin_objects):
# Connect to the PiAlert SQLite database
conn = sqlite3.connect(dbPath)
sql = conn.cursor()
# Initialize variables
recordsToUpdate = []
ignored = 0
notFound = 0
mylog('verbose', [' Searching devices vendor'])
# Get devices without a vendor
sql.execute ("""SELECT
dev_MAC,
dev_LastIP,
dev_Name,
dev_Vendor
FROM Devices
WHERE dev_Vendor = '(unknown)'
OR dev_Vendor = ''
OR dev_Vendor IS NULL
""")
devices = sql.fetchall()
conn.commit()
# Close the database connection
conn.close()
# All devices loop
for device in devices:
# Search vendor in HW Vendors DB
vendor = query_MAC_vendor (device[0])
if vendor == -1 :
notFound += 1
elif vendor == -2 :
ignored += 1
else :
plugin_objects.add_object(
primaryId = device[0], # MAC (Device Name)
secondaryId = device[1], # IP Address (always 0.0.0.0)
watched1 = vendor,
watched2 = device[2], # Device name
watched3 = "",
watched4 = "",
extra = "",
foreignKey = device[0]
)
# Print log
mylog('verbose', [" Devices Ignored : ", ignored])
mylog('verbose', [" Devices with missing vendor : ", len(devices)])
mylog('verbose', [" Vendors Not Found : ", notFound])
mylog('verbose', [" Vendors updated : ", len(plugin_objects) ])
return plugin_objects
#===============================================================================
# BEGIN
#===============================================================================
if __name__ == '__main__':
main()

View File

@@ -309,6 +309,7 @@
"settings":[
{
"function": "RUN",
"events": ["run"],
"type": "text.select",
"default_value":"disabled",
"options": ["disabled", "once", "schedule", "always_after_scan", "on_new_device"],

View File

@@ -34,8 +34,8 @@ function initFields() {
// if the current mac has changed, reinitialize the data
if(mac != undefined && $("#txtMacFilter").val() != mac)
{
$("#txtMacFilter").val(mac);
console.log("UPDATE");
getData();
}
@@ -512,6 +512,7 @@ function shouldBeShown(entry, pluginObj)
compare_use_quotes = dataFilters[i].compare_use_quotes;
compare_field_id_value = $(`#${compare_field_id}`).val();
// apply filter i sthe filter field has a valid value
if(compare_field_id_value != undefined && compare_field_id_value != '--')
{
// valid value

View File

@@ -150,17 +150,6 @@
<!-- box-body -->
<div class="box-body table-responsive">
<!-- spinner -->
<div id="loading" style="display: none">
<div class="pa_semitransparent-panel"></div>
<div class="panel panel-default pa_spinner">
<table>
<td width="130px" align="middle"><?= lang("Presence_Loading");?></td>
<td><i class="ion ion-ios-loop-strong fa-spin fa-2x fa-fw"></td>
</table>
</div>
</div>
<!-- Calendar -->
<div id="calendar"></div>
</div>
@@ -344,9 +333,9 @@ function initializeCalendar () {
loading: function( isLoading, view ) {
if (isLoading) {
$("#loading").show();
showSpinner();
} else {
$("#loading").hide();
hideSpinner();
}
}

View File

@@ -575,17 +575,25 @@ while ($row = $result -> fetchArray (SQLITE3_ASSOC)) {
showModalOk('WARNING', "<?= lang("settings_missing_block")?>");
} else
{
// trigger a save settings event in the backend
$.ajax({
method: "POST",
url: "php/server/util.php",
data: {
function: 'savesettings',
settings: JSON.stringify(collectSettings()) },
success: function(data, textStatus) {
success: function(data, textStatus) {
showModalOk ('Result', data );
// Remove navigation prompt "Are you sure you want to leave..."
window.onbeforeunload = null;
// Reloads the current page
setTimeout("window.location.reload()", 3000);
}
});
}
@@ -607,26 +615,46 @@ while ($row = $result -> fetchArray (SQLITE3_ASSOC)) {
var result = data;
if(key == "Back_Settings_Imported")
{
fileModificationTime = <?php echo filemtime($confPath)*1000;?>;
importedMiliseconds = parseInt(result.match( /\d+/g ).join('')); // sanitize the string and get only the numbers
result = (new Date(importedMiliseconds)).toLocaleString("en-UK", { timeZone: "<?php echo $timeZone?>" }); //.toDateString("");
// check if displayed settings are outdated
if(fileModificationTime > importedMiliseconds)
{
showModalOk('WARNING: Outdated settings displayed', "<?= lang("settings_old")?>");
}
} else{
result = result.replaceAll('"', '');
}
result = result.replaceAll('"', '');
document.getElementById(targetId).innerHTML = result.replaceAll('"', '');
});
}
// -----------------------------------------------------------------------------
function handleLoadingDialog()
{
$.get('api/app_state.json?nocache=' + Date.now(), function(appState) {
fileModificationTime = <?php echo filemtime($confPath)*1000;?>;
console.log(appState["settingsImported"]*1000)
importedMiliseconds = parseInt((appState["settingsImported"]*1000));
humanReadable = (new Date(importedMiliseconds)).toLocaleString("en-UK", { timeZone: "<?php echo $timeZone?>" });
console.log(humanReadable.replaceAll('"', ''))
// check if displayed settings are outdated
// if(fileModificationTime > importedMiliseconds)
if(appState["showSpinner"] || fileModificationTime > importedMiliseconds)
{
showSpinner("settings_old")
setTimeout("handleLoadingDialog()", 1000);
} else
{
hideSpinner()
}
document.getElementById('lastImportedTime').innerHTML = humanReadable;
})
}
// -----------------------------------------------------------------------------
function toggleAllSettings()
@@ -722,6 +750,10 @@ while ($row = $result -> fetchArray (SQLITE3_ASSOC)) {
// ---------------------------------------------------------
// Show last time settings have been imported
getParam("lastImportedTime", "Back_Settings_Imported", skipCache = true);
// getParam("lastImportedTime", "Back_Settings_Imported", skipCache = true);
handleLoadingDialog()
</script>

View File

@@ -4,13 +4,16 @@ server {
index index.php;
rewrite /pialert/(.*) / permanent;
location ~* \.php$ {
fastcgi_pass unix:/run/php/php7.4-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_connect_timeout 75;
fastcgi_send_timeout 600;
fastcgi_read_timeout 600;
}
location ~* \.php$ {
set $php_version "7.4"; # Default PHP version
# Use the selected PHP version
fastcgi_pass unix:/run/php/php$php_version-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_connect_timeout 75;
fastcgi_send_timeout 600;
fastcgi_read_timeout 600;
}
}

98
install/install.sh Executable file
View File

@@ -0,0 +1,98 @@
#!/bin/bash
# Check if script is run as root
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root. Please use 'sudo'."
exit 1
fi
# Set environment variables
PORT=20211
INSTALL_DIR=/home/pi # Specify the installation directory here
# Update and upgrade system packages
# apt-get update
# apt-get upgrade -y
# Install Git
apt-get install -y git
# Clone the application repository
git clone https://github.com/jokob-sk/Pi.Alert "$INSTALL_DIR/pialert"
# Install dependencies
apt-get install -y \
tini snmp ca-certificates curl libwww-perl arp-scan perl apt-utils cron sudo \
nginx-light php php-cgi php-fpm php-sqlite3 php-curl sqlite3 dnsutils net-tools \
python3 iproute2 nmap python3-pip zip systemctl usbutils traceroute
# ---------------------------------------------------------------
# alternate dependencies
sudo apt-get install -y \
nginx nginx-core mtr mtr-tiny php-fpm php7.4-fpm
sudo apt install php-cli php7.4 php7.4-fpm -y
sudo apt install php7.4-sqlite3 -y
sudo phpenmod -v 7.4 sqlite3
sudo apt install net-tools -y
curl -sSL https://bootstrap.pypa.io/get-pip.py | python3
# ---------------------------------------------------------------
# Install Python packages
pip3 install requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi
# Update alternatives for Python
update-alternatives --install /usr/bin/python python /usr/bin/python3 10
# Configure Nginx
echo "Configure Nginx"
echo "---------------------------------------------------------------"
sudo rm -R /var/www/html
ln -s $INSTALL_DIR/pialert/front /var/www/html
sudo rm /etc/nginx/sites-available/default
sudo ln -s "$INSTALL_DIR/pialert/install/default" /etc/nginx/sites-available/default
sudo sed -i 's/listen 80/listen '"$PORT"'/g' /etc/nginx/sites-available/default
echo "Run the hardware vendors update"
echo "---------------------------------------------------------------"
# Run the hardware vendors update
"$INSTALL_DIR/pialert/back/update_vendors.sh"
# Create a backup of pialert.conf
cp "$INSTALL_DIR/pialert/config/pialert.conf" "$INSTALL_DIR/pialert/back/pialert.conf_bak"
# Create a backup of pialert.db
cp "$INSTALL_DIR/pialert/db/pialert.db" "$INSTALL_DIR/pialert/back/pialert.db_bak"
# Create buildtimestamp.txt
date +%s > "$INSTALL_DIR/pialert/front/buildtimestamp.txt"
chmod -R a+rwx $INSTALL_DIR
chmod -R a+rwx /var/www/html
chmod -R a+rw $INSTALL_DIR/front/log
chmod -R a+rw $INSTALL_DIR/config
/etc/init.d/php7.4-fpm start
# /etc/init.d/php8.2-fpm start
/etc/init.d/nginx start
# Start Nginx and your application to start at boot (if needed)
systemctl start nginx
systemctl enable nginx
systemctl enable pi-alert
sudo systemctl restart nginx
# Provide instructions or additional setup if needed
echo "Installation completed. Please configure any additional settings for your application."
cd $INSTALL_DIR/pialert
"$INSTALL_DIR/pialert/dockerfiles/start.sh"
# Exit the script
exit 0

View File

@@ -8,7 +8,6 @@ The original pilaert.py code is now moved to this new folder and split into diff
|```__init__.py```| an empty init file|
|```README.md```| this readme file|
|**publishers**| a folder containing all modules used to publish the results|
|**scanners**| a folder containing all modules used to scan for devices |
|```api.py```| updating the API endpoints with the relevant data. (Should move to publishers)|
|```const.py```| A place to define the constants for Pi.Alert like log path or config path.|
|```conf.py```| conf.py holds the configuration variables and makes them available for all modules. It is also the <b>workaround</b> for global variables that need to be resolved at some point|
@@ -17,7 +16,6 @@ The original pilaert.py code is now moved to this new folder and split into diff
|```helper.py```| Helper as the name suggest contains multiple little functions and methods used in many of the other modules and helps keep things clean |
|```initialise.py```| Initiatlise sets up the environment and makes everything ready to go |
|```logger.py```| Logger is there the keep all the logs organised and looking identical. |
|```mac_vendor.py```| This module runs and manages the ``` update_vendors.sh ``` script from within Pi.Alert |
|```networscan.py```| Networkscan orchestrates the actual scanning of the network, calling the individual scanners and managing the results |
|```plugin.py```| This is where the plugins get integrated into the backend of Pi.Alert |
|```reporting.py```| Reporting generates the email, html and json reports to be sent by the publishers |
@@ -36,12 +34,5 @@ publishers generally have a check_config method as well as a send method.
|```pushsafer.py```| integrate with pushsafer |
|```webhook.py```| integrate via webhook |
## scanners
different methods to scan the network for devices or to find more details about the discovered devices
| Module | Description |
|--------|-----------|
|```__init__.py```| an empty init file (oops missing in the repo)|
|```internet.py```| discover the internet interface and check the external IP also manage Dynamic DNS |
|```nmapscan.py```| use Nmap to discover more about devices |

View File

@@ -18,7 +18,6 @@ El código pilaert.py original ahora se mueve a esta nueva carpeta y se divide e
|```helper.py```| Helper como su nombre indica contiene múltiples pequeñas funciones y métodos utilizados en muchos de los otros módulos y ayuda a mantener las cosas limpias |
|```initialise.py```| Initiatlise prepara el entorno y deja todo listo para funcionar |
|```logger.py```| Logger está ahí para mantener todos los registros organizados y con el mismo aspecto |
|```mac_vendor.py```| Este módulo ejecuta y gestiona el ``` update_vendors.sh ``` script desde Pi.Alert |
|```networscan.py```| El escaneado de red organiza el escaneado real de la red, llamando a los escáneres individuales y gestionando los resultados |
|```plugin.py```| Aquí es donde los plugins se integran en el backend de Pi.Alert |
|```reporting.py```| La generación de informes genera los informes de correo electrónico, html y json que deben enviar los editores |

View File

@@ -24,19 +24,15 @@ import multiprocessing
import conf
from const import *
from logger import mylog
from helper import filePermissions, isNewVersion, timeNowTZ, updateState, get_setting_value
from helper import filePermissions, timeNowTZ, updateState, get_setting_value
from api import update_api
from networkscan import process_scan
from initialise import importConfigs
from mac_vendor import update_devices_MAC_vendors
from database import DB, get_all_devices
from reporting import check_and_run_event, send_notifications
from reporting import send_notifications
from plugin_utils import check_and_run_user_event
from plugin import run_plugin_scripts
# different scanners
from scanners.internet import check_internet_IP
#===============================================================================
#===============================================================================
@@ -54,15 +50,10 @@ main structure of Pi Alert
run plugins (once)
run frontend events
update API
run scans
run plugins (scheduled)
check internet IP
check vendor
run "scan_network()"
processing scan results
run plugins (after Scan)
reporting
cleanup
run plugins (scheduled)
processing scan results
run plugins (after Scan)
reporting - could be replaced by run flows TODO
end loop
"""
@@ -71,10 +62,6 @@ def main ():
mylog('none', [f'[conf.tz] Setting up ...{conf.tz}'])
# indicates, if a new version is available
conf.newVersionAvailable = False
# check file permissions and fix if required
filePermissions()
@@ -93,6 +80,9 @@ def main ():
mylog('debug', '[MAIN] Starting loop')
# Header + init app state
updateState("Initializing")
while True:
# re-load user configuration and plugins
@@ -101,19 +91,7 @@ def main ():
# update time started
conf.loop_start_time = timeNowTZ()
# TODO fix these
loop_start_time = conf.loop_start_time # TODO fix
last_update_vendors = conf.last_update_vendors
last_cleanup = conf.last_cleanup
last_version_check = conf.last_version_check
# check if new version is available / only check once an hour
if conf.last_version_check + datetime.timedelta(hours=1) < conf.loop_start_time :
# if newVersionAvailable is already true the function does nothing and returns true again
mylog('debug', [f"[Version check] Last version check timestamp: {conf.last_version_check}"])
conf.last_version_check = conf.loop_start_time
conf.newVersionAvailable = isNewVersion(conf.newVersionAvailable)
# Handle plugins executed ONCE
if conf.plugins_once_run == False:
@@ -121,7 +99,7 @@ def main ():
conf.plugins_once_run = True
# check if there is a front end initiated event which needs to be executed
pluginsState = check_and_run_event(db, pluginsState)
pluginsState = check_and_run_user_event(db, pluginsState)
# Update API endpoints
update_api(db)
@@ -131,10 +109,9 @@ def main ():
# last time any scan or maintenance/upkeep was run
conf.last_scan_run = loop_start_time
last_internet_IP_scan = conf.last_internet_IP_scan
# Header
updateState(db,"Process: Start")
updateState("Process: Start")
# Timestamp
startTime = loop_start_time
@@ -146,19 +123,6 @@ def main ():
# determine run/scan type based on passed time
# --------------------------------------------
# check for changes in Internet IP
if last_internet_IP_scan + datetime.timedelta(minutes=3) < loop_start_time:
conf.cycle = 'internet_IP'
last_internet_IP_scan = loop_start_time
check_internet_IP(db)
# Update vendors once a week
if conf.last_update_vendors + datetime.timedelta(days = 7) < loop_start_time:
conf.last_update_vendors = loop_start_time
conf.cycle = 'update_vendors'
mylog('verbose', ['[MAIN] cycle:',conf.cycle])
update_devices_MAC_vendors(db)
# Run splugin scripts which are set to run every timne after a scans finished
pluginsState = run_plugin_scripts(db,'always_after_scan', pluginsState)
@@ -186,18 +150,11 @@ def main ():
# send all configured notifications
send_notifications(db)
# clean up the DB once an hour
if conf.last_cleanup + datetime.timedelta(hours = 1) < loop_start_time:
conf.last_cleanup = loop_start_time
conf.cycle = 'cleanup'
mylog('verbose', ['[MAIN] cycle:',conf.cycle])
db.cleanup_database(startTime, conf.DAYS_TO_KEEP_EVENTS, get_setting_value('PHOLUS_DAYS_DATA'), conf.HRS_TO_KEEP_NEWDEV, conf.PLUGINS_KEEP_HIST)
# Commit SQL
db.commitDB()
# Footer
updateState(db,"Process: Wait")
updateState("Process: Wait")
mylog('verbose', ['[MAIN] Process: Wait'])
else:
# do something

View File

@@ -75,7 +75,7 @@ class api_endpoint_class:
index = index + 1
# cehck if API endpoints have changed or if it's a new one
# check if API endpoints have changed or if it's a new one
if not found or changed:
mylog('verbose', [f'[API] Updating {self.fileName} file in /front/api'])

View File

@@ -6,7 +6,6 @@
# These are global variables, not config items and should not exist !
mySettings = []
mySettingsSQLsafe = []
debug_force_notification = False
cycle = 1
userSubnets = []
mySchedules = [] # bad solution for global - TO-DO
@@ -21,10 +20,7 @@ plugins_once_run = False
newVersionAvailable = False
time_started = ''
startTime = ''
last_internet_IP_scan = ''
last_scan_run = ''
last_cleanup = ''
last_update_vendors = ''
last_version_check = ''
arpscan_devices = []
@@ -32,75 +28,77 @@ arpscan_devices = []
mqtt_connected_to_broker = False
mqtt_sensors = []
client = None # mqtt client
# for notifications
# ACTUAL CONFIGRATION ITEMS set to defaults
# -------------------------------------------
# General
SCAN_SUBNETS = ['192.168.1.0/24 --interface=eth1', '192.168.1.0/24 --interface=eth0']
LOG_LEVEL = 'verbose'
TIMEZONE = 'Europe/Berlin'
PIALERT_WEB_PROTECTION = False
PIALERT_WEB_PASSWORD = '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92'
INCLUDED_SECTIONS = ['internet', 'new_devices', 'down_devices', 'events']
DAYS_TO_KEEP_EVENTS = 90
REPORT_DASHBOARD_URL = 'http://pi.alert/'
DIG_GET_IP_ARG = '-4 myip.opendns.com @resolver1.opendns.com'
UI_LANG = 'English'
UI_PRESENCE = ['online', 'offline', 'archived']
# -------------------------------------------
SCAN_SUBNETS = ['192.168.1.0/24 --interface=eth1', '192.168.1.0/24 --interface=eth0']
LOG_LEVEL = 'verbose'
TIMEZONE = 'Europe/Berlin'
DIG_GET_IP_ARG = '-4 myip.opendns.com @resolver1.opendns.com'
UI_LANG = 'English'
UI_PRESENCE = ['online', 'offline', 'archived']
PIALERT_WEB_PROTECTION = False
PIALERT_WEB_PASSWORD = '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92'
INCLUDED_SECTIONS = ['new_devices', 'down_devices', 'events']
DAYS_TO_KEEP_EVENTS = 90
REPORT_DASHBOARD_URL = 'http://pi.alert/'
# -------------------------------------------
# Notification gateways
# -------------------------------------------
# Email
REPORT_MAIL = False
SMTP_SERVER = ''
SMTP_PORT = 587
REPORT_TO = 'user@gmail.com'
REPORT_FROM = 'Pi.Alert <user@gmail.com>'
SMTP_SKIP_LOGIN = False
SMTP_USER = ''
SMTP_PASS = ''
SMTP_SKIP_TLS = False
SMTP_FORCE_SSL = False
REPORT_MAIL = False
SMTP_SERVER = ''
SMTP_PORT = 587
REPORT_TO = 'user@gmail.com'
REPORT_FROM = 'Pi.Alert <user@gmail.com>'
SMTP_SKIP_LOGIN = False
SMTP_USER = ''
SMTP_PASS = ''
SMTP_SKIP_TLS = False
SMTP_FORCE_SSL = False
# Webhooks
REPORT_WEBHOOK = False
WEBHOOK_URL = ''
REPORT_WEBHOOK = False
WEBHOOK_URL = ''
WEBHOOK_PAYLOAD = 'json'
WEBHOOK_REQUEST_METHOD = 'GET'
WEBHOOK_SECRET = ''
# Apprise
REPORT_APPRISE = False
APPRISE_HOST = ''
APPRISE_URL = ''
REPORT_APPRISE = False
APPRISE_HOST = ''
APPRISE_URL = ''
APPRISE_PAYLOAD = 'html'
# NTFY
REPORT_NTFY = False
NTFY_HOST ='https://ntfy.sh'
NTFY_TOPIC =''
NTFY_USER = ''
NTFY_PASSWORD = ''
REPORT_NTFY = False
NTFY_HOST = 'https://ntfy.sh'
NTFY_TOPIC = ''
NTFY_USER = ''
NTFY_PASSWORD = ''
# PUSHSAFER
REPORT_PUSHSAFER = False
PUSHSAFER_TOKEN = 'ApiKey'
REPORT_PUSHSAFER = False
PUSHSAFER_TOKEN = 'ApiKey'
# MQTT
REPORT_MQTT = False
MQTT_BROKER = ''
MQTT_PORT = 1883
MQTT_USER = ''
MQTT_PASSWORD = ''
MQTT_QOS = 0
MQTT_DELAY_SEC = 2
REPORT_MQTT = False
MQTT_BROKER = ''
MQTT_PORT = 1883
MQTT_USER = ''
MQTT_PASSWORD = ''
MQTT_QOS = 0
MQTT_DELAY_SEC = 2
# DynDNS
DDNS_ACTIVE = False
DDNS_DOMAIN = 'your_domain.freeddns.org'
DDNS_USER = 'dynu_user'
DDNS_PASSWORD = 'A0000000B0000000C0000000D0000000'
DDNS_UPDATE_URL = 'https://api.dynu.com/nic/update?'
# -------------------------------------------
# Misc
# -------------------------------------------
# API
API_CUSTOM_SQL = 'SELECT * FROM Devices WHERE dev_PresentLastScan = 0'
API_CUSTOM_SQL = 'SELECT * FROM Devices WHERE dev_PresentLastScan = 0'

View File

@@ -4,10 +4,9 @@
# PATHS
#===============================================================================
pialertPath = '/home/pi/pialert'
#pialertPath ='/home/roland/repos/Pi.Alert'
confPath = "/config/pialert.conf"
dbPath = '/db/pialert.db'
confPath = "/config/pialert.conf"
dbPath = '/db/pialert.db'
pluginsPath = pialertPath + '/front/plugins'
@@ -15,10 +14,8 @@ logPath = pialertPath + '/front/log'
apiPath = pialertPath + '/front/api/'
fullConfPath = pialertPath + confPath
fullDbPath = pialertPath + dbPath
vendorsDB = '/usr/share/arp-scan/ieee-oui.txt'
vendorsPath6 = '/usr/share/arp-scan/ieee-oui.txt'
vendorsPath9 = '/usr/share/arp-scan/ieee-iab.txt'

View File

@@ -74,86 +74,6 @@ class DB():
return arr
#===============================================================================
# Cleanup / upkeep database
#===============================================================================
def cleanup_database (self, startTime, DAYS_TO_KEEP_EVENTS, PHOLUS_DAYS_DATA, HRS_TO_KEEP_NEWDEV, PLUGINS_KEEP_HIST):
"""
Cleaning out old records from the tables that don't need to keep all data.
"""
# Header
#updateState(self,"Upkeep: Clean DB")
mylog('verbose', ['[DB Cleanup] Upkeep Database:' ])
# Cleanup Online History
mylog('verbose', ['[DB Cleanup] Online_History: Delete all but keep latest 150 entries'])
self.sql.execute ("""DELETE from Online_History where "Index" not in (
SELECT "Index" from Online_History
order by Scan_Date desc limit 150)""")
mylog('verbose', ['[DB Cleanup] Optimize Database'])
# Cleanup Events
mylog('verbose', [f'[DB Cleanup] Events: Delete all older than {str(DAYS_TO_KEEP_EVENTS)} days (DAYS_TO_KEEP_EVENTS setting)'])
self.sql.execute (f"""DELETE FROM Events
WHERE eve_DateTime <= date('now', '-{str(DAYS_TO_KEEP_EVENTS)} day')""")
# Trim Plugins_History entries to less than PLUGINS_KEEP_HIST setting per unique "Plugin" column entry
mylog('verbose', [f'[DB Cleanup] Plugins_History: Trim Plugins_History entries to less than {str(PLUGINS_KEEP_HIST)} per Plugin (PLUGINS_KEEP_HIST setting)'])
# Build the SQL query to delete entries that exceed the limit per unique "Plugin" column entry
delete_query = f"""DELETE FROM Plugins_History
WHERE "Index" NOT IN (
SELECT "Index"
FROM (
SELECT "Index",
ROW_NUMBER() OVER(PARTITION BY "Plugin" ORDER BY DateTimeChanged DESC) AS row_num
FROM Plugins_History
) AS ranked_objects
WHERE row_num <= {str(PLUGINS_KEEP_HIST)}
);"""
self.sql.execute(delete_query)
# Cleanup Pholus_Scan
if PHOLUS_DAYS_DATA != 0:
mylog('verbose', ['[DB Cleanup] Pholus_Scan: Delete all older than ' + str(PHOLUS_DAYS_DATA) + ' days (PHOLUS_DAYS_DATA setting)'])
# todo: improvement possibility: keep at least N per mac
self.sql.execute (f"""DELETE FROM Pholus_Scan
WHERE Time <= date('now', '-{str(PHOLUS_DAYS_DATA)} day')""")
# Cleanup New Devices
if HRS_TO_KEEP_NEWDEV != 0:
mylog('verbose', [f'[DB Cleanup] Devices: Delete all New Devices older than {str(HRS_TO_KEEP_NEWDEV)} hours (HRS_TO_KEEP_NEWDEV setting)'])
self.sql.execute (f"""DELETE FROM Devices
WHERE dev_NewDevice = 1 AND dev_FirstConnection < date('now', '+{str(HRS_TO_KEEP_NEWDEV)} hour')""")
# De-dupe (de-duplicate) from the Plugins_Objects table
# TODO This shouldn't be necessary - probably a concurrency bug somewhere in the code :(
mylog('verbose', ['[DB Cleanup] Plugins_Objects: Delete all duplicates'])
self.sql.execute("""
DELETE FROM Plugins_Objects
WHERE rowid > (
SELECT MIN(rowid) FROM Plugins_Objects p2
WHERE Plugins_Objects.Plugin = p2.Plugin
AND Plugins_Objects.Object_PrimaryID = p2.Object_PrimaryID
AND Plugins_Objects.Object_SecondaryID = p2.Object_SecondaryID
AND Plugins_Objects.UserData = p2.UserData
)
""")
# De-Dupe (de-duplicate - remove duplicate entries) from the Pholus_Scan table
mylog('verbose', ['[DB Cleanup] Pholus_Scan: Delete all duplicates'])
self.sql.execute ("""DELETE FROM Pholus_Scan
WHERE rowid > (
SELECT MIN(rowid) FROM Pholus_Scan p2
WHERE Pholus_Scan.MAC = p2.MAC
AND Pholus_Scan.Value = p2.Value
AND Pholus_Scan.Record_Type = p2.Record_Type
);""")
# Shrink DB
mylog('verbose', ['[DB Cleanup] Shrink Database'])
self.sql.execute ("VACUUM;")
self.commitDB()
#-------------------------------------------------------------------------------
def upgradeDB(self):
@@ -443,7 +363,6 @@ class DB():
# indicates, if CurrentScan table is available
self.sql.execute("DROP TABLE IF EXISTS CurrentScan;")
self.sql.execute(""" CREATE TABLE CurrentScan (
cur_ScanCycle INTEGER,
cur_MAC STRING(50) NOT NULL COLLATE NOCASE,
cur_IP STRING(50) NOT NULL COLLATE NOCASE,
cur_Vendor STRING(250),

View File

@@ -3,28 +3,16 @@ import subprocess
import conf
import re
from helper import timeNowTZ, get_setting, get_setting_value,resolve_device_name_dig, resolve_device_name_pholus
from scanners.internet import check_IP_format, get_internet_IP
from helper import timeNowTZ, get_setting, get_setting_value,resolve_device_name_dig, resolve_device_name_pholus, check_IP_format
from logger import mylog, print_log
from mac_vendor import query_MAC_vendor
from const import vendorsPath6, vendorsPath9
#-------------------------------------------------------------------------------
def save_scanned_devices (db):
sql = db.sql #TO-DO
cycle = 1 # always 1, only one cycle supported
# handled by the ARPSCAN plugin
# handled by the Pi-hole plugin
# Check Internet connectivity
internet_IP = get_internet_IP( conf.DIG_GET_IP_ARG )
# TESTING - Force IP
# internet_IP = ""
if internet_IP != "" :
sql.execute (f"""INSERT INTO CurrentScan (cur_ScanCycle, cur_MAC, cur_IP, cur_Vendor, cur_ScanMethod)
VALUES ( 1, 'Internet', '{internet_IP}', Null, 'queryDNS') """)
# #76 Add Local MAC of default local interface
# BUGFIX #106 - Device that pialert is running
@@ -50,7 +38,7 @@ def save_scanned_devices (db):
# Check if local mac has been detected with other methods
sql.execute (f"SELECT COUNT(*) FROM CurrentScan WHERE cur_MAC = '{local_mac}'")
if sql.fetchone()[0] == 0 :
sql.execute (f"""INSERT INTO CurrentScan (cur_ScanCycle, cur_MAC, cur_IP, cur_Vendor, cur_ScanMethod) VALUES ( 1, '{local_mac}', '{local_ip}', Null, 'local_MAC') """)
sql.execute (f"""INSERT INTO CurrentScan (cur_MAC, cur_IP, cur_Vendor, cur_ScanMethod) VALUES ( '{local_mac}', '{local_ip}', Null, 'local_MAC') """)
#-------------------------------------------------------------------------------
def print_scan_stats(db):
@@ -165,71 +153,6 @@ def create_new_devices (db):
sql.execute (sqlQuery, (startTime, startTime) )
# Pi-hole - Insert events for new devices
# NOT STRICYLY NECESARY (Devices can be created through CurrentScan)
# Bugfix #2 - Pi-hole devices w/o IP
# mylog('debug','[New Devices] 3 Pi-hole Events')
# sql.execute ("""INSERT INTO Events (eve_MAC, eve_IP, eve_DateTime,
# eve_EventType, eve_AdditionalInfo,
# eve_PendingAlertEmail)
# SELECT PH_MAC, IFNULL (PH_IP,'-'), ?, 'New Device',
# '(Pi-Hole) ' || PH_Vendor, 1
# FROM PiHole_Network
# WHERE NOT EXISTS (SELECT 1 FROM Devices
# WHERE dev_MAC = PH_MAC) """,
# (startTime, ) )
# # Pi-hole - Create New Devices
# # Bugfix #2 - Pi-hole devices w/o IP
# mylog('debug','[New Devices] 4 Pi-hole Create devices')
# sqlQuery = f"""INSERT INTO Devices (dev_MAC, dev_name, dev_Vendor,
# dev_LastIP, dev_FirstConnection, dev_LastConnection,
# {newDevColumns})
# SELECT PH_MAC, PH_Name, PH_Vendor, IFNULL (PH_IP,'-'),
# ?, ?,
# {newDevDefaults}
# FROM PiHole_Network
# WHERE NOT EXISTS (SELECT 1 FROM Devices
# WHERE dev_MAC = PH_MAC) """
# mylog('debug',f'[New Devices] 4 Create devices SQL: {sqlQuery}')
# sql.execute (sqlQuery, (startTime, startTime) )
# # DHCP Leases - Insert events for new devices
# mylog('debug','[New Devices] 5 DHCP Leases Events')
# sql.execute (f"""INSERT INTO Events (eve_MAC, eve_IP, eve_DateTime,
# eve_EventType, eve_AdditionalInfo,
# eve_PendingAlertEmail)
# SELECT DHCP_MAC, DHCP_IP, '{startTime}', 'New Device', '(DHCP lease)',1
# FROM DHCP_Leases
# WHERE NOT EXISTS (SELECT 1 FROM Devices
# WHERE dev_MAC = DHCP_MAC) """)
# # DHCP Leases - Create New Devices
# mylog('debug','[New Devices] 6 DHCP Leases Create devices')
# sqlQuery = f"""INSERT INTO Devices (dev_MAC, dev_name, dev_LastIP,
# dev_Vendor, dev_FirstConnection, dev_LastConnection,
# {newDevColumns})
# SELECT DISTINCT DHCP_MAC,
# (SELECT DHCP_Name FROM DHCP_Leases AS D2
# WHERE D2.DHCP_MAC = D1.DHCP_MAC
# ORDER BY DHCP_DateTime DESC LIMIT 1),
# (SELECT DHCP_IP FROM DHCP_Leases AS D2
# WHERE D2.DHCP_MAC = D1.DHCP_MAC
# ORDER BY DHCP_DateTime DESC LIMIT 1),
# '(unknown)', ?, ?,
# {newDevDefaults}
# FROM DHCP_Leases AS D1
# WHERE NOT EXISTS (SELECT 1 FROM Devices
# WHERE dev_MAC = DHCP_MAC) """
# mylog('debug',f'[New Devices] 6 Create devices SQL: {sqlQuery}')
# sql.execute (sqlQuery, (startTime, startTime) )
mylog('debug','[New Devices] New Devices end')
db.commitDB()
@@ -376,3 +299,43 @@ def check_mac_or_internet(input_str):
return True
else:
return False
#===============================================================================
# Lookup unknown vendors on devices
#===============================================================================
#-------------------------------------------------------------------------------
def query_MAC_vendor (pMAC):
pMACstr = str(pMAC)
# Check MAC parameter
mac = pMACstr.replace (':','')
if len(pMACstr) != 17 or len(mac) != 12 :
return -2 # return -2 if ignored MAC
# Search vendor in HW Vendors DB
mac_start_string6 = mac[0:6]
mac_start_string9 = mac[0:9]
try:
with open(vendorsPath6, 'r') as f:
for line in f:
if line.startswith(mac_start_string6):
vendor = line.split(' ', 1)[1].strip()
mylog('debug', [f"[Vendor Check] Found '{vendor}' for '{pMAC}' in {vendorsPath6}"])
return vendor
with open(vendorsPath9, 'r') as f:
for line in f:
if line.startswith(mac_start_string9):
vendor = line.split(' ', 1)[1].strip()
mylog('debug', [f"[Vendor Check] Found '{vendor}' for '{pMAC}' in {vendorsPath9}"])
return vendor
return -1 # MAC address not found in the database
except FileNotFoundError:
mylog('none', [f"[Vendor Check] Error: Vendors file {vendorsPath} not found."])
return -1

View File

@@ -3,12 +3,12 @@
import io
import sys
import datetime
# from datetime import strptime
import os
import re
import subprocess
import pytz
from pytz import timezone
from datetime import timedelta
import json
import time
from pathlib import Path
@@ -18,8 +18,10 @@ import conf
from const import *
from logger import mylog, logResult
#-------------------------------------------------------------------------------
# DateTime
#-------------------------------------------------------------------------------
# Get the current time in the current TimeZone
def timeNowTZ():
if isinstance(conf.TIMEZONE, str):
tz = pytz.timezone(conf.TIMEZONE)
@@ -31,19 +33,74 @@ def timeNowTZ():
def timeNow():
return datetime.datetime.now().replace(microsecond=0)
#-------------------------------------------------------------------------------
def updateState(db, newState):
# App state
#-------------------------------------------------------------------------------
# A class to manage the application state and to provide a frontend accessible API point
class app_state_class:
def __init__(self, currentState, settingsSaved=None, settingsImported=None, showSpinner=False):
# json file containing the state to communicate with the frontend
stateFile = apiPath + '/app_state.json'
# ?? Why is the state written to the DB?
# The state is written to the DB so the front-end can use the value to display the current state in the header of the app
# The Parameters DB table is used to communicate with the front end.
# if currentState == 'Initializing':
# checkNewVersion(False)
#sql = db.sql
# Update self
self.currentState = currentState
self.lastUpdated = str(timeNowTZ())
# Check if the file exists and init values
if os.path.exists(stateFile):
with open(stateFile, 'r') as json_file:
previousState = json.load(json_file)
self.settingsSaved = previousState.get("settingsSaved", 0)
self.settingsImported = previousState.get("settingsImported", 0)
self.showSpinner = previousState.get("showSpinner", False)
self.isNewVersion = previousState.get("isNewVersion", False)
self.isNewVersionChecked = previousState.get("isNewVersionChecked", 0)
else:
self.settingsSaved = 0
self.settingsImported = 0
self.showSpinner = False
self.isNewVersion = checkNewVersion()
self.isNewVersionChecked = int(timeNow().timestamp())
# Overwrite with provided parameters if supplied
if settingsSaved is not None:
self.settingsSaved = settingsSaved
if settingsImported is not None:
self.settingsImported = settingsImported
if showSpinner is not None:
self.showSpinner = showSpinner
# check for new version every hour and if currently not running new version
if self.isNewVersion is False and self.isNewVersionChecked + 3600 < int(timeNow().timestamp()):
self.isNewVersion = checkNewVersion()
self.isNewVersionChecked = int(timeNow().timestamp())
# Update .json file
with open(stateFile, 'w') as json_file:
json.dump(self, json_file, cls=AppStateEncoder, indent=4)
def isSet(self):
result = False
if self.currentState != "":
result = True
return result
#-------------------------------------------------------------------------------
# method to update the state
def updateState(newState, settingsSaved = None, settingsImported = None, showSpinner = False):
state = app_state_class(newState, settingsSaved, settingsImported, showSpinner)
mylog('debug', '[updateState] changing state to: "' + newState +'"')
db.sql.execute ("UPDATE Parameters SET par_Value='"+ newState +"' WHERE par_ID='Back_App_State'")
db.commitDB()
#-------------------------------------------------------------------------------
def updateSubnets(scan_subnets):
subnets = []
@@ -60,6 +117,8 @@ def updateSubnets(scan_subnets):
#-------------------------------------------------------------------------------
# File system permission handling
#-------------------------------------------------------------------------------
# check RW access of DB and config file
def checkPermissionsOK():
@@ -126,7 +185,7 @@ def initialiseFile(pathToCheck, defaultFile):
mylog('none', ["[Setup] Error copying ("+defaultFile+"). Make sure the app has Read & Write access to " + pathToCheck])
mylog('none', [e.output])
#-------------------------------------------------------------------------------
def filePermissions():
# check and initialize pialert.conf
(confR_access, dbR_access) = checkPermissionsOK() # Initial check
@@ -143,165 +202,8 @@ def filePermissions():
#-------------------------------------------------------------------------------
def bytes_to_string(value):
# if value is of type bytes, convert to string
if isinstance(value, bytes):
value = value.decode('utf-8')
return value
# File manipulation methods
#-------------------------------------------------------------------------------
def if_byte_then_to_str(input):
if isinstance(input, bytes):
input = input.decode('utf-8')
input = bytes_to_string(re.sub('[^a-zA-Z0-9-_\s]', '', str(input)))
return input
#-------------------------------------------------------------------------------
def collect_lang_strings(db, json, pref):
for prop in json["localized"]:
for language_string in json[prop]:
import_language_string(db, language_string["language_code"], pref + "_" + prop, language_string["string"])
#-------------------------------------------------------------------------------
# Creates a JSON object from a DB row
def row_to_json(names, row):
rowEntry = {}
index = 0
for name in names:
rowEntry[name]= if_byte_then_to_str(row[name])
index += 1
return rowEntry
#-------------------------------------------------------------------------------
def import_language_string(db, code, key, value, extra = ""):
db.sql.execute ("""INSERT INTO Plugins_Language_Strings ("Language_Code", "String_Key", "String_Value", "Extra") VALUES (?, ?, ?, ?)""", (str(code), str(key), str(value), str(extra)))
db.commitDB()
#-------------------------------------------------------------------------------
def checkIPV4(ip):
""" Define a function to validate an Ip address
"""
ipRegex = "^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$"
if(re.search(ipRegex, ip)):
return True
else:
return False
#-------------------------------------------------------------------------------
def isNewVersion(newVersion: bool):
mylog('debug', [f"[Version check] New version available? {newVersion}"])
if newVersion == False:
f = open(pialertPath + '/front/buildtimestamp.txt', 'r')
buildTimestamp = int(f.read().strip())
f.close()
data = ""
try:
url = requests.get("https://api.github.com/repos/jokob-sk/Pi.Alert/releases")
text = url.text
data = json.loads(text)
except requests.exceptions.ConnectionError as e:
mylog('minimal', [" Couldn't check for new release."])
data = ""
# make sure we received a valid response and not an API rate limit exceeded message
if data != "" and len(data) > 0 and isinstance(data, list) and "published_at" in data[0]:
dateTimeStr = data[0]["published_at"]
realeaseTimestamp = int(datetime.datetime.strptime(dateTimeStr, '%Y-%m-%dT%H:%M:%SZ').strftime('%s'))
if realeaseTimestamp > buildTimestamp + 600:
mylog('none', ["[Version check] New version of the container available!"])
newVersion = True
return newVersion
#-------------------------------------------------------------------------------
def hide_email(email):
m = email.split('@')
if len(m) == 2:
return f'{m[0][0]}{"*"*(len(m[0])-2)}{m[0][-1] if len(m[0]) > 1 else ""}@{m[1]}'
return email
#-------------------------------------------------------------------------------
def removeDuplicateNewLines(text):
if "\n\n\n" in text:
return removeDuplicateNewLines(text.replace("\n\n\n", "\n\n"))
else:
return text
#-------------------------------------------------------------------------------
def add_json_list (row, list):
new_row = []
for column in row :
column = bytes_to_string(column)
new_row.append(column)
list.append(new_row)
return list
#-------------------------------------------------------------------------------
def sanitize_string(input):
if isinstance(input, bytes):
input = input.decode('utf-8')
value = bytes_to_string(re.sub('[^a-zA-Z0-9-_\s]', '', str(input)))
return value
#-------------------------------------------------------------------------------
def generate_mac_links (html, deviceUrl):
p = re.compile(r'(?:[0-9a-fA-F]:?){12}')
MACs = re.findall(p, html)
for mac in MACs:
html = html.replace('<td>' + mac + '</td>','<td><a href="' + deviceUrl + mac + '">' + mac + '</a></td>')
return html
#-------------------------------------------------------------------------------
def initOrSetParam(db, parID, parValue):
sql = db.sql
sql.execute ("INSERT INTO Parameters(par_ID, par_Value) VALUES('"+str(parID)+"', '"+str(parValue)+"') ON CONFLICT(par_ID) DO UPDATE SET par_Value='"+str(parValue)+"' where par_ID='"+str(parID)+"'")
db.commitDB()
#-------------------------------------------------------------------------------
class json_struc:
def __init__(self, jsn, columnNames):
self.json = jsn
self.columnNames = columnNames
#-------------------------------------------------------------------------------
def get_file_content(path):
@@ -336,16 +238,8 @@ def write_file(pPath, pText):
file.close()
#-------------------------------------------------------------------------------
class noti_struc:
def __init__(self, json, text, html):
self.json = json
self.text = text
self.html = html
# Setting methods
#-------------------------------------------------------------------------------
def isJsonObject(value):
return isinstance(value, dict)
#-------------------------------------------------------------------------------
# Return whole setting touple
def get_setting(key):
@@ -377,6 +271,80 @@ def get_setting_value(key):
return ''
#-------------------------------------------------------------------------------
# IP validation methods
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
def checkIPV4(ip):
""" Define a function to validate an Ip address
"""
ipRegex = "^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$"
if(re.search(ipRegex, ip)):
return True
else:
return False
#-------------------------------------------------------------------------------
def check_IP_format (pIP):
# Check IP format
IPv4SEG = r'(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])'
IPv4ADDR = r'(?:(?:' + IPv4SEG + r'\.){3,3}' + IPv4SEG + r')'
IP = re.search(IPv4ADDR, pIP)
# Return error if not IP
if IP is None :
return ""
# Return IP
return IP.group(0)
#-------------------------------------------------------------------------------
def resolve_device_name_dig (pMAC, pIP):
newName = ""
try :
dig_args = ['dig', '+short', '-x', pIP]
# Execute command
try:
# try runnning a subprocess
newName = subprocess.check_output (dig_args, universal_newlines=True)
except subprocess.CalledProcessError as e:
# An error occured, handle it
mylog('none', ['[device_name_dig] ', e.output])
# newName = "Error - check logs"
return -1
# Check returns
newName = newName.strip()
if len(newName) == 0 :
return -1
# Cleanup
newName = cleanResult(newName)
if newName == "" or len(newName) == 0:
return -1
# Return newName
return newName
# not Found
except subprocess.CalledProcessError :
return -1
#-------------------------------------------------------------------------------
# DNS record (Pholus/Name resolution) cleanup methods
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
# Disclaimer - I'm interfacing with a script I didn't write (pholus3.py) so it's possible I'm missing types of answers
# it's also possible the pholus3.py script can be adjusted to provide a better output to interface with it
@@ -452,43 +420,7 @@ def resolve_device_name_pholus (pMAC, pIP, allRes):
return -1
#-------------------------------------------------------------------------------
def resolve_device_name_dig (pMAC, pIP):
newName = ""
try :
dig_args = ['dig', '+short', '-x', pIP]
# Execute command
try:
# try runnning a subprocess
newName = subprocess.check_output (dig_args, universal_newlines=True)
except subprocess.CalledProcessError as e:
# An error occured, handle it
mylog('none', ['[device_name_dig] ', e.output])
# newName = "Error - check logs"
return -1
# Check returns
newName = newName.strip()
if len(newName) == 0 :
return -1
# Cleanup
newName = cleanResult(newName)
if newName == "" or len(newName) == 0:
return -1
# Return newName
return newName
# not Found
except subprocess.CalledProcessError :
return -1
#-------------------------------------------------------------------------------
def cleanResult(str):
@@ -507,3 +439,179 @@ def cleanResult(str):
str = str[:-1]
return str
#-------------------------------------------------------------------------------
# String manipulation methods
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
def bytes_to_string(value):
# if value is of type bytes, convert to string
if isinstance(value, bytes):
value = value.decode('utf-8')
return value
#-------------------------------------------------------------------------------
def if_byte_then_to_str(input):
if isinstance(input, bytes):
input = input.decode('utf-8')
input = bytes_to_string(re.sub('[^a-zA-Z0-9-_\s]', '', str(input)))
return input
#-------------------------------------------------------------------------------
def hide_email(email):
m = email.split('@')
if len(m) == 2:
return f'{m[0][0]}{"*"*(len(m[0])-2)}{m[0][-1] if len(m[0]) > 1 else ""}@{m[1]}'
return email
#-------------------------------------------------------------------------------
def removeDuplicateNewLines(text):
if "\n\n\n" in text:
return removeDuplicateNewLines(text.replace("\n\n\n", "\n\n"))
else:
return text
#-------------------------------------------------------------------------------
def sanitize_string(input):
if isinstance(input, bytes):
input = input.decode('utf-8')
value = bytes_to_string(re.sub('[^a-zA-Z0-9-_\s]', '', str(input)))
return value
#-------------------------------------------------------------------------------
def generate_mac_links (html, deviceUrl):
p = re.compile(r'(?:[0-9a-fA-F]:?){12}')
MACs = re.findall(p, html)
for mac in MACs:
html = html.replace('<td>' + mac + '</td>','<td><a href="' + deviceUrl + mac + '">' + mac + '</a></td>')
return html
#-------------------------------------------------------------------------------
# JSON methods
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
def isJsonObject(value):
return isinstance(value, dict)
#-------------------------------------------------------------------------------
def add_json_list (row, list):
new_row = []
for column in row :
column = bytes_to_string(column)
new_row.append(column)
list.append(new_row)
return list
#-------------------------------------------------------------------------------
# Checks if the object has a __dict__ attribute. If it does, it assumes that it's an instance of a class and serializes its attributes dynamically.
class AppStateEncoder(json.JSONEncoder):
def default(self, obj):
if hasattr(obj, '__dict__'):
# If the object has a '__dict__', assume it's an instance of a class
return obj.__dict__
return super().default(obj)
#-------------------------------------------------------------------------------
# Creates a JSON object from a DB row
def row_to_json(names, row):
rowEntry = {}
index = 0
for name in names:
rowEntry[name]= if_byte_then_to_str(row[name])
index += 1
return rowEntry
#-------------------------------------------------------------------------------
# Get language strings from plugin JSON
def collect_lang_strings(json, pref, stringSqlParams):
for prop in json["localized"]:
for language_string in json[prop]:
stringSqlParams.append((str(language_string["language_code"]), str(pref + "_" + prop), str(language_string["string"]), ""))
return stringSqlParams
#-------------------------------------------------------------------------------
# Misc
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
def checkNewVersion():
mylog('debug', [f"[Version check] Checking if new version available"])
newVersion = False
f = open(pialertPath + '/front/buildtimestamp.txt', 'r')
buildTimestamp = int(f.read().strip())
f.close()
data = ""
try:
url = requests.get("https://api.github.com/repos/jokob-sk/Pi.Alert/releases")
text = url.text
data = json.loads(text)
except requests.exceptions.ConnectionError as e:
mylog('minimal', ["[Version check] Error: Couldn't check for new release."])
data = ""
# make sure we received a valid response and not an API rate limit exceeded message
if data != "" and len(data) > 0 and isinstance(data, list) and "published_at" in data[0]:
dateTimeStr = data[0]["published_at"]
realeaseTimestamp = int(datetime.datetime.strptime(dateTimeStr, '%Y-%m-%dT%H:%M:%SZ').strftime('%s'))
if realeaseTimestamp > buildTimestamp + 600:
mylog('none', ["[Version check] New version of the container available!"])
newVersion = True
else:
mylog('none', ["[Version check] Running the latest version."])
return newVersion
#-------------------------------------------------------------------------------
def initOrSetParam(db, parID, parValue):
sql = db.sql
sql.execute ("INSERT INTO Parameters(par_ID, par_Value) VALUES('"+str(parID)+"', '"+str(parValue)+"') ON CONFLICT(par_ID) DO UPDATE SET par_Value='"+str(parValue)+"' where par_ID='"+str(parID)+"'")
db.commitDB()
#-------------------------------------------------------------------------------
class json_struc:
def __init__(self, jsn, columnNames):
self.json = jsn
self.columnNames = columnNames
#-------------------------------------------------------------------------------
class noti_struc:
def __init__(self, json, text, html):
self.json = json
self.text = text
self.html = html

View File

@@ -9,7 +9,7 @@ import json
import conf
from const import fullConfPath
from helper import collect_lang_strings, updateSubnets, initOrSetParam, isJsonObject
from helper import collect_lang_strings, updateSubnets, initOrSetParam, isJsonObject, updateState
from logger import mylog
from api import update_api
from scheduler import schedule_class
@@ -73,6 +73,13 @@ def importConfigs (db):
mylog('debug', ['[Import Config] skipping config file import'])
return
# Header
updateState("Import config", showSpinner = True)
# remove all plugin langauge strings
sql.execute("DELETE FROM Plugins_Language_Strings;")
db.commitDB()
mylog('debug', ['[Import Config] importing config file'])
conf.mySettings = [] # reset settings
conf.mySettingsSQLsafe = [] # same as above but safe to be passed into a SQL query
@@ -84,12 +91,14 @@ def importConfigs (db):
# Import setting if found in the dictionary
# General
# ----------------------------------------
conf.LOG_LEVEL = ccd('LOG_LEVEL', 'verbose' , c_d, 'Log verboseness', 'text.select', "['none', 'minimal', 'verbose', 'debug']", 'General')
conf.TIMEZONE = ccd('TIMEZONE', 'Europe/Berlin' , c_d, 'Time zone', 'text', '', 'General')
conf.PLUGINS_KEEP_HIST = ccd('PLUGINS_KEEP_HIST', 250 , c_d, 'Keep history entries', 'integer', '', 'General')
conf.PIALERT_WEB_PROTECTION = ccd('PIALERT_WEB_PROTECTION', False , c_d, 'Enable logon', 'boolean', '', 'General')
conf.PIALERT_WEB_PASSWORD = ccd('PIALERT_WEB_PASSWORD', '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92' , c_d, 'Logon password', 'readonly', '', 'General')
conf.INCLUDED_SECTIONS = ccd('INCLUDED_SECTIONS', ['internet', 'new_devices', 'down_devices', 'events'] , c_d, 'Notify on', 'text.multiselect', "['internet', 'new_devices', 'down_devices', 'events', 'plugins']", 'General')
conf.INCLUDED_SECTIONS = ccd('INCLUDED_SECTIONS', ['new_devices', 'down_devices', 'events'] , c_d, 'Notify on', 'text.multiselect', "['new_devices', 'down_devices', 'events', 'plugins']", 'General')
conf.REPORT_DASHBOARD_URL = ccd('REPORT_DASHBOARD_URL', 'http://pi.alert/' , c_d, 'PiAlert URL', 'text', '', 'General')
conf.DIG_GET_IP_ARG = ccd('DIG_GET_IP_ARG', '-4 myip.opendns.com @resolver1.opendns.com' , c_d, 'DIG arguments', 'text', '', 'General')
conf.UI_LANG = ccd('UI_LANG', 'English' , c_d, 'Language Interface', 'text.select', "['English', 'German', 'Spanish']", 'General')
@@ -97,10 +106,14 @@ def importConfigs (db):
conf.DAYS_TO_KEEP_EVENTS = ccd('DAYS_TO_KEEP_EVENTS', 90 , c_d, 'Delete events days', 'integer', '', 'General')
conf.HRS_TO_KEEP_NEWDEV = ccd('HRS_TO_KEEP_NEWDEV', 0 , c_d, 'Keep new devices for', 'integer', "0", 'General')
conf.API_CUSTOM_SQL = ccd('API_CUSTOM_SQL', 'SELECT * FROM Devices WHERE dev_PresentLastScan = 0' , c_d, 'Custom endpoint', 'text', '', 'General')
conf.NETWORK_DEVICE_TYPES = ccd('NETWORK_DEVICE_TYPES', ['AP', 'Gateway', 'Firewall', 'Hypervisor', 'Powerline', 'Switch', 'WLAN', 'PLC', 'Router','USB LAN Adapter', 'USB WIFI Adapter', 'Internet'] , c_d, 'Network device types', 'list', '', 'General')
# ARPSCAN (+ other settings provided by the ARPSCAN plugin)
# ARPSCAN (+ more settings are provided by the ARPSCAN plugin)
conf.SCAN_SUBNETS = ccd('SCAN_SUBNETS', ['192.168.1.0/24 --interface=eth1', '192.168.1.0/24 --interface=eth0'] , c_d, 'Subnets to scan', 'subnets', '', 'ARPSCAN')
# Notification gateways
# ----------------------------------------
# Email
conf.REPORT_MAIL = ccd('REPORT_MAIL', False , c_d, 'Enable email', 'boolean', '', 'Email', ['test'])
conf.SMTP_SERVER = ccd('SMTP_SERVER', '' , c_d,'SMTP server URL', 'text', '', 'Email')
@@ -119,6 +132,7 @@ def importConfigs (db):
conf.WEBHOOK_PAYLOAD = ccd('WEBHOOK_PAYLOAD', 'json' , c_d, 'Payload type', 'text.select', "['json', 'html', 'text']", 'Webhooks')
conf.WEBHOOK_REQUEST_METHOD = ccd('WEBHOOK_REQUEST_METHOD', 'GET' , c_d, 'Req type', 'text.select', "['GET', 'POST', 'PUT']", 'Webhooks')
conf.WEBHOOK_SIZE = ccd('WEBHOOK_SIZE', 1024 , c_d, 'Payload size', 'integer', '', 'Webhooks')
conf.WEBHOOK_SECRET = ccd('WEBHOOK_SECRET', '' , c_d, 'Secret', 'text', '', 'Webhooks')
# Apprise
conf.REPORT_APPRISE = ccd('REPORT_APPRISE', False , c_d, 'Enable Apprise', 'boolean', '', 'Apprise', ['test'])
@@ -147,14 +161,6 @@ def importConfigs (db):
conf.MQTT_QOS = ccd('MQTT_QOS', 0 , c_d, 'MQTT Quality of Service', 'integer.select', "['0', '1', '2']", 'MQTT')
conf.MQTT_DELAY_SEC = ccd('MQTT_DELAY_SEC', 2 , c_d, 'MQTT delay', 'integer.select', "['2', '3', '4', '5']", 'MQTT')
# DynDNS
conf.DDNS_ACTIVE = ccd('DDNS_ACTIVE', False , c_d, 'Enable DynDNS', 'boolean', '', 'DynDNS')
conf.DDNS_DOMAIN = ccd('DDNS_DOMAIN', 'your_domain.freeddns.org' , c_d, 'DynDNS domain URL', 'text', '', 'DynDNS')
conf.DDNS_USER = ccd('DDNS_USER', 'dynu_user' , c_d, 'DynDNS user', 'text', '', 'DynDNS')
conf.DDNS_PASSWORD = ccd('DDNS_PASSWORD', 'A0000000B0000000C0000000D0000000' , c_d, 'DynDNS password', 'password', '', 'DynDNS')
conf.DDNS_UPDATE_URL = ccd('DDNS_UPDATE_URL', 'https://api.dynu.com/nic/update?' , c_d, 'DynDNS update URL', 'text', '', 'DynDNS')
# Init timezone in case it changed
conf.tz = timezone(conf.TIMEZONE)
@@ -164,18 +170,13 @@ def importConfigs (db):
conf.cycle = ""
conf.plugins_once_run = False
#cron_instance = Cron()
# timestamps of last execution times
conf.startTime = conf.time_started
now_minus_24h = conf.time_started - datetime.timedelta(hours = 24)
conf.startTime = conf.time_started
now_minus_24h = conf.time_started - datetime.timedelta(hours = 24)
# set these times to the past to force the first run
conf.last_internet_IP_scan = now_minus_24h
conf.last_scan_run = now_minus_24h
conf.last_cleanup = now_minus_24h
conf.last_update_vendors = conf.time_started - datetime.timedelta(days = 6) # update vendors 24h after first run and then once a week
conf.last_version_check = now_minus_24h
conf.last_scan_run = now_minus_24h
conf.last_version_check = now_minus_24h
# TODO cleanup later ----------------------------------------------------------------------------------
@@ -192,14 +193,21 @@ def importConfigs (db):
mylog('none', ['[Config] Plugins: Number of dynamically loaded plugins: ', len(conf.plugins)])
# handle plugins
index = 0
for plugin in conf.plugins:
# Header
updateState(f"Import plugin {index} of {len(conf.plugins)}")
index +=1
pref = plugin["unique_prefix"]
print_plugin_info(plugin, ['display_name','description'])
# if plugin["enabled"] == 'true':
stringSqlParams = []
# collect plugin level language strings
collect_lang_strings(db, plugin, pref)
stringSqlParams = collect_lang_strings(plugin, pref, stringSqlParams)
for set in plugin["settings"]:
setFunction = set["function"]
@@ -230,12 +238,17 @@ def importConfigs (db):
# Collect settings related language strings
# Creates an entry with key, for example ARPSCAN_CMD_name
collect_lang_strings(db, set, pref + "_" + set["function"])
stringSqlParams = collect_lang_strings(set, pref + "_" + set["function"], stringSqlParams)
# Collect column related language strings
for clmn in plugin.get('database_column_definitions', []):
# Creates an entry with key, for example ARPSCAN_Object_PrimaryID_name
collect_lang_strings(db, clmn, pref + "_" + clmn.get("column", ""))
stringSqlParams = collect_lang_strings(clmn, pref + "_" + clmn.get("column", ""), stringSqlParams)
# bulk-import language strings
sql.executemany ("""INSERT INTO Plugins_Language_Strings ("Language_Code", "String_Key", "String_Value", "Extra") VALUES (?, ?, ?, ?)""", stringSqlParams )
db.commitDB()
@@ -264,7 +277,7 @@ def importConfigs (db):
# Used to determine the next import
conf.lastImportedConfFile = os.path.getmtime(config_file)
#TO DO this creates a circular reference between API and HELPER !
updateState("Config imported", conf.lastImportedConfFile, conf.lastImportedConfFile, False)
mylog('minimal', '[Config] Imported new config')

View File

@@ -1,113 +0,0 @@
import subprocess
import conf
from const import pialertPath, vendorsDB
from helper import timeNowTZ, updateState
from logger import mylog
#===============================================================================
# UPDATE DEVICE MAC VENDORS
#===============================================================================
def update_devices_MAC_vendors (db, pArg = ''):
sql = db.sql # TO-DO
# Header
updateState(db,"Upkeep: Vendors")
mylog('verbose', ['[', timeNowTZ(), '] Upkeep - Update HW Vendors:' ])
# Update vendors DB (iab oui)
mylog('verbose', [' Updating vendors DB (iab & oui)'])
update_args = ['sh', pialertPath + '/back/update_vendors.sh', pArg]
# Execute command
if conf.LOG_LEVEL == 'debug':
# try runnning a subprocess
update_output = subprocess.check_output (update_args)
else:
try:
# try runnning a subprocess safely
update_output = subprocess.check_output (update_args)
except subprocess.CalledProcessError as e:
# An error occured, handle it
mylog('none', [' FAILED: Updating vendors DB, set LOG_LEVEL=debug for more info'])
mylog('none', [e.output])
# Initialize variables
recordsToUpdate = []
ignored = 0
notFound = 0
# All devices loop
mylog('verbose', [' Searching devices vendor'])
for device in sql.execute ("""SELECT * FROM Devices
WHERE dev_Vendor = '(unknown)'
OR dev_Vendor =''
OR dev_Vendor IS NULL""") :
# Search vendor in HW Vendors DB
vendor = query_MAC_vendor (device['dev_MAC'])
if vendor == -1 :
notFound += 1
elif vendor == -2 :
ignored += 1
else :
recordsToUpdate.append ([vendor, device['dev_MAC']])
# Print log
mylog('verbose', [" Devices Ignored: ", ignored])
mylog('verbose', [" Vendors Not Found:", notFound])
mylog('verbose', [" Vendors updated: ", len(recordsToUpdate) ])
# update devices
sql.executemany ("UPDATE Devices SET dev_Vendor = ? WHERE dev_MAC = ? ",
recordsToUpdate )
# Commit DB
db.commitDB()
if len(recordsToUpdate) > 0:
return True
else:
return False
#-------------------------------------------------------------------------------
def query_MAC_vendor (pMAC):
try :
# BUGFIX #6 - Fix pMAC parameter as numbers
pMACstr = str(pMAC)
# Check MAC parameter
mac = pMACstr.replace (':','')
if len(pMACstr) != 17 or len(mac) != 12 :
return -2
# Search vendor in HW Vendors DB
mac = mac[0:6]
grep_args = ['grep', '-i', mac, vendorsDB]
# Execute command
if conf.LOG_LEVEL == 'debug':
# try runnning a subprocess
grep_output = subprocess.check_output (grep_args)
else:
try:
# try runnning a subprocess
grep_output = subprocess.check_output (grep_args)
except subprocess.CalledProcessError as e:
# An error occured, handle it
mylog('none', ["[Mac Vendor Check] Error: ", e.output])
grep_output = " There was an error, check logs for details"
# Return Vendor
vendor = grep_output[7:]
vendor = vendor.rstrip()
return vendor
# not Found
except subprocess.CalledProcessError :
return -1

View File

@@ -106,7 +106,7 @@ class plugins_state:
def run_plugin_scripts(db, runType, pluginsState = plugins_state()):
# Header
updateState(db,"Run: Plugins")
updateState("Run: Plugins")
mylog('debug', ['[Plugins] Check if any plugins need to be executed on run type: ', runType])
@@ -129,7 +129,7 @@ def run_plugin_scripts(db, runType, pluginsState = plugins_state()):
if shouldRun:
# Header
updateState(db,f"Plugins: {prefix}")
updateState(f"Plugins: {prefix}")
print_plugin_info(plugin, ['display_name'])
mylog('debug', ['[Plugins] CMD: ', get_plugin_setting(plugin, "CMD")["value"]])

View File

@@ -183,3 +183,87 @@ def handle_empty(value):
value = 'null'
return value
#===============================================================================
# Handling of user initialized front-end events
#===============================================================================
#-------------------------------------------------------------------------------
def check_and_run_user_event(db, pluginsState):
sql = db.sql # TO-DO
sql.execute(""" select * from Parameters where par_ID = "Front_Event" """)
rows = sql.fetchall()
event, param = ['','']
if len(rows) > 0 and rows[0]['par_Value'] != 'finished':
keyValue = rows[0]['par_Value'].split('|')
if len(keyValue) == 2:
event = keyValue[0]
param = keyValue[1]
else:
return pluginsState
if event == 'test':
handle_test(param)
if event == 'run':
pluginsState = handle_run(param, db, pluginsState)
# clear event execution flag
sql.execute ("UPDATE Parameters SET par_Value='finished' WHERE par_ID='Front_Event'")
# commit to DB
db.commitDB()
mylog('debug', [f'[MAIN] processScan3: {pluginsState.processScan}'])
return pluginsState
#-------------------------------------------------------------------------------
def handle_run(runType, db, pluginsState):
mylog('minimal', ['[', timeNowTZ(), '] START Run: ', runType])
# run the plugin to run
for plugin in conf.plugins:
if plugin["unique_prefix"] == runType:
pluginsState = execute_plugin(db, plugin, pluginsState)
mylog('minimal', ['[', timeNowTZ(), '] END Run: ', runType])
return pluginsState
#-------------------------------------------------------------------------------
def handle_test(testType):
mylog('minimal', ['[', timeNowTZ(), '] START Test: ', testType])
# Open text sample
sample_txt = get_file_content(pialertPath + '/back/report_sample.txt')
# Open html sample
sample_html = get_file_content(pialertPath + '/back/report_sample.html')
# Open json sample and get only the payload part
sample_json_payload = json.loads(get_file_content(pialertPath + '/back/webhook_json_sample.json'))[0]["body"]["attachments"][0]["text"]
sample_msg = noti_struc(sample_json_payload, sample_txt, sample_html )
if testType == 'Email':
send_email(sample_msg)
elif testType == 'Webhooks':
send_webhook (sample_msg)
elif testType == 'Apprise':
send_apprise (sample_msg)
elif testType == 'NTFY':
send_ntfy (sample_msg)
elif testType == 'PUSHSAFER':
send_pushsafer (sample_msg)
else:
mylog('none', ['[Test Publishers] No test matches: ', testType])
mylog('minimal', ['[Test Publishers] END Test: ', testType])

View File

@@ -1,5 +1,7 @@
import json
import subprocess
import hashlib
import hmac
import conf
from const import logPath
@@ -71,6 +73,7 @@ def send (msg: noti_struc):
}]
}
# DEBUG - Write the json payload into a log file for debugging
write_file (logPath + '/webhook_payload.json', json.dumps(_json_payload))
@@ -81,7 +84,13 @@ def send (msg: noti_struc):
curlParams = ["curl","-i","-H", "Content-Type:application/json" ,"-d", json.dumps(_json_payload), _WEBHOOK_URL]
else:
_WEBHOOK_URL = conf.WEBHOOK_URL
curlParams = ["curl","-i","-X", conf.WEBHOOK_REQUEST_METHOD ,"-H", "Content-Type:application/json" ,"-d", json.dumps(_json_payload), _WEBHOOK_URL]
curlParams = ["curl","-i","-X", conf.WEBHOOK_REQUEST_METHOD , "-H", "Content-Type:application/json", "-d", json.dumps(_json_payload), _WEBHOOK_URL]
# Add HMAC signature if configured
if(conf.WEBHOOK_SECRET != ''):
h = hmac.new(conf.WEBHOOK_SECRET.encode("UTF-8"), json.dumps(_json_payload, separators=(',', ':')).encode(), hashlib.sha256).hexdigest()
curlParams.insert(4,"-H")
curlParams.insert(5,f"X-Webhook-Signature: sha256={h}")
try:
# Execute CURL call

View File

@@ -25,7 +25,6 @@ import const
from const import pialertPath, logPath, apiPath
from helper import noti_struc, generate_mac_links, removeDuplicateNewLines, timeNowTZ, hide_email, updateState, get_file_content, write_file
from logger import logResult, mylog, print_log
from plugin import execute_plugin
from publishers.email import (check_config as email_check_config,
send as send_email )
@@ -44,7 +43,7 @@ from publishers.mqtt import (check_config as mqtt_check_config,
#===============================================================================
# REPORTING
#===============================================================================
# create a json for webhook and mqtt notifications to provide further integration options
# create a json of the notifications to provide further integration options (e.g. used in webhook, mqtt notifications)
json_final = []
@@ -183,21 +182,6 @@ def send_notifications (db):
mylog('verbose', ['[Notification] included sections: ', conf.INCLUDED_SECTIONS ])
if 'internet' in conf.INCLUDED_SECTIONS :
# Compose Internet Section
sqlQuery = """SELECT eve_MAC as MAC, eve_IP as IP, eve_DateTime as Datetime, eve_EventType as "Event Type", eve_AdditionalInfo as "More info" FROM Events
WHERE eve_PendingAlertEmail = 1 AND eve_MAC = 'Internet'
ORDER BY eve_DateTime"""
notiStruc = construct_notifications(db, sqlQuery, "Internet IP change")
# collect "internet" (IP changes) for the webhook json
json_internet = notiStruc.json["data"]
mail_text = mail_text.replace ('<SECTION_INTERNET>', notiStruc.text + '\n')
mail_html = mail_html.replace ('<INTERNET_TABLE>', notiStruc.html)
mylog('verbose', ['[Notification] Internet sections done.'])
if 'new_devices' in conf.INCLUDED_SECTIONS :
# Compose New Devices Section
sqlQuery = """SELECT eve_MAC as MAC, eve_DateTime as Datetime, dev_LastIP as IP, eve_EventType as "Event Type", dev_Name as "Device name", dev_Comments as Comments FROM Events_Devices
@@ -223,7 +207,7 @@ def send_notifications (db):
notiStruc = construct_notifications(db, sqlQuery, "Down devices")
# collect "new_devices" for the webhook json
# collect "down_devices" for the webhook json
json_down_devices = notiStruc.json["data"]
mail_text = mail_text.replace ('<SECTION_DEVICES_DOWN>', notiStruc.text + '\n')
@@ -282,8 +266,8 @@ def send_notifications (db):
write_file (logPath + '/report_output.txt', mail_text)
write_file (logPath + '/report_output.html', mail_html)
# Send Mail
if json_internet != [] or json_new_devices != [] or json_down_devices != [] or json_events != [] or json_ports != [] or conf.debug_force_notification or plugins_report:
# Notify is something to report
if json_internet != [] or json_new_devices != [] or json_down_devices != [] or json_events != [] or json_ports != [] or plugins_report:
mylog('none', ['[Notification] Changes detected, sending reports'])
@@ -293,38 +277,38 @@ def send_notifications (db):
send_api()
if conf.REPORT_MAIL and check_config('email'):
updateState(db,"Send: Email")
updateState("Send: Email")
mylog('minimal', ['[Notification] Sending report by Email'])
send_email (msg )
else :
mylog('verbose', ['[Notification] Skip email'])
if conf.REPORT_APPRISE and check_config('apprise'):
updateState(db,"Send: Apprise")
updateState("Send: Apprise")
mylog('minimal', ['[Notification] Sending report by Apprise'])
send_apprise (msg)
else :
mylog('verbose', ['[Notification] Skip Apprise'])
if conf.REPORT_WEBHOOK and check_config('webhook'):
updateState(db,"Send: Webhook")
updateState("Send: Webhook")
mylog('minimal', ['[Notification] Sending report by Webhook'])
send_webhook (msg)
else :
mylog('verbose', ['[Notification] Skip webhook'])
if conf.REPORT_NTFY and check_config('ntfy'):
updateState(db,"Send: NTFY")
updateState("Send: NTFY")
mylog('minimal', ['[Notification] Sending report by NTFY'])
send_ntfy (msg)
else :
mylog('verbose', ['[Notification] Skip NTFY'])
if conf.REPORT_PUSHSAFER and check_config('pushsafer'):
updateState(db,"Send: PUSHSAFER")
updateState("Send: PUSHSAFER")
mylog('minimal', ['[Notification] Sending report by PUSHSAFER'])
send_pushsafer (msg)
else :
mylog('verbose', ['[Notification] Skip PUSHSAFER'])
# Update MQTT entities
if conf.REPORT_MQTT and check_config('mqtt'):
updateState(db,"Send: MQTT")
updateState("Send: MQTT")
mylog('minimal', ['[Notification] Establishing MQTT thread'])
mqtt_start(db)
else :
@@ -356,38 +340,14 @@ def check_config(service):
if service == 'email':
return email_check_config()
# if conf.SMTP_SERVER == '' or conf.REPORT_FROM == '' or conf.REPORT_TO == '':
# mylog('none', ['[Check Config] Error: Email service not set up correctly. Check your pialert.conf SMTP_*, REPORT_FROM and REPORT_TO variables.'])
# return False
# else:
# return True
if service == 'apprise':
return apprise_check_config()
# if conf.APPRISE_URL == '' or conf.APPRISE_HOST == '':
# mylog('none', ['[Check Config] Error: Apprise service not set up correctly. Check your pialert.conf APPRISE_* variables.'])
# return False
# else:
# return True
if service == 'webhook':
return webhook_check_config()
# if conf.WEBHOOK_URL == '':
# mylog('none', ['[Check Config] Error: Webhook service not set up correctly. Check your pialert.conf WEBHOOK_* variables.'])
# return False
# else:
# return True
if service == 'ntfy':
return ntfy_check_config ()
#
# if conf.NTFY_HOST == '' or conf.NTFY_TOPIC == '':
# mylog('none', ['[Check Config] Error: NTFY service not set up correctly. Check your pialert.conf NTFY_* variables.'])
# return False
# else:
# return True
if service == 'pushsafer':
return pushsafer_check_config()
@@ -472,85 +432,3 @@ def skip_repeated_notifications (db):
db.commitDB()
#===============================================================================
# UTIL
#===============================================================================
#-------------------------------------------------------------------------------
def check_and_run_event(db, pluginsState):
sql = db.sql # TO-DO
sql.execute(""" select * from Parameters where par_ID = "Front_Event" """)
rows = sql.fetchall()
event, param = ['','']
if len(rows) > 0 and rows[0]['par_Value'] != 'finished':
keyValue = rows[0]['par_Value'].split('|')
if len(keyValue) == 2:
event = keyValue[0]
param = keyValue[1]
else:
return pluginsState
if event == 'test':
handle_test(param)
if event == 'run':
pluginsState = handle_run(param, db, pluginsState)
# clear event execution flag
sql.execute ("UPDATE Parameters SET par_Value='finished' WHERE par_ID='Front_Event'")
# commit to DB
db.commitDB()
mylog('debug', [f'[MAIN] processScan3: {pluginsState.processScan}'])
return pluginsState
#-------------------------------------------------------------------------------
def handle_run(runType, db, pluginsState):
mylog('minimal', ['[', timeNowTZ(), '] START Run: ', runType])
# run the plugin to run
for plugin in conf.plugins:
if plugin["unique_prefix"] == runType:
pluginsState = execute_plugin(db, plugin, pluginsState)
mylog('minimal', ['[', timeNowTZ(), '] END Run: ', runType])
return pluginsState
#-------------------------------------------------------------------------------
def handle_test(testType):
mylog('minimal', ['[', timeNowTZ(), '] START Test: ', testType])
# Open text sample
sample_txt = get_file_content(pialertPath + '/back/report_sample.txt')
# Open html sample
sample_html = get_file_content(pialertPath + '/back/report_sample.html')
# Open json sample and get only the payload part
sample_json_payload = json.loads(get_file_content(pialertPath + '/back/webhook_json_sample.json'))[0]["body"]["attachments"][0]["text"]
sample_msg = noti_struc(sample_json_payload, sample_txt, sample_html )
if testType == 'Email':
send_email(sample_msg)
elif testType == 'Webhooks':
send_webhook (sample_msg)
elif testType == 'Apprise':
send_apprise (sample_msg)
elif testType == 'NTFY':
send_ntfy (sample_msg)
elif testType == 'PUSHSAFER':
send_pushsafer (sample_msg)
else:
mylog('none', ['[Test Publishers] No test matches: ', testType])
mylog('minimal', ['[Test Publishers] END Test: ', testType])

View File

@@ -1,194 +0,0 @@
""" internet related functions to support Pi.Alert """
import subprocess
import re
# pialert modules
import conf
from helper import timeNowTZ, updateState
from logger import append_line_to_file, mylog
from const import logPath
# need to find a better way to deal with settings !
#global DDNS_ACTIVE, DDNS_DOMAIN, DDNS_UPDATE_URL, DDNS_USER, DDNS_PASSWORD
#===============================================================================
# INTERNET IP CHANGE
#===============================================================================
def check_internet_IP ( db ):
# Header
updateState(db,"Scan: Internet IP")
mylog('verbose', ['[Internet IP] Check Internet IP started'])
# Get Internet IP
mylog('verbose', ['[Internet IP] - Retrieving Internet IP'])
internet_IP = get_internet_IP(conf.DIG_GET_IP_ARG)
# TESTING - Force IP
# internet_IP = "1.2.3.4"
# Check result = IP
if internet_IP == "" :
mylog('none', ['[Internet IP] Error retrieving Internet IP'])
mylog('none', ['[Internet IP] Exiting...'])
return False
mylog('verbose', ['[Internet IP] IP: ', internet_IP])
# Get previous stored IP
mylog('verbose', ['[Internet IP] Retrieving previous IP:'])
previous_IP = get_previous_internet_IP (db)
mylog('verbose', ['[Internet IP] ', previous_IP])
# Check IP Change
if internet_IP != previous_IP :
mylog('minimal', ['[Internet IP] New internet IP: ', internet_IP])
save_new_internet_IP (db, internet_IP)
else :
mylog('verbose', ['[Internet IP] No changes to perform'])
# Get Dynamic DNS IP
if conf.DDNS_ACTIVE :
mylog('verbose', ['[DDNS] Retrieving Dynamic DNS IP'])
dns_IP = get_dynamic_DNS_IP()
# Check Dynamic DNS IP
if dns_IP == "" or dns_IP == "0.0.0.0" :
mylog('none', ['[DDNS] Error retrieving Dynamic DNS IP'])
mylog('none', ['[DDNS] ', dns_IP])
# Check DNS Change
if dns_IP != internet_IP :
mylog('none', ['[DDNS] Updating Dynamic DNS IP'])
message = set_dynamic_DNS_IP ()
mylog('none', ['[DDNS] ', message])
else :
mylog('verbose', ['[DDNS] No changes to perform'])
else :
mylog('verbose', ['[DDNS] Skipping Dynamic DNS update'])
#-------------------------------------------------------------------------------
def get_internet_IP (DIG_GET_IP_ARG):
# BUGFIX #46 - curl http://ipv4.icanhazip.com repeatedly is very slow
# Using 'dig'
dig_args = ['dig', '+short'] + DIG_GET_IP_ARG.strip().split()
try:
cmd_output = subprocess.check_output (dig_args, universal_newlines=True)
except subprocess.CalledProcessError as e:
mylog('none', [e.output])
cmd_output = '' # no internet
# Check result is an IP
IP = check_IP_format (cmd_output)
# Handle invalid response
if IP == '':
IP = '0.0.0.0'
return IP
#-------------------------------------------------------------------------------
def get_previous_internet_IP (db):
previous_IP = '0.0.0.0'
# get previous internet IP stored in DB
db.sql.execute ("SELECT dev_LastIP FROM Devices WHERE dev_MAC = 'Internet' ")
result = db.sql.fetchone()
db.commitDB()
if result is not None and len(result) > 0 :
previous_IP = result[0]
# return previous IP
return previous_IP
#-------------------------------------------------------------------------------
def save_new_internet_IP (db, pNewIP):
# Log new IP into logfile
append_line_to_file (logPath + '/IP_changes.log',
'['+str(timeNowTZ()) +']\t'+ pNewIP +'\n')
prevIp = get_previous_internet_IP(db)
# Save event
db.sql.execute ("""INSERT INTO Events (eve_MAC, eve_IP, eve_DateTime,
eve_EventType, eve_AdditionalInfo,
eve_PendingAlertEmail)
VALUES ('Internet', ?, ?, 'Internet IP Changed',
'Previous Internet IP: '|| ?, 1) """,
(pNewIP, timeNowTZ(), prevIp) )
# Save new IP
db.sql.execute ("""UPDATE Devices SET dev_LastIP = ?
WHERE dev_MAC = 'Internet' """,
(pNewIP,) )
# commit changes
db.commitDB()
#-------------------------------------------------------------------------------
def check_IP_format (pIP):
# Check IP format
IPv4SEG = r'(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])'
IPv4ADDR = r'(?:(?:' + IPv4SEG + r'\.){3,3}' + IPv4SEG + r')'
IP = re.search(IPv4ADDR, pIP)
# Return error if not IP
if IP is None :
return ""
# Return IP
return IP.group(0)
#-------------------------------------------------------------------------------
def get_dynamic_DNS_IP ():
# Using OpenDNS server
# dig_args = ['dig', '+short', DDNS_DOMAIN, '@resolver1.opendns.com']
# Using default DNS server
dig_args = ['dig', '+short', conf.DDNS_DOMAIN]
try:
# try runnning a subprocess
dig_output = subprocess.check_output (dig_args, universal_newlines=True)
except subprocess.CalledProcessError as e:
# An error occured, handle it
mylog('none', ['[DDNS] ERROR - ', e.output])
dig_output = '' # probably no internet
# Check result is an IP
IP = check_IP_format (dig_output)
# Handle invalid response
if IP == '':
IP = '0.0.0.0'
return IP
#-------------------------------------------------------------------------------
def set_dynamic_DNS_IP ():
try:
# try runnning a subprocess
# Update Dynamic IP
curl_output = subprocess.check_output (['curl', '-s',
conf.DDNS_UPDATE_URL +
'username=' + conf.DDNS_USER +
'&password=' + conf.DDNS_PASSWORD +
'&hostname=' + conf.DDNS_DOMAIN],
universal_newlines=True)
except subprocess.CalledProcessError as e:
# An error occured, handle it
mylog('none', ['[DDNS] ERROR - ',e.output])
curl_output = ""
return curl_output

View File

@@ -25,9 +25,7 @@ class schedule_class:
nowTime = datetime.datetime.now(conf.tz).replace(microsecond=0)
# Run the schedule if the current time is past the schedule time we saved last time and
# (maybe the following check is unnecessary:)
# if the last run is past the last time we run a scheduled Pholus scan
# if nowTime > self.last_next_schedule and self.last_run < self.last_next_schedule:
# (maybe the following check is unnecessary)
if nowTime > self.last_next_schedule:
mylog('debug',f'[Scheduler] - Scheduler run for {self.service}: YES')
self.was_last_schedule_used = True