LinuxMonitor 1.5.6__tar.gz → 1.5.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {linuxmonitor-1.5.6 → linuxmonitor-1.5.8}/LinuxMonitor.egg-info/PKG-INFO +47 -2
- linuxmonitor-1.5.6/README.md → linuxmonitor-1.5.8/PKG-INFO +73 -0
- linuxmonitor-1.5.6/PKG-INFO → linuxmonitor-1.5.8/README.md +32 -28
- {linuxmonitor-1.5.6 → linuxmonitor-1.5.8}/linuxmonitor/__main__.py +2 -1
- {linuxmonitor-1.5.6 → linuxmonitor-1.5.8}/linuxmonitor/linuxmonitor.py +160 -33
- {linuxmonitor-1.5.6 → linuxmonitor-1.5.8}/setup.py +1 -1
- {linuxmonitor-1.5.6 → linuxmonitor-1.5.8}/LICENSE.md +0 -0
- {linuxmonitor-1.5.6 → linuxmonitor-1.5.8}/LinuxMonitor.egg-info/SOURCES.txt +0 -0
- {linuxmonitor-1.5.6 → linuxmonitor-1.5.8}/LinuxMonitor.egg-info/dependency_links.txt +0 -0
- {linuxmonitor-1.5.6 → linuxmonitor-1.5.8}/LinuxMonitor.egg-info/requires.txt +0 -0
- {linuxmonitor-1.5.6 → linuxmonitor-1.5.8}/LinuxMonitor.egg-info/top_level.txt +0 -0
- {linuxmonitor-1.5.6 → linuxmonitor-1.5.8}/linuxmonitor/__init__.py +0 -0
- {linuxmonitor-1.5.6 → linuxmonitor-1.5.8}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: LinuxMonitor
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.8
|
|
4
4
|
Summary: Get information and warning status of Linux server like service, port, ping, ssl certificate, disk/folder/cpu/ram/swap usage, ip connection, ... (Python and shell library, Linux ONLY)
|
|
5
5
|
Home-page: https://github.com/QuentinCG/Linux-Monitor-Python-Library
|
|
6
6
|
Author: Quentin Comte-Gaz
|
|
@@ -25,6 +25,19 @@ License-File: LICENSE.md
|
|
|
25
25
|
Requires-Dist: psutil
|
|
26
26
|
Requires-Dist: typing
|
|
27
27
|
Requires-Dist: asyncio
|
|
28
|
+
Dynamic: author
|
|
29
|
+
Dynamic: author-email
|
|
30
|
+
Dynamic: classifier
|
|
31
|
+
Dynamic: description
|
|
32
|
+
Dynamic: description-content-type
|
|
33
|
+
Dynamic: home-page
|
|
34
|
+
Dynamic: keywords
|
|
35
|
+
Dynamic: license
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
Dynamic: platform
|
|
38
|
+
Dynamic: requires-dist
|
|
39
|
+
Dynamic: requires-python
|
|
40
|
+
Dynamic: summary
|
|
28
41
|
|
|
29
42
|
# Linux Monitor (Python library)
|
|
30
43
|
[](https://pypi.org/project/LinuxMonitor/) [](https://github.com/QuentinCG/Linux-Monitor-Python-Library/blob/master/LICENSE.md) [](https://paypal.me/QuentinCG) [](https://pepy.tech/project/LinuxMonitor) [](https://pepy.tech/project/LinuxMonitor)
|
|
@@ -85,6 +98,8 @@ echo "USERNAME_HERE ALL=(ALL) NOPASSWD: /bin/systemctl" >> /etc/sudoers.d/USERNA
|
|
|
85
98
|
echo "USERNAME_HERE ALL=(ALL) NOPASSWD: /etc/init.d/apache2" >> /etc/sudoers.d/USERNAME_HERE
|
|
86
99
|
echo "USERNAME_HERE ALL=(ALL) NOPASSWD: /etc/init.d/mariadb" >> /etc/sudoers.d/USERNAME_HERE
|
|
87
100
|
```
|
|
101
|
+
- Install missing packages :
|
|
102
|
+
- If `last` command not available, install needed package: `sudo apt install wtmpdb util-linux`
|
|
88
103
|
- Check monitor possibilities (you can find it out by calling `python -m linuxmonitor --help` from the root of this repository)
|
|
89
104
|
- Load your shell or python script
|
|
90
105
|
|
|
@@ -162,15 +177,45 @@ python3 -m linuxmonitor --list_commands --config_file config-example.json --node
|
|
|
162
177
|
# Execute a custom commands
|
|
163
178
|
python3 -m linuxmonitor --execute_command CUSTOM_COMMAND_HERE --config_file config-example.json --nodebug
|
|
164
179
|
|
|
180
|
+
# Execute a custom command with parameters (only for commands with "accept_parameters": true or a {arg1}/{args} placeholder)
|
|
181
|
+
python3 -m linuxmonitor --execute_command unban_ip --parameters "1.2.3.4" --config_file config-example.json --nodebug
|
|
182
|
+
|
|
165
183
|
# Execute all custom commands
|
|
166
184
|
python3 -m linuxmonitor --execute_all_commands --config_file config-example.json --nodebug
|
|
167
185
|
```
|
|
168
186
|
|
|
187
|
+
## Custom commands with parameters
|
|
188
|
+
|
|
189
|
+
A custom command (in the `commands` section of the config file) can receive parameters at execution time.
|
|
190
|
+
This is opt-in per command: set `"accept_parameters": true` and/or use a placeholder in the `command` string.
|
|
191
|
+
|
|
192
|
+
Placeholders inside the `command` string:
|
|
193
|
+
- `{arg1}`, `{arg2}`, ... : replaced by the Nth provided parameter
|
|
194
|
+
- `{args}` : replaced by all provided parameters
|
|
195
|
+
- no placeholder : all parameters are appended at the end of the command
|
|
196
|
+
|
|
197
|
+
Every parameter is shell-quoted before substitution, so it is always treated as a single literal
|
|
198
|
+
argument. This prevents command injection through the parameters.
|
|
199
|
+
|
|
200
|
+
Example config entry:
|
|
201
|
+
```json
|
|
202
|
+
"unban_ip": {
|
|
203
|
+
"display_name": "Unban an IP from Fail2Ban",
|
|
204
|
+
"command": "sudo fail2ban-client unban {arg1}",
|
|
205
|
+
"is_private": true,
|
|
206
|
+
"accept_parameters": true,
|
|
207
|
+
"show_content_if_success": true,
|
|
208
|
+
"show_content_if_issue": true,
|
|
209
|
+
"timeout_in_sec": 60
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
169
213
|
## How to use in python script
|
|
170
214
|
|
|
171
215
|
Example of python script using this library:
|
|
172
216
|
- [Discord Bot Linux Monitor](https://github.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library)
|
|
173
217
|
|
|
218
|
+
|
|
174
219
|
## License
|
|
175
220
|
|
|
176
221
|
This project is under MIT license. This means you can use it as you want (just don't delete the library header).
|
|
@@ -1,3 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: LinuxMonitor
|
|
3
|
+
Version: 1.5.8
|
|
4
|
+
Summary: Get information and warning status of Linux server like service, port, ping, ssl certificate, disk/folder/cpu/ram/swap usage, ip connection, ... (Python and shell library, Linux ONLY)
|
|
5
|
+
Home-page: https://github.com/QuentinCG/Linux-Monitor-Python-Library
|
|
6
|
+
Author: Quentin Comte-Gaz
|
|
7
|
+
Author-email: quentin@comte-gaz.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: linux monitor monitoring server service port ping ssl certificate disk folder cpu ram swap usage ip connection
|
|
10
|
+
Platform: Linux
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Natural Language :: English
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: System Administrators
|
|
17
|
+
Classifier: Topic :: System :: Monitoring
|
|
18
|
+
Classifier: Topic :: System :: Networking :: Monitoring
|
|
19
|
+
Classifier: Topic :: System :: Systems Administration
|
|
20
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
21
|
+
Classifier: Environment :: Console
|
|
22
|
+
Requires-Python: >=3.3
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE.md
|
|
25
|
+
Requires-Dist: psutil
|
|
26
|
+
Requires-Dist: typing
|
|
27
|
+
Requires-Dist: asyncio
|
|
28
|
+
Dynamic: author
|
|
29
|
+
Dynamic: author-email
|
|
30
|
+
Dynamic: classifier
|
|
31
|
+
Dynamic: description
|
|
32
|
+
Dynamic: description-content-type
|
|
33
|
+
Dynamic: home-page
|
|
34
|
+
Dynamic: keywords
|
|
35
|
+
Dynamic: license
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
Dynamic: platform
|
|
38
|
+
Dynamic: requires-dist
|
|
39
|
+
Dynamic: requires-python
|
|
40
|
+
Dynamic: summary
|
|
41
|
+
|
|
1
42
|
# Linux Monitor (Python library)
|
|
2
43
|
[](https://pypi.org/project/LinuxMonitor/) [](https://github.com/QuentinCG/Linux-Monitor-Python-Library/blob/master/LICENSE.md) [](https://paypal.me/QuentinCG) [](https://pepy.tech/project/LinuxMonitor) [](https://pepy.tech/project/LinuxMonitor)
|
|
3
44
|
|
|
@@ -57,6 +98,8 @@ echo "USERNAME_HERE ALL=(ALL) NOPASSWD: /bin/systemctl" >> /etc/sudoers.d/USERNA
|
|
|
57
98
|
echo "USERNAME_HERE ALL=(ALL) NOPASSWD: /etc/init.d/apache2" >> /etc/sudoers.d/USERNAME_HERE
|
|
58
99
|
echo "USERNAME_HERE ALL=(ALL) NOPASSWD: /etc/init.d/mariadb" >> /etc/sudoers.d/USERNAME_HERE
|
|
59
100
|
```
|
|
101
|
+
- Install missing packages :
|
|
102
|
+
- If `last` command not available, install needed package: `sudo apt install wtmpdb util-linux`
|
|
60
103
|
- Check monitor possibilities (you can find it out by calling `python -m linuxmonitor --help` from the root of this repository)
|
|
61
104
|
- Load your shell or python script
|
|
62
105
|
|
|
@@ -134,15 +177,45 @@ python3 -m linuxmonitor --list_commands --config_file config-example.json --node
|
|
|
134
177
|
# Execute a custom commands
|
|
135
178
|
python3 -m linuxmonitor --execute_command CUSTOM_COMMAND_HERE --config_file config-example.json --nodebug
|
|
136
179
|
|
|
180
|
+
# Execute a custom command with parameters (only for commands with "accept_parameters": true or a {arg1}/{args} placeholder)
|
|
181
|
+
python3 -m linuxmonitor --execute_command unban_ip --parameters "1.2.3.4" --config_file config-example.json --nodebug
|
|
182
|
+
|
|
137
183
|
# Execute all custom commands
|
|
138
184
|
python3 -m linuxmonitor --execute_all_commands --config_file config-example.json --nodebug
|
|
139
185
|
```
|
|
140
186
|
|
|
187
|
+
## Custom commands with parameters
|
|
188
|
+
|
|
189
|
+
A custom command (in the `commands` section of the config file) can receive parameters at execution time.
|
|
190
|
+
This is opt-in per command: set `"accept_parameters": true` and/or use a placeholder in the `command` string.
|
|
191
|
+
|
|
192
|
+
Placeholders inside the `command` string:
|
|
193
|
+
- `{arg1}`, `{arg2}`, ... : replaced by the Nth provided parameter
|
|
194
|
+
- `{args}` : replaced by all provided parameters
|
|
195
|
+
- no placeholder : all parameters are appended at the end of the command
|
|
196
|
+
|
|
197
|
+
Every parameter is shell-quoted before substitution, so it is always treated as a single literal
|
|
198
|
+
argument. This prevents command injection through the parameters.
|
|
199
|
+
|
|
200
|
+
Example config entry:
|
|
201
|
+
```json
|
|
202
|
+
"unban_ip": {
|
|
203
|
+
"display_name": "Unban an IP from Fail2Ban",
|
|
204
|
+
"command": "sudo fail2ban-client unban {arg1}",
|
|
205
|
+
"is_private": true,
|
|
206
|
+
"accept_parameters": true,
|
|
207
|
+
"show_content_if_success": true,
|
|
208
|
+
"show_content_if_issue": true,
|
|
209
|
+
"timeout_in_sec": 60
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
141
213
|
## How to use in python script
|
|
142
214
|
|
|
143
215
|
Example of python script using this library:
|
|
144
216
|
- [Discord Bot Linux Monitor](https://github.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library)
|
|
145
217
|
|
|
218
|
+
|
|
146
219
|
## License
|
|
147
220
|
|
|
148
221
|
This project is under MIT license. This means you can use it as you want (just don't delete the library header).
|
|
@@ -1,31 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: LinuxMonitor
|
|
3
|
-
Version: 1.5.6
|
|
4
|
-
Summary: Get information and warning status of Linux server like service, port, ping, ssl certificate, disk/folder/cpu/ram/swap usage, ip connection, ... (Python and shell library, Linux ONLY)
|
|
5
|
-
Home-page: https://github.com/QuentinCG/Linux-Monitor-Python-Library
|
|
6
|
-
Author: Quentin Comte-Gaz
|
|
7
|
-
Author-email: quentin@comte-gaz.com
|
|
8
|
-
License: MIT
|
|
9
|
-
Keywords: linux monitor monitoring server service port ping ssl certificate disk folder cpu ram swap usage ip connection
|
|
10
|
-
Platform: Linux
|
|
11
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
-
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
-
Classifier: Natural Language :: English
|
|
15
|
-
Classifier: Intended Audience :: Developers
|
|
16
|
-
Classifier: Intended Audience :: System Administrators
|
|
17
|
-
Classifier: Topic :: System :: Monitoring
|
|
18
|
-
Classifier: Topic :: System :: Networking :: Monitoring
|
|
19
|
-
Classifier: Topic :: System :: Systems Administration
|
|
20
|
-
Classifier: Development Status :: 5 - Production/Stable
|
|
21
|
-
Classifier: Environment :: Console
|
|
22
|
-
Requires-Python: >=3.3
|
|
23
|
-
Description-Content-Type: text/markdown
|
|
24
|
-
License-File: LICENSE.md
|
|
25
|
-
Requires-Dist: psutil
|
|
26
|
-
Requires-Dist: typing
|
|
27
|
-
Requires-Dist: asyncio
|
|
28
|
-
|
|
29
1
|
# Linux Monitor (Python library)
|
|
30
2
|
[](https://pypi.org/project/LinuxMonitor/) [](https://github.com/QuentinCG/Linux-Monitor-Python-Library/blob/master/LICENSE.md) [](https://paypal.me/QuentinCG) [](https://pepy.tech/project/LinuxMonitor) [](https://pepy.tech/project/LinuxMonitor)
|
|
31
3
|
|
|
@@ -85,6 +57,8 @@ echo "USERNAME_HERE ALL=(ALL) NOPASSWD: /bin/systemctl" >> /etc/sudoers.d/USERNA
|
|
|
85
57
|
echo "USERNAME_HERE ALL=(ALL) NOPASSWD: /etc/init.d/apache2" >> /etc/sudoers.d/USERNAME_HERE
|
|
86
58
|
echo "USERNAME_HERE ALL=(ALL) NOPASSWD: /etc/init.d/mariadb" >> /etc/sudoers.d/USERNAME_HERE
|
|
87
59
|
```
|
|
60
|
+
- Install missing packages :
|
|
61
|
+
- If `last` command not available, install needed package: `sudo apt install wtmpdb util-linux`
|
|
88
62
|
- Check monitor possibilities (you can find it out by calling `python -m linuxmonitor --help` from the root of this repository)
|
|
89
63
|
- Load your shell or python script
|
|
90
64
|
|
|
@@ -162,15 +136,45 @@ python3 -m linuxmonitor --list_commands --config_file config-example.json --node
|
|
|
162
136
|
# Execute a custom commands
|
|
163
137
|
python3 -m linuxmonitor --execute_command CUSTOM_COMMAND_HERE --config_file config-example.json --nodebug
|
|
164
138
|
|
|
139
|
+
# Execute a custom command with parameters (only for commands with "accept_parameters": true or a {arg1}/{args} placeholder)
|
|
140
|
+
python3 -m linuxmonitor --execute_command unban_ip --parameters "1.2.3.4" --config_file config-example.json --nodebug
|
|
141
|
+
|
|
165
142
|
# Execute all custom commands
|
|
166
143
|
python3 -m linuxmonitor --execute_all_commands --config_file config-example.json --nodebug
|
|
167
144
|
```
|
|
168
145
|
|
|
146
|
+
## Custom commands with parameters
|
|
147
|
+
|
|
148
|
+
A custom command (in the `commands` section of the config file) can receive parameters at execution time.
|
|
149
|
+
This is opt-in per command: set `"accept_parameters": true` and/or use a placeholder in the `command` string.
|
|
150
|
+
|
|
151
|
+
Placeholders inside the `command` string:
|
|
152
|
+
- `{arg1}`, `{arg2}`, ... : replaced by the Nth provided parameter
|
|
153
|
+
- `{args}` : replaced by all provided parameters
|
|
154
|
+
- no placeholder : all parameters are appended at the end of the command
|
|
155
|
+
|
|
156
|
+
Every parameter is shell-quoted before substitution, so it is always treated as a single literal
|
|
157
|
+
argument. This prevents command injection through the parameters.
|
|
158
|
+
|
|
159
|
+
Example config entry:
|
|
160
|
+
```json
|
|
161
|
+
"unban_ip": {
|
|
162
|
+
"display_name": "Unban an IP from Fail2Ban",
|
|
163
|
+
"command": "sudo fail2ban-client unban {arg1}",
|
|
164
|
+
"is_private": true,
|
|
165
|
+
"accept_parameters": true,
|
|
166
|
+
"show_content_if_success": true,
|
|
167
|
+
"show_content_if_issue": true,
|
|
168
|
+
"timeout_in_sec": 60
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
169
172
|
## How to use in python script
|
|
170
173
|
|
|
171
174
|
Example of python script using this library:
|
|
172
175
|
- [Discord Bot Linux Monitor](https://github.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library)
|
|
173
176
|
|
|
177
|
+
|
|
174
178
|
## License
|
|
175
179
|
|
|
176
180
|
This project is under MIT license. This means you can use it as you want (just don't delete the library header).
|
|
@@ -31,6 +31,7 @@ def main() -> None:
|
|
|
31
31
|
parser.add_argument('--list_commands', action='store_true', help='📋 List all available commands 📋')
|
|
32
32
|
parser.add_argument('execute_command', type=str, help='📋 Execute a command 📋')
|
|
33
33
|
parser.add_argument('execute_all_commands', type=str, help='📋 Execute all commands 📋')
|
|
34
|
+
parser.add_argument('--parameters', type=str, default='', help='Optional parameters passed to --execute_command (space separated, quoted if needed)')
|
|
34
35
|
|
|
35
36
|
parser.add_argument('--debug', action='store_true', help='Enable debug mode')
|
|
36
37
|
parser.add_argument('--nodebug', action='store_true', help='Disable all logs')
|
|
@@ -163,7 +164,7 @@ def main() -> None:
|
|
|
163
164
|
if args.execute_command is not None:
|
|
164
165
|
handled = True
|
|
165
166
|
print(f"Executing command: {args.execute_command}...")
|
|
166
|
-
out_msg: str = asyncio.run(monitoring.execute_command(is_private=True, command_name=args.execute_command))
|
|
167
|
+
out_msg: str = asyncio.run(monitoring.execute_command(is_private=True, command_name=args.execute_command, parameters=args.parameters))
|
|
167
168
|
print(out_msg)
|
|
168
169
|
|
|
169
170
|
if args.execute_all_commands is not None:
|
|
@@ -31,9 +31,9 @@ Non exhaustive list of features (available by using it in shell or in python scr
|
|
|
31
31
|
__author__ = 'Quentin Comte-Gaz'
|
|
32
32
|
__email__ = "quentin@comte-gaz.com"
|
|
33
33
|
__license__ = "MIT License"
|
|
34
|
-
__copyright__ = "Copyright Quentin Comte-Gaz (
|
|
34
|
+
__copyright__ = "Copyright Quentin Comte-Gaz (2026)"
|
|
35
35
|
__python_version__ = "3.+"
|
|
36
|
-
__version__ = "1.5.
|
|
36
|
+
__version__ = "1.5.8 (2026/08/22)"
|
|
37
37
|
__status__ = "Usable for any Linux project"
|
|
38
38
|
|
|
39
39
|
import json
|
|
@@ -48,6 +48,7 @@ import socket
|
|
|
48
48
|
from datetime import datetime, timedelta
|
|
49
49
|
import platform
|
|
50
50
|
import re
|
|
51
|
+
import shlex
|
|
51
52
|
import logging
|
|
52
53
|
import asyncio
|
|
53
54
|
from http.client import responses
|
|
@@ -1574,11 +1575,11 @@ class LinuxMonitor:
|
|
|
1574
1575
|
:return: A dictionary containing the recent user logins.
|
|
1575
1576
|
"""
|
|
1576
1577
|
# Format the date without microseconds (milliseconds)
|
|
1577
|
-
past_date: str = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%
|
|
1578
|
+
past_date: str = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d %H:%M:%S')
|
|
1578
1579
|
|
|
1579
1580
|
try:
|
|
1580
1581
|
# Fetch the logs since the specified date
|
|
1581
|
-
command: str = f"sudo last --ip --hostlast --time-format iso --since '{past_date}' | grep 'pts/' || true"
|
|
1582
|
+
command: str = f"sudo /usr/bin/last --ip --hostlast --time-format iso --since '{past_date}' | grep 'pts/' || true"
|
|
1582
1583
|
last_output: str = subprocess.check_output(
|
|
1583
1584
|
args=command,
|
|
1584
1585
|
shell=True,
|
|
@@ -2020,40 +2021,166 @@ class LinuxMonitor:
|
|
|
2020
2021
|
logging.exception(msg=out_msg)
|
|
2021
2022
|
return out_msg
|
|
2022
2023
|
|
|
2023
|
-
|
|
2024
|
+
def _apply_command_parameters(self, command_template: str, parameters: str) -> str:
|
|
2025
|
+
"""
|
|
2026
|
+
Substitute user-provided parameters into a command template in an injection-safe way.
|
|
2027
|
+
|
|
2028
|
+
Placeholders supported in the template:
|
|
2029
|
+
- {args} : replaced by all provided parameters (space separated)
|
|
2030
|
+
- {arg1}, {arg2} : replaced by the Nth provided parameter
|
|
2031
|
+
If the template contains no placeholder, all parameters are appended at the end.
|
|
2032
|
+
|
|
2033
|
+
Every parameter is shell-quoted (shlex.quote) so it is always treated as a single
|
|
2034
|
+
literal argument, which prevents command injection through the parameters.
|
|
2035
|
+
|
|
2036
|
+
:param command_template: The command as defined in the configuration file.
|
|
2037
|
+
:param parameters: The raw parameters string provided by the user.
|
|
2038
|
+
|
|
2039
|
+
:return: The final command ready to be executed.
|
|
2040
|
+
"""
|
|
2041
|
+
param_list: List[str] = shlex.split(parameters) if parameters else []
|
|
2042
|
+
quoted_params: List[str] = [shlex.quote(param) for param in param_list]
|
|
2043
|
+
|
|
2044
|
+
has_placeholder: bool = ("{args}" in command_template) or bool(re.search(pattern=r"\{arg\d+\}", string=command_template))
|
|
2045
|
+
|
|
2046
|
+
# No placeholder: append every quoted parameter at the end of the command
|
|
2047
|
+
if not has_placeholder:
|
|
2048
|
+
if quoted_params:
|
|
2049
|
+
return command_template + " " + " ".join(quoted_params)
|
|
2050
|
+
return command_template
|
|
2051
|
+
|
|
2052
|
+
final_command: str = command_template
|
|
2053
|
+
|
|
2054
|
+
# Replace positional placeholders {arg1}, {arg2}, ...
|
|
2055
|
+
for index, quoted_param in enumerate(iterable=quoted_params, start=1):
|
|
2056
|
+
final_command = final_command.replace(f"{{arg{index}}}", quoted_param)
|
|
2057
|
+
|
|
2058
|
+
# Replace the catch-all {args} placeholder with every quoted parameter
|
|
2059
|
+
final_command = final_command.replace("{args}", " ".join(quoted_params))
|
|
2060
|
+
|
|
2061
|
+
# Any positional placeholder left unprovided is replaced by an empty string
|
|
2062
|
+
final_command = re.sub(pattern=r"\{arg\d+\}", repl="", string=final_command)
|
|
2063
|
+
|
|
2064
|
+
return final_command
|
|
2065
|
+
|
|
2066
|
+
def _normalize_command_name(self, name: str) -> str:
|
|
2067
|
+
"""
|
|
2068
|
+
Normalize a command name for permissive matching: lowercase and ignore spaces, '-' and '_'.
|
|
2069
|
+
"""
|
|
2070
|
+
return re.sub(pattern=r"[\s\-_]+", repl="", string=name.strip().lower())
|
|
2071
|
+
|
|
2072
|
+
def _levenshtein_distance(self, a: str, b: str) -> int:
|
|
2073
|
+
"""
|
|
2074
|
+
Compute the Levenshtein (edit) distance between two strings (number of single-character
|
|
2075
|
+
insertions, deletions or substitutions needed to turn one into the other).
|
|
2076
|
+
"""
|
|
2077
|
+
if a == b:
|
|
2078
|
+
return 0
|
|
2079
|
+
if len(a) == 0:
|
|
2080
|
+
return len(b)
|
|
2081
|
+
if len(b) == 0:
|
|
2082
|
+
return len(a)
|
|
2083
|
+
|
|
2084
|
+
previous_row: List[int] = list(range(len(b) + 1))
|
|
2085
|
+
for i, char_a in enumerate(iterable=a, start=1):
|
|
2086
|
+
current_row: List[int] = [i]
|
|
2087
|
+
for j, char_b in enumerate(iterable=b, start=1):
|
|
2088
|
+
insert_cost: int = current_row[j - 1] + 1
|
|
2089
|
+
delete_cost: int = previous_row[j] + 1
|
|
2090
|
+
replace_cost: int = previous_row[j - 1] + (0 if char_a == char_b else 1)
|
|
2091
|
+
current_row.append(min(insert_cost, delete_cost, replace_cost))
|
|
2092
|
+
previous_row = current_row
|
|
2093
|
+
|
|
2094
|
+
return previous_row[-1]
|
|
2095
|
+
|
|
2096
|
+
def _find_matching_command_key(self, command_name: str) -> Optional[str]:
|
|
2097
|
+
"""
|
|
2098
|
+
Find the configured command key matching the requested name in a permissive way:
|
|
2099
|
+
exact match first, then case/'-'/'_'/space-insensitive match, then a small typo tolerance.
|
|
2100
|
+
Ambiguous matches (several equally good candidates) are rejected to avoid running the wrong command.
|
|
2101
|
+
|
|
2102
|
+
:param command_name: The command name requested by the user.
|
|
2103
|
+
|
|
2104
|
+
:return: The matching command key from the configuration, or None if no safe match is found.
|
|
2105
|
+
"""
|
|
2106
|
+
command_keys: List[str] = list(self.config['commands'].keys())
|
|
2107
|
+
|
|
2108
|
+
# 1. Exact match
|
|
2109
|
+
if command_name in command_keys:
|
|
2110
|
+
return command_name
|
|
2111
|
+
|
|
2112
|
+
# 2. Normalized match (case-insensitive, '-'/'_'/spaces ignored)
|
|
2113
|
+
target: str = self._normalize_command_name(name=command_name)
|
|
2114
|
+
normalized_matches: List[str] = [key for key in command_keys if self._normalize_command_name(name=key) == target]
|
|
2115
|
+
if len(normalized_matches) == 1:
|
|
2116
|
+
return normalized_matches[0]
|
|
2117
|
+
if len(normalized_matches) > 1:
|
|
2118
|
+
# Several commands normalize to the same name: too ambiguous to guess
|
|
2119
|
+
return None
|
|
2120
|
+
|
|
2121
|
+
# 3. Fuzzy match tolerating small typos (on the normalized names)
|
|
2122
|
+
threshold: int = 1 if len(target) <= 4 else 2
|
|
2123
|
+
best_distance: Optional[int] = None
|
|
2124
|
+
best_keys: List[str] = []
|
|
2125
|
+
for key in command_keys:
|
|
2126
|
+
distance: int = self._levenshtein_distance(a=self._normalize_command_name(name=key), b=target)
|
|
2127
|
+
if best_distance is None or distance < best_distance:
|
|
2128
|
+
best_distance = distance
|
|
2129
|
+
best_keys = [key]
|
|
2130
|
+
elif distance == best_distance:
|
|
2131
|
+
best_keys.append(key)
|
|
2132
|
+
|
|
2133
|
+
# Only accept a typo-based match if it is close enough AND unambiguous
|
|
2134
|
+
if best_distance is not None and best_distance <= threshold and len(best_keys) == 1:
|
|
2135
|
+
return best_keys[0]
|
|
2136
|
+
|
|
2137
|
+
return None
|
|
2138
|
+
|
|
2139
|
+
async def execute_command(self, is_private: bool, command_name: str, parameters: str = "") -> str:
|
|
2024
2140
|
try:
|
|
2025
2141
|
out_msg: str = ""
|
|
2026
|
-
display_name: str = command_name
|
|
2027
|
-
for command_config_key in self.config['commands'].keys():
|
|
2028
|
-
if command_config_key == command_name:
|
|
2029
|
-
display_name = self.config['commands'][command_config_key]['display_name']
|
|
2030
|
-
command: str = self.config['commands'][command_config_key]['command']
|
|
2031
|
-
is_private_cmd: bool = self.config['commands'][command_config_key]['is_private']
|
|
2032
|
-
|
|
2033
|
-
if is_private or is_private == is_private_cmd:
|
|
2034
|
-
show_content_if_success: bool = self.config['commands'][command_config_key].get('show_content_if_success', False)
|
|
2035
|
-
show_content_if_issue: bool = self.config['commands'][command_config_key].get('show_content_if_issue', False)
|
|
2036
|
-
is_content_json: bool = self.config['commands'][command_config_key].get('is_content_json', False)
|
|
2037
|
-
timeout_in_sec: int = self.config['commands'][command_config_key].get('timeout_in_sec', 10)
|
|
2038
|
-
|
|
2039
|
-
res: str = await self._execute_command(command=command, display_name=display_name, show_content_if_success=show_content_if_success,
|
|
2040
|
-
show_content_if_issue=show_content_if_issue, is_content_json=is_content_json,
|
|
2041
|
-
timeout_in_sec=timeout_in_sec, display_only_if_critical=False)
|
|
2042
|
-
if res != "":
|
|
2043
|
-
if out_msg != "":
|
|
2044
|
-
out_msg += "\n"
|
|
2045
|
-
out_msg += f"{res}"
|
|
2046
|
-
else:
|
|
2047
|
-
out_msg = f"⚠️ **Command {display_name} is not allowed**"
|
|
2048
|
-
logging.warning(msg=out_msg)
|
|
2049
2142
|
|
|
2050
|
-
|
|
2051
|
-
|
|
2143
|
+
command_config_key: Optional[str] = self._find_matching_command_key(command_name=command_name)
|
|
2144
|
+
if command_config_key is None:
|
|
2145
|
+
out_msg = f"⚠️ **Command {command_name} not found**"
|
|
2146
|
+
logging.warning(msg=out_msg)
|
|
2147
|
+
return out_msg
|
|
2148
|
+
|
|
2149
|
+
display_name: str = self.config['commands'][command_config_key]['display_name']
|
|
2150
|
+
command: str = self.config['commands'][command_config_key]['command']
|
|
2151
|
+
is_private_cmd: bool = self.config['commands'][command_config_key]['is_private']
|
|
2152
|
+
|
|
2153
|
+
if is_private or is_private == is_private_cmd:
|
|
2154
|
+
parameters = parameters.strip()
|
|
2155
|
+
# A command accepts parameters only if it exposes a placeholder or explicitly opts in
|
|
2156
|
+
accept_parameters: bool = self.config['commands'][command_config_key].get('accept_parameters', False) \
|
|
2157
|
+
or ("{args}" in command) or bool(re.search(pattern=r"\{arg\d+\}", string=command))
|
|
2158
|
+
|
|
2159
|
+
if parameters != "" and not accept_parameters:
|
|
2160
|
+
out_msg = f"⚠️ **Command {display_name} does not accept parameters**"
|
|
2161
|
+
logging.warning(msg=out_msg)
|
|
2162
|
+
else:
|
|
2163
|
+
final_command: str = self._apply_command_parameters(command_template=command, parameters=parameters)
|
|
2052
2164
|
|
|
2053
|
-
|
|
2165
|
+
show_content_if_success: bool = self.config['commands'][command_config_key].get('show_content_if_success', False)
|
|
2166
|
+
show_content_if_issue: bool = self.config['commands'][command_config_key].get('show_content_if_issue', False)
|
|
2167
|
+
is_content_json: bool = self.config['commands'][command_config_key].get('is_content_json', False)
|
|
2168
|
+
timeout_in_sec: int = self.config['commands'][command_config_key].get('timeout_in_sec', 10)
|
|
2169
|
+
|
|
2170
|
+
res: str = await self._execute_command(command=final_command, display_name=display_name, show_content_if_success=show_content_if_success,
|
|
2171
|
+
show_content_if_issue=show_content_if_issue, is_content_json=is_content_json,
|
|
2172
|
+
timeout_in_sec=timeout_in_sec, display_only_if_critical=False)
|
|
2173
|
+
if res != "":
|
|
2174
|
+
if out_msg != "":
|
|
2175
|
+
out_msg += "\n"
|
|
2176
|
+
out_msg += f"{res}"
|
|
2177
|
+
else:
|
|
2178
|
+
out_msg = f"⚠️ **Command {display_name} is not allowed**"
|
|
2179
|
+
logging.warning(msg=out_msg)
|
|
2180
|
+
|
|
2181
|
+
if out_msg != "":
|
|
2182
|
+
out_msg = f"# 📜 Command {display_name} 📜\n{out_msg}"
|
|
2054
2183
|
|
|
2055
|
-
out_msg = f"⚠️ **Command {display_name} not found**"
|
|
2056
|
-
logging.warning(msg=out_msg)
|
|
2057
2184
|
return out_msg
|
|
2058
2185
|
except Exception as e:
|
|
2059
2186
|
return f"⚠️ **Error executing command {command_name}**:\n```sh\n{e}\n```"
|
|
@@ -6,7 +6,7 @@ with io.open(file='README.md', mode='r', encoding='utf-8') as readme_file:
|
|
|
6
6
|
|
|
7
7
|
setup(
|
|
8
8
|
name="LinuxMonitor",
|
|
9
|
-
version="1.5.
|
|
9
|
+
version="1.5.8",
|
|
10
10
|
description="Get information and warning status of Linux server like service, port, ping, ssl certificate, disk/folder/cpu/ram/swap usage, ip connection, ... (Python and shell library, Linux ONLY)",
|
|
11
11
|
long_description=readme,
|
|
12
12
|
long_description_content_type="text/markdown",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|