├── .github └── FUNDING.yml ├── LICENSE ├── README.md ├── config.php ├── config.toml.example ├── index.php └── report.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | ko_fi: dusoft 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Daniel Duris 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RPi Monitor Dashboard 2 | 3 | Raspberry Pi Monitor Dashboard is a simple monitoring tool with a dashboard suitable for monitoring multiple RPi devices (or any Linux devices). The number of devices you can monitor is unlimited. Fully configurable as to what report from bash / cli / terminal run. 4 | 5 | **Remote configuration updates as well as one-time commands to run on Linux devices (RPis) can be managed in the dashboard.** 6 | 7 | By default it reports these data: 8 | 9 | * hostname 10 | * CPU temperature 11 | * network IP address 12 | * ping results 13 | * running browser (only Firefox and Chromium are checked at the moment) 14 | * optionally, a screenshot of Pi's X screen (DISPLAY=:0) 15 | 16 | **It can report anything as commands are defined as standard bash commands and are fully configurable in config file.** 17 | 18 | ## Screenshots 19 | ### Dashboard preview 20 | ![Dashboard screenshot](https://github.com/user-attachments/assets/4ed59bdf-6876-4ceb-b7e1-67ae04e4534d) 21 | ### Remote configuration + one-time commands (non-root) to be run on next connect 22 | ![Remote commands](https://github.com/user-attachments/assets/80985bac-e1ba-4909-9769-0d93969e67c4) 23 | ### New config confirmed, can be canceled before next contact 24 | ![New config warning](https://github.com/user-attachments/assets/b5af03d6-88d0-4a54-a2eb-fed920c40000) 25 | 26 | ## Architecture 27 | 28 | * **Server** receiver and dashboard (one file) written in PHP (+config) - hosted anywhere 29 | * **Client** reporting script (one file) written in Python (+config) - used on RPi / Linux device 30 | 31 | ## Installation 32 | 33 | ### Server 34 | 1. Upload `index.php` and `config.php` to your server (any desired path), _optionally edit `config.php` options_ 35 | 2. Create `logs/` directory in the same path and make it writable (owned by the same user or 755) 36 | 37 | ### Client 38 | 1. Rename `config.toml.example` to `config.toml` and edit: URL to server receiver path and _optionally monitoring commands to execute_ (see Configuration in detail below) 39 | 2. Upload `report.py` and `config.toml` to your Raspberry Pi (e.g. put in a new directory `/home/pi/rpi-monitor/`) 40 | 3. Edit CRON (`crontab -e`) and add this line: `1 * * * * cd /home/pi/rpi-monitor/ && python3 report.py` (adjust reporting interval, if needed) 41 | 42 | **Open server dashboard URL in your browser and enjoy.** 43 | 44 | ## Configuration explained 45 | 46 | ### Server (config.php) 47 | 1. You can change`$config['timezone']` to your timezone for dashboard to report correct time. 48 | 2. You can set `$config['username']` and `$config['password']` to secure dashboard URL with login (digest in-browser authentication is used). 49 | 50 | Dashboard URL works without login by default. 51 | 52 | ### Client (config.toml) 53 | Standard commands **with output** are put under `[commands]` section: 54 | 55 | This will run `uptime` command and fetch full output: 56 | 57 | ```uptime = "/usr/bin/uptime"``` 58 | 59 | This will run `ifconfig` command and fetch **only** output of line containing "inet ": 60 | 61 | ```network = ["/usr/sbin/ifconfig wlan0", ["inet "] ]``` 62 | 63 | This will run `ps` command and fetch **only** output of lines containing "firefox" or "chromium": 64 | 65 | ```browser = ["/usr/bin/ps -A", ["firefox", "chromium"] ]``` 66 | 67 | You can add as many strings to search for as you need. Any found output lines will be joined into one string and reported back under the name of command (e.g. browser). 68 | 69 | Shell commands **without output** are put under `[commands_shell]` section (executed with `shell=True` in Python subprocess) - e.g. screenshot functionality. 70 | 71 | You can use any names for the commands, the names are then shown in the dashboard. 72 | 73 | ### How does config update work? (explanation for geeks) 74 | 75 | 1. Each remote device (RPi or Linux) connects to the receiver URL (defined in config.toml) on the server. 76 | 2. Before reporting metrics based on current config.toml, it asks server whether there was a config update (HTTP HEAD method is used to save bandwidth). If it exists, it creates a config backup (as a safety measure) and retrieves the new config (GET method). 77 | 3. The script replaces `[commands]` and `[commands_shell]` sections with the updated commands and saves it as a new config.toml. Receiver URL is never replaced (and is unavailable in the dashboard) as an incorrect URL could stop the remote device from further reporting. This would be similar to crashing your monitoring, so the tool prevents it by default. 78 | 4. The updated local config.toml is tested for validity. If invalid TOML syntax is detected, backup version of the former config is used again. If valid syntax is detected, the new config will be used. 79 | 80 | ## Upgrade (v1 to v2) 81 | 1. Please, upload new config.toml.example and change any reporting metrics, if required (bash commands). 82 | 2. Copy new report.py to your client(s), index.php to your server. 83 | 3. Optionally delete any files in logs/ directory, so you don't have reported data in old format hanging around. 84 | 85 | ## Upgrade (v2.x to v3) 86 | 1. Use your dashboard to update configs of your devices with new config option: 87 | ``` 88 | # Allow automatic updates of report.py from Github (experimental) 89 | auto_update = false 90 | ``` 91 | 2. Set auto_update to true, if you wish. This is still experimental, but hopefully updates won't break your monitoring. 92 | 3. Update config.php with `// REPORTING INTERVAL` section to have unresponsive devices displayed with red background in your dashboard. 93 | 4. Copy new report.py to your client(s), index.php to your server. 94 | 95 | ## Dependencies 96 | * Python v3 97 | * Python modules: 98 | * sys, subprocess, json, requests, binascii (should be available by default) 99 | * tomli / tomllib (`pip3 install tomli` for Python <3.10, available by default from 3.11) 100 | * If screenshots are enabled, scrot is recommended: 101 | * scrot (`sudo apt install scrot`) 102 | 103 | ## Please ⭐ star 🌟 this repo, if you like it and use it. 104 | -------------------------------------------------------------------------------- /config.php: -------------------------------------------------------------------------------- 1 | config update, 1 => one-time commands, 2 => python script code update] 31 | $available_updates = [0, 0, 0]; 32 | // Handle config update check 33 | if (isset(getallheaders()['X-Hostname'])) { 34 | $filename = 'logs/' . md5(getallheaders()['X-Hostname']) . '_new.config'; 35 | } 36 | if ((isset($filename) and file_exists($filename)) or file_exists('logs/_new.config')) { 37 | $available_updates[0] = '1'; 38 | } 39 | unset($filename); 40 | // Handle one-time commands update check 41 | if (isset(getallheaders()['X-Hostname'])) { 42 | $filename = 'logs/' . md5(getallheaders()['X-Hostname']) . '_new.onetime'; 43 | } 44 | if ((isset($filename) and file_exists($filename)) or file_exists('logs/_new.onetime')) { 45 | $available_updates[1] = '1'; 46 | } 47 | // Handle python script update check 48 | if (isset(getallheaders()['X-Crc32']) and getallheaders()['X-Crc32'] != $crc32) { 49 | $available_updates[2] = '1'; 50 | } 51 | $available_updates = implode('', $available_updates); 52 | if ((int) $available_updates) { 53 | header('HTTP/1.1 200 OK'); 54 | header('X-Update: ' . $available_updates); 55 | } 56 | exit; 57 | } 58 | 59 | $display_dashboard = false; 60 | if ($_SERVER['REQUEST_METHOD'] == 'POST') { 61 | // Handle uploads and receive data 62 | if (isset($_POST) and isset($_POST['new_config'])) { 63 | // save new config 64 | if (isset($_POST['all']) and $_POST['all'] == 1) { 65 | file_put_contents('logs/_new.config', $_POST['new_config']); 66 | } else { 67 | file_put_contents('logs/' . md5($_POST['hostname']) . '_new.config', $_POST['new_config']); 68 | } 69 | $display_dashboard = true; 70 | } elseif (isset($_POST) and isset($_POST['new_onetime'])) { 71 | // save new config 72 | if (isset($_POST['all']) and $_POST['all'] == 1) { 73 | file_put_contents('logs/_new.onetime', $_POST['new_onetime']); 74 | } else { 75 | file_put_contents('logs/' . md5($_POST['hostname']) . '_new.onetime', $_POST['new_onetime']); 76 | } 77 | $display_dashboard = true; 78 | } elseif (isset($_FILES) and isset($_FILES['screenshot']['name'])) { 79 | // Handle image upload 80 | $filename = pathinfo($_FILES['screenshot']['name'])['filename']; 81 | if (move_uploaded_file($_FILES['screenshot']['tmp_name'], 'logs/' . md5($filename) . '.png')) { 82 | header('HTTP/1.1 200 OK'); 83 | } else { 84 | header('HTTP/1.1 401 Unauthorized'); 85 | header('WWW-Authenticate: Digest realm="' . $realm . '",qop="auth",algorithm=SHA-256,nonce="' . uniqid() . '",opaque="' . hash('sha256', $realm) . '"'); 86 | } 87 | exit; 88 | } else { 89 | // receive data 90 | header('HTTP/1.1 200 OK'); 91 | $content = json_decode(file_get_contents('php://input')); 92 | if (isset($content->hostname) and isset($content->_config)) { 93 | file_put_contents('logs/' . md5($content->hostname) . '.config', $content->_config); 94 | } 95 | unset($content->_config); 96 | if (isset($content->hostname)) { 97 | file_put_contents('logs/' . md5($content->hostname) . '.log', json_encode($content)); 98 | echo '{"status":"OK"}'; 99 | } else { 100 | echo '{"status":"Missing identifier"}'; 101 | } 102 | exit; 103 | } 104 | } 105 | 106 | if ($_SERVER['REQUEST_METHOD'] == 'GET' or $display_dashboard === true) { 107 | // Handle authentication, dashboard and configuration update for remotes 108 | if (isset($_GET['update'])) { 109 | if ($_GET['update'] == 1) { 110 | $filename = '_new.config'; 111 | } elseif ($_GET['update'] == 2) { 112 | $filename = '_new.onetime'; 113 | } elseif ($_GET['update'] == 3) { 114 | $filename = $latest_code_filename; 115 | } 116 | // Send update to remote devices 117 | if (file_exists('logs/' . $filename)) { 118 | // count, if all remotes retrieved updated config 119 | $file = file_get_contents('logs/' . $filename); 120 | $counter_file = 'logs/' . str_replace('.', '_', $filename) . '.count'; 121 | $count = 1; 122 | if (file_exists($counter_file)) { 123 | $counter = fopen($counter_file, 'r+'); 124 | flock($counter, LOCK_EX); 125 | $count = trim(fread($counter, filesize($counter_file))); 126 | $count++; 127 | rewind($counter); 128 | fwrite($counter, $count); 129 | flock($counter, LOCK_UN); 130 | fclose($counter); 131 | } else { 132 | $counter = fopen($counter_file, 'w'); 133 | flock($counter, LOCK_EX); 134 | fwrite($counter, $count); 135 | flock($counter, LOCK_UN); 136 | fclose($counter); 137 | } 138 | // delete new config once retrieved by all remotes 139 | if (count(glob('logs/*.log')) == $count) { 140 | unlink('logs/' . $filename); 141 | unlink($counter_file); 142 | } 143 | } 144 | // not elseif, we can have both update for all and an individual update that overrides all device update 145 | if (isset(getallheaders()['X-Hostname']) and file_exists('logs/' . md5(getallheaders()['X-Hostname']) . $filename)) { 146 | $file = file_get_contents('logs/' . md5(getallheaders()['X-Hostname']) . $filename); 147 | unlink('logs/' . md5(getallheaders()['X-Hostname']) . $filename); 148 | } 149 | echo $file; 150 | exit; 151 | } 152 | if (isset($_GET['cancel'])) { 153 | // cancel config update 154 | if ($_GET['cancel'] == 1) { 155 | if (isset($_GET['hostname']) and file_exists('logs/' . md5($_GET['hostname']) . '_new.config')) { 156 | unlink('logs/' . md5($_GET['hostname']) . '_new.config'); 157 | $flash = 'ℹ️ Config update canceled.'; 158 | } elseif (file_exists('logs/_new.config')) { 159 | unlink('logs/_new.config'); 160 | $flash = 'ℹ️ Config update canceled for all devices.'; 161 | } 162 | } elseif ($_GET['cancel'] == 2) { 163 | if (isset($_GET['hostname']) and file_exists('logs/' . md5($_GET['hostname']) . '_new.onetime')) { 164 | unlink('logs/' . md5($_GET['hostname']) . '_new.onetime'); 165 | $flash = 'ℹ️ One-time commands canceled.'; 166 | } elseif (file_exists('logs/_new.onetime')) { 167 | unlink('logs/_new.onetime'); 168 | $flash = 'ℹ️ One-time commands canceled for all devices.'; 169 | } 170 | } 171 | } 172 | // Assume user is authenticated, unless auth is configured and digest tells you otherwise 173 | $authenticated = true; 174 | if ($config['username'] and $config['password']) { 175 | // Handle Digest authentication, if user/pass configured 176 | $realm = 'RPi Monitor Dashboard'; 177 | if (empty($_SERVER['PHP_AUTH_DIGEST'])) { 178 | header('HTTP/1.1 401 Unauthorized'); 179 | header('WWW-Authenticate: Digest realm="' . $realm . '",qop="auth",algorithm=SHA-256,nonce="' . uniqid() . '",opaque="' . hash('sha256', $realm) . '"'); 180 | $error = '

Not authorized. Enter username and password to login.

'; 181 | $authenticated = false; 182 | } elseif (!($data = http_digest_parse($_SERVER['PHP_AUTH_DIGEST'])) or $data['username'] != $config['username']) { 183 | $error = '

Incorrect login details.

'; 184 | $authenticated = false; 185 | } 186 | if (isset($data['response'])) { 187 | $hash1 = hash('sha256', $data['username'] . ':' . $realm . ':' . $config['password']); 188 | $hash2 = hash('sha256', $_SERVER['REQUEST_METHOD'] . ':' . $data['uri']); 189 | $valid_response = hash('sha256', $hash1 . ':' . $data['nonce'] . ':' . $data['nc'] . ':' . $data['cnonce'] . ':' . $data['qop'] . ':' . $hash2); 190 | if ($data['response'] != $valid_response) { 191 | header('HTTP/1.1 401 Unauthorized'); 192 | header('WWW-Authenticate: Digest realm="' . $realm . '",qop="auth",algorithm=SHA-256,nonce="' . uniqid() . '",opaque="' . hash('sha256', $realm) . '"'); 193 | $error = '

Incorrect login details.

'; 194 | $authenticated = false; 195 | } 196 | } 197 | } 198 | echo 'RPi Monitor Dashboard'; 219 | if ($authenticated === true) { 220 | // Display dashboard, if auth check passed 221 | echo '

RPi Monitor Dashboard

'; 222 | if (file_exists('logs/_new.config')) { 223 | echo '
⚠️ New configuration will be applied on next contact to all devices.
Config
'; 224 | } 225 | if (file_exists('logs/_new.onetime')) { 226 | echo '
⚠️ New one-time commands will be applied on next contact to all devices.
One-time commands
'; 227 | } 228 | if (isset($flash) and !isset($_GET['hostname'])) { 229 | echo '
' . $flash . '
'; 230 | } 231 | echo '
'; 232 | foreach (glob('logs/*.log') as $log) { 233 | $modified_time = filemtime($log); 234 | $content = file_get_contents($log); 235 | $content = json_decode($content); 236 | echo ''; 242 | if (isset($flash) and isset($_GET['hostname']) and $_GET['hostname'] == $content->hostname) { 243 | echo '
' . $flash . '
'; 244 | } 245 | if (file_exists('logs/' . md5($content->hostname) . '_new.config')) { 246 | echo '
⚠️ New configuration will be applied on next contact.
Config
'; 247 | } 248 | if (file_exists('logs/' . md5($content->hostname) . '_new.onetime')) { 249 | echo '
⚠️ One-time commands will be applied on next contact.
One-time commands
'; 250 | } 251 | echo 'Last update: ' . date('F d Y H:i:s', $modified_time); 252 | $config_filename = str_ireplace('.log', '.config', $log); 253 | if (file_exists($config_filename)) { 254 | $cleaned_config = stripReceiverFromTOMLConfig(file_get_contents($config_filename)); 255 | echo '
Config
'; 256 | } 257 | $onetime_filename = str_ireplace('.log', '.onetime', $log); 258 | $onetime = ''; 259 | if (file_exists($onetime_filename)) { 260 | $onetime = file_get_contents($onetime_filename); 261 | } 262 | echo '
One-time commands
'; 263 | echo '

' . $content->hostname . '

'; 264 | $screenshot = str_ireplace('.log', '.png', $log); 265 | if (file_exists($screenshot)) { 266 | echo '' . $content->hostname . ' screenshot'; 267 | } 268 | unset($content->hostname); 269 | echo ''; 270 | foreach ($content as $key => $item) { 271 | echo ''; 275 | } 276 | echo '
'; 272 | echo '

' . $key . '

'; 273 | echo nl2br($item, false); 274 | echo '
'; 277 | echo ''; 278 | } 279 | echo '
'; 280 | } else { 281 | echo $error; 282 | } 283 | echo ''; 284 | } 285 | 286 | // HTTP Digest authentication parser 287 | // see: https://www.php.net/manual/en/features.http-auth.php 288 | function http_digest_parse($txt) 289 | { 290 | // protect against missing data 291 | $needed_parts = ['nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1]; 292 | $data = []; 293 | $keys = implode('|', array_keys($needed_parts)); 294 | preg_match_all('@(' . $keys . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', $txt, $matches, PREG_SET_ORDER); 295 | foreach ($matches as $m) { 296 | $data[$m[1]] = $m[3] ? $m[3] : $m[4]; 297 | unset($needed_parts[$m[1]]); 298 | } 299 | return $needed_parts ? false : $data; 300 | } 301 | 302 | function stripReceiverFromTOMLConfig($config) 303 | { 304 | $cleaned_config = ''; 305 | $config_lines = explode("\n", $config); 306 | foreach ($config_lines as $key => $line) { 307 | if (stripos($line, '#-#*+*#-#') !== false) { 308 | $offset_key = $key + 1; 309 | } 310 | if (isset($offset_key) and $key >= $offset_key) { 311 | $cleaned_config .= $line . "\n"; 312 | } 313 | } 314 | return $cleaned_config; 315 | } 316 | -------------------------------------------------------------------------------- /report.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | ########################################################### 3 | ## RPi Monitor Dashboard ## 4 | ## https://github.com/nekromoff/rpi-monitor-dashboard ## 5 | ## Copyright (c) 2024+ Daniel Duris, dusoft@staznosti.sk ## 6 | ## License: MIT ## 7 | ## Version: 3.0 ## 8 | ########################################################### 9 | 10 | __version__="3.0" 11 | 12 | import sys 13 | # tomli/tomllib compatibility layer as Python 3.11+ contains tomllib by default 14 | if sys.version_info >= (3, 11): 15 | import tomllib 16 | else: 17 | import tomli as tomllib 18 | import subprocess 19 | import json 20 | import requests 21 | import binascii 22 | 23 | with open("config.toml", "rb") as f: 24 | config = tomllib.load(f) 25 | 26 | content={} 27 | 28 | # always extract hostname 29 | output=subprocess.run("/usr/bin/hostname", universal_newlines = True, stdout = subprocess.PIPE) 30 | hostname=output.stdout.strip() 31 | content["hostname"]=hostname 32 | 33 | headers = {"User-Agent": "RPi Monitor Dashboard/"+__version__, "X-Hostname": hostname, "X-Crc32": str(binascii.crc32(open("report.py", "rb").read())), "Content-Type": "application/json"} 34 | 35 | # check for available updates 36 | response=requests.head(config["receiver"], headers = headers) 37 | 38 | # check for python script update and overwrite, if auto update enabled 39 | if "X-Update" in response.headers and response.headers["X-Update"][2]=="1" and config['auto_update']==True: 40 | response=requests.get(config["receiver"]+'?update=3', headers = headers) 41 | try: 42 | if len(response.text)>0: 43 | subprocess.run('cp report.py report.py.bak', shell = True) 44 | f = open("report.py", "w") 45 | f.write(response.text) 46 | f.close() 47 | except Exception: 48 | # ignore, skip 49 | pass 50 | 51 | # check for one-time commands and proceed with execution 52 | if "X-Update" in response.headers and response.headers["X-Update"][1]=="1": 53 | response=requests.get(config["receiver"]+'?update=2', headers = headers) 54 | commands=response.text.split("\n") 55 | try: 56 | for line_no, command in enumerate(commands): 57 | command=command.replace("{hostname}", hostname) 58 | subprocess.run([command], shell = True) 59 | except Exception: 60 | # ignore, skip 61 | pass 62 | 63 | # check for config update and proceed with update 64 | if "X-Update" in response.headers and response.headers["X-Update"][0]=="1": 65 | # backup current config in case the new one is messed up 66 | subprocess.run('cp config.toml config.toml.bak', shell = True) 67 | response=requests.get(config["receiver"]+'?update=1', headers = headers) 68 | current_config=open("config.toml", "rt").read() 69 | current_config_lines=current_config.split("\n") 70 | new_config='' 71 | for line_no, line in enumerate(current_config_lines): 72 | new_config=new_config+line+"\n" 73 | # search for our special line 74 | pos=line.strip().find("#-#*+*#-#") 75 | # if found, assemble new config from existing receiver and config update received 76 | if pos!=-1: 77 | new_config=new_config+response.text; 78 | new_config=new_config.strip() 79 | f = open("config.toml", "w") 80 | f.write(new_config) 81 | f.close() 82 | # validate new config 83 | try: 84 | with open("config.toml", "rb") as f: 85 | test_config = tomllib.load(f) 86 | # once validated, confirm it back to receiver 87 | # content["_config"]=open("config.toml", "rt").read() 88 | #json_data = json.dumps(content) 89 | #response = requests.post(config["receiver"], data=json_data, headers=headers) 90 | except tomllib.TOMLDecodeError: 91 | # new config parse fail, invalid TOML config, fallback to backup config 92 | subprocess.run('cp config.toml.bak config.toml', shell = True) 93 | # backup cleanup 94 | subprocess.run('rm config.toml.bak', shell = True) 95 | break; 96 | 97 | with open("config.toml", "rb") as f: 98 | config = tomllib.load(f) 99 | 100 | # include config contents in the payload 101 | content["_config"]=open("config.toml", "rt").read() 102 | 103 | for name, command in config["commands"].items(): 104 | try: 105 | # simple command (TOML string) 106 | if isinstance(command, str): 107 | command=command.replace("{hostname}", hostname) 108 | parts=command.split(" ") 109 | output=subprocess.run(parts, universal_newlines = True, stdout = subprocess.PIPE) 110 | content[name]=output.stdout.strip() 111 | # command with text search and extraction (TOML array) 112 | elif isinstance(command, list): 113 | command[0]=command[0].replace("{hostname}", hostname) 114 | parts=command[0].split(" ") 115 | output=subprocess.run(parts, universal_newlines = True, stdout = subprocess.PIPE) 116 | content[name]="" 117 | for string in command[1]: 118 | for line in output.stdout.split("\n"): 119 | if string in line: 120 | content[name]=content[name]+line.strip()+"\n" 121 | content[name]=content[name].strip() 122 | except Exception: 123 | # ignore, skip 124 | pass 125 | 126 | try: 127 | for name, command in config["commands_shell"].items(): 128 | command=command.replace("{hostname}", hostname) 129 | subprocess.run([command], shell = True) 130 | except Exception: 131 | # ignore, skip 132 | pass 133 | 134 | # post payload 135 | json_data = json.dumps(content) 136 | response = requests.post(config["receiver"], data = json_data, headers = headers) 137 | 138 | headers.pop("Content-Type") 139 | 140 | # if screenshot command exists, upload image 141 | try: 142 | if config["commands_shell"]["screenshot"]: 143 | files = {'screenshot': open(hostname+".png", 'rb')} 144 | r = requests.post(config["receiver"], files = files, headers = headers) 145 | except Exception: 146 | # ignore, skip 147 | pass 148 | --------------------------------------------------------------------------------