2024-11-25 16:55:24 +01:00
#!/bin/python3
2025-04-25 16:03:07 +02:00
# Public Python libraries
2024-11-25 16:55:24 +01:00
import argparse
import asyncio
2025-02-14 13:03:35 +01:00
from colorama import just_fix_windows_console
2025-07-11 16:24:21 +02:00
import pyotp
2024-11-25 16:55:24 +01:00
import json
2025-01-09 00:22:07 +01:00
import meshctrl
2024-11-25 16:55:24 +01:00
2025-04-25 16:03:07 +02:00
# Local Python libraries/modules
2025-12-29 10:55:57 +01:00
from modules . console import Console
from modules . executor import Executor
from modules . history import History
from modules . utilities import Transform , Utilities
2025-02-12 16:58:20 +01:00
2025-12-29 10:55:57 +01:00
meshbook_version = " 1.3.2 "
2025-04-25 16:03:07 +02:00
grace_period = 3 # Grace period will last for x (by default 3) second(s).
2025-01-09 00:22:07 +01:00
2025-12-29 10:55:57 +01:00
def define_cmdargs ( ) - > argparse . ArgumentParser :
parser = argparse . ArgumentParser ( description = " Process command-line arguments " )
parser . add_argument ( " -mb " , " --meshbook " , type = str , help = " Path to the meshbook yaml file. " )
parser . add_argument ( " --historydir " , type = str , help = " Define a custom history log directory (default: ./history). " , default = " ./history " )
parser . add_argument ( " --nohistory " , action = " store_true " , help = " Disable the logging of the history into a local log (text) file inside ' ./history ' . " )
parser . add_argument ( " --flushhistory " , action = " store_true " , help = " Clear old history logs before running the Meshbook. " )
parser . add_argument ( " -oc " , " --oscategories " , type = str , help = " Path to the Operating System categories JSON file. " , default = " ./os_categories.json " )
parser . add_argument ( " --conf " , type = str , help = " Path for the API configuration file (default: ./config.conf). " , default = " ./api.conf " )
parser . add_argument ( " --nograce " , action = " store_true " , help = " Disable the grace 3 seconds before running the meshbook. " )
parser . add_argument ( " -g " , " --group " , type = str , help = " Specify a manual override for the group. " , default = " " )
parser . add_argument ( " -d " , " --device " , type = str , help = " Specify a manual override for a device. " , default = " " )
parser . add_argument ( " -i " , " --indent " , action = " store_true " , help = " Use an JSON indentation of 4 when this flag is passed. " , default = False )
parser . add_argument ( " -s " , " --silent " , action = " store_true " , help = " Suppress terminal output. " , default = False )
parser . add_argument ( " --shlex " , action = " store_true " , help = " Shlex the lines. (SHell LEXical Analysis) " , default = False )
parser . add_argument ( " --version " , action = " store_true " , help = " Show the Meshbook version. " )
return parser
2025-01-09 00:22:07 +01:00
async def init_connection ( credentials : dict ) - > meshctrl . Session :
2025-02-13 15:21:18 +01:00
'''
Use the libmeshctrl library to initiate a Secure Websocket ( wss ) connection to the MeshCentral instance .
'''
2025-07-21 22:42:16 +02:00
if " totp_secret " in credentials :
totp = pyotp . TOTP ( credentials [ " totp_secret " ] )
otp = totp . now ( )
session = meshctrl . Session (
credentials [ ' hostname ' ] ,
user = credentials [ ' username ' ] ,
password = credentials [ ' password ' ] ,
token = otp
)
else :
session = meshctrl . Session (
credentials [ ' hostname ' ] ,
user = credentials [ ' username ' ] ,
password = credentials [ ' password ' ]
)
2025-01-09 00:22:07 +01:00
await session . initialized . wait ( )
return session
2024-11-25 16:55:24 +01:00
async def main ( ) :
2025-12-29 10:55:57 +01:00
local_categories_file = " ./os_categories.json "
2025-02-14 13:03:35 +01:00
just_fix_windows_console ( )
2025-02-13 15:21:18 +01:00
'''
Main function where the program starts . Place from which all comands originate ( eventually ) .
'''
2025-12-29 10:55:57 +01:00
# Define the cmd arguments
parser = define_cmdargs ( )
2024-11-25 16:55:24 +01:00
args = parser . parse_args ( )
2025-02-12 16:58:20 +01:00
2025-04-25 16:03:07 +02:00
if args . version :
2025-12-29 10:55:57 +01:00
Console . print_text ( args . silent ,
Console . text_color . reset + " MeshBook Version: " + Console . text_color . yellow + str ( meshbook_version ) )
2025-04-25 16:03:07 +02:00
return
2025-07-21 22:42:16 +02:00
2025-04-25 16:03:07 +02:00
if not args . meshbook :
parser . print_help ( )
return
2024-11-25 16:55:24 +01:00
try :
2025-02-12 16:58:20 +01:00
with open ( local_categories_file , " r " ) as file :
os_categories = json . load ( file )
2025-01-09 00:22:07 +01:00
2025-12-29 10:55:57 +01:00
if not Utilities . path_exist ( args . meshbook ) or Utilities . path_type ( args . meshbook ) != " File " :
Console . print_text ( args . silent ,
Console . text_color . red + " The given meshbook path is either not present on the filesystem or not a file. " )
return
2025-02-13 15:21:18 +01:00
credentials , meshbook = await asyncio . gather (
2025-12-29 10:55:57 +01:00
( Utilities . load_config ( args ) ) ,
( Utilities . compile_book ( args . meshbook ) )
2025-01-09 00:22:07 +01:00
)
2025-08-07 11:14:36 +02:00
if args . group != " " :
meshbook [ " group " ] = args . group
2025-08-07 13:56:38 +02:00
if " device " in meshbook :
2026-01-15 13:40:21 +01:00
del meshbook [ " device " ]
if " devices " in meshbook :
del meshbook [ " devices " ]
2025-08-07 11:14:36 +02:00
elif args . device != " " :
meshbook [ " device " ] = args . device
2025-08-07 13:56:38 +02:00
if " group " in meshbook :
2026-01-15 13:40:21 +01:00
del meshbook [ " group " ]
if " groups " in meshbook :
del meshbook [ " groups " ]
2025-08-07 11:14:36 +02:00
2025-02-13 11:44:53 +01:00
'''
2025-12-29 10:55:57 +01:00
The following section mainly displays used variables and first steps of the program to the Console .
2025-02-13 11:44:53 +01:00
'''
2025-04-25 16:03:07 +02:00
# INIT ARGUMENTS PRINTING
2025-12-29 10:55:57 +01:00
Console . print_line ( args . silent )
Console . print_text ( args . silent ,
" meshbook: " + Console . text_color . yellow + args . meshbook + Console . text_color . reset + " . " )
Console . print_text ( args . silent ,
" Operating System Categorisation file: " + Console . text_color . yellow + args . oscategories + Console . text_color . reset + " . " )
Console . print_text ( args . silent ,
" Configuration file: " + Console . text_color . yellow + args . conf + Console . text_color . reset + " . " )
2025-04-25 16:03:07 +02:00
# TARGET OS PRINTING
2025-02-27 21:47:55 +01:00
if " target_os " in meshbook :
2025-12-29 10:55:57 +01:00
Console . print_text ( args . silent ,
" Target Operating System category given: " + Console . text_color . yellow + meshbook [ " target_os " ] + Console . text_color . reset + " . " )
2025-04-25 16:03:07 +02:00
else :
2025-12-29 10:55:57 +01:00
Console . print_text ( args . silent ,
" Target Operating System category given: " + Console . text_color . yellow + " All " + Console . text_color . reset + " . " )
2025-07-21 22:42:16 +02:00
2025-04-25 16:03:07 +02:00
# Should Meshbook ignore categorisation?
if " ignore_categorisation " in meshbook :
2025-12-29 10:55:57 +01:00
Console . print_text ( args . silent ,
" Ignore the OS Categorisation file: " + Console . text_color . yellow + str ( meshbook [ " ignore_categorisation " ] ) + Console . text_color . reset + " . " )
2025-04-25 16:03:07 +02:00
if meshbook [ " ignore_categorisation " ] :
2025-12-29 10:55:57 +01:00
Console . print_text ( args . silent ,
Console . text_color . red + " !!!! \n " +
Console . text_color . yellow +
2025-04-25 16:03:07 +02:00
" Ignore categorisation is True. \n This means that the program checks if the target Operating System is somewhere in the reported device Operating System. " +
2025-12-29 10:55:57 +01:00
Console . text_color . red + " \n !!!! " )
2025-02-27 21:47:55 +01:00
else :
2025-12-29 10:55:57 +01:00
Console . print_text ( args . silent ,
" Ignore the OS Categorisation file: " + Console . text_color . yellow + " False " + Console . text_color . reset + " . " )
2025-07-21 22:42:16 +02:00
2025-04-25 16:03:07 +02:00
# TARGET TAG PRINTING
if " target_tag " in meshbook :
2025-12-29 10:55:57 +01:00
Console . print_text ( args . silent ,
" Target Device tag given: " + Console . text_color . yellow + meshbook [ " target_tag " ] + Console . text_color . reset + " . " )
2025-04-25 16:03:07 +02:00
else :
2025-12-29 10:55:57 +01:00
Console . print_text ( args . silent ,
" Target Device tag given: " + Console . text_color . yellow + " All " + Console . text_color . reset + " . " )
2025-02-27 21:47:55 +01:00
2025-04-25 16:03:07 +02:00
# TARGET PRINTING
2025-02-13 15:21:18 +01:00
if " device " in meshbook :
2025-12-29 10:55:57 +01:00
Console . print_text ( args . silent ,
" Target device: " + Console . text_color . yellow + str ( meshbook [ " device " ] ) + Console . text_color . reset + " . " )
2025-04-25 16:03:07 +02:00
elif " devices " in meshbook :
2025-12-29 10:55:57 +01:00
Console . print_text ( args . silent ,
" Target devices: " + Console . text_color . yellow + str ( meshbook [ " devices " ] ) + Console . text_color . reset + " . " )
2025-02-13 15:21:18 +01:00
elif " group " in meshbook :
2025-12-29 10:55:57 +01:00
Console . print_text ( args . silent ,
" Target group: " + Console . text_color . yellow + str ( meshbook [ " group " ] ) + Console . text_color . reset + " . " )
2025-04-25 16:03:07 +02:00
elif " groups " in meshbook :
2025-12-29 10:55:57 +01:00
Console . print_text ( args . silent ,
" Target groups: " + Console . text_color . yellow + str ( meshbook [ " groups " ] ) + Console . text_color . reset + " . " )
2025-02-13 11:44:53 +01:00
2025-04-25 16:03:07 +02:00
# RUNNING PARAMETERS PRINTING
2026-01-06 08:48:33 +01:00
Console . print_text ( args . silent , " Grace: " + Console . text_color . yellow + str ( not args . nograce ) + Console . text_color . reset + " . " ) # Negation of bool for correct explanation
Console . print_text ( args . silent , " Silent: " + Console . text_color . yellow + " False " + Console . text_color . reset + " . " ) # Can be pre-defined because if silent flag was passed then none of this would be printed.
2025-02-13 11:44:53 +01:00
2025-01-09 00:22:07 +01:00
session = await init_connection ( credentials )
2025-04-25 16:03:07 +02:00
# PROCESS PRINTING aka what its doing in the moment...
2025-12-29 10:55:57 +01:00
Console . print_line ( args . silent )
Console . print_text ( args . silent ,
Console . text_color . italic + " Trying to load the MeshCentral account credential file... " )
Console . print_text ( args . silent ,
Console . text_color . italic + " Trying to load the meshbook yaml file and compile it into something workable... " )
Console . print_text ( args . silent ,
Console . text_color . italic + " Trying to load the Operating System categorisation JSON file... " )
Console . print_text ( args . silent ,
Console . text_color . italic + " Connecting to MeshCentral and establish a session using variables from previous credential file. " )
Console . print_text ( args . silent ,
Console . text_color . italic + " Generating group list with nodes and reference the targets from that. " )
2025-02-13 15:21:18 +01:00
2025-02-13 11:44:53 +01:00
'''
End of the main information displaying section .
'''
2025-02-12 16:58:20 +01:00
2025-12-29 10:55:57 +01:00
group_list = await Transform . compile_group_list ( session )
2026-01-02 15:01:15 +01:00
compiled_device_list = await Utilities . gather_targets ( args . silent , meshbook , group_list , os_categories )
2025-01-09 00:22:07 +01:00
2025-12-29 10:55:57 +01:00
# Check if we have reachable targets on the MeshCentral host
2025-09-25 09:49:19 +02:00
if " target_list " not in compiled_device_list or len ( compiled_device_list [ " target_list " ] ) == 0 :
2025-12-29 10:55:57 +01:00
Console . print_text ( args . silent ,
Console . text_color . red + " No targets found or targets unreachable, quitting. " )
2025-04-30 16:58:48 +02:00
2025-12-29 10:55:57 +01:00
Console . print_line ( args . silent )
return
2025-02-13 11:44:53 +01:00
2025-12-29 10:55:57 +01:00
Console . print_line ( args . silent )
match meshbook :
case { " group " : candidate_target_name } :
target_name = candidate_target_name
case { " groups " : candidate_target_name } :
target_name = str ( candidate_target_name )
case { " device " : candidate_target_name } :
target_name = candidate_target_name
2025-02-13 14:22:03 +01:00
2025-12-29 10:55:57 +01:00
case { " devices " : candidate_target_name } :
target_name = str ( candidate_target_name )
2025-02-13 14:22:03 +01:00
2025-12-29 10:55:57 +01:00
case _ :
target_name = " "
2025-02-13 14:22:03 +01:00
2025-12-29 10:55:57 +01:00
# Initialize the history / logging functions class (whatever you want to name it)
history = History ( args . silent , args . historydir , args . flushhistory )
2025-02-13 14:22:03 +01:00
2025-12-29 10:55:57 +01:00
# Conclude history initlialization
Console . print_line ( args . silent )
2025-02-13 14:22:03 +01:00
2025-12-29 10:55:57 +01:00
# From here on the actual exection happens
Console . print_text ( args . silent ,
Console . text_color . yellow + " Executing meshbook on the target(s): " + Console . text_color . green + target_name + Console . text_color . yellow + " . " )
2025-09-25 09:49:19 +02:00
2025-12-29 10:55:57 +01:00
if not args . nograce :
Console . print_text ( args . silent ,
Console . text_color . yellow + " Initiating grace-period... " )
2025-09-25 09:49:19 +02:00
2025-12-29 10:55:57 +01:00
for x in range ( grace_period ) :
Console . print_text ( args . silent ,
Console . text_color . yellow + " {} ... " . format ( x + 1 ) ) # Countdown!
await asyncio . sleep ( 1 )
2025-02-12 16:58:20 +01:00
2025-12-29 10:55:57 +01:00
Console . print_line ( args . silent )
complete_log = await Executor . execute_meshbook ( args . silent ,
args . shlex ,
session ,
compiled_device_list ,
meshbook ,
group_list )
Console . print_line ( args . silent )
2025-02-13 11:44:53 +01:00
2025-12-29 10:55:57 +01:00
indent = None
if args . indent : indent = 4
2025-02-13 11:44:53 +01:00
2025-12-29 10:55:57 +01:00
formatted_history = json . dumps ( complete_log , indent = indent )
Console . print_text ( args . silent , formatted_history , 9 )
# Pass the output of the whole program to the history class
if args . nohistory :
Console . print_text ( args . silent , " Not writing to file. " )
else :
Console . print_text ( args . silent , " Writing to file... " )
history . write_history ( formatted_history )
2025-01-09 00:22:07 +01:00
except OSError as message :
2026-01-02 15:54:04 +01:00
Console . print_text (
args . silent ,
Console . text_color . red + f ' { message } '
)
2025-12-29 10:55:57 +01:00
except asyncio . CancelledError :
2026-01-02 15:54:04 +01:00
Console . print_text (
args . silent ,
Console . text_color . red + " Received SIGINT, Aborting - (Tasks may still be running on targets). "
)
2025-12-29 10:55:57 +01:00
raise
2024-11-25 16:55:24 +01:00
2026-01-02 15:54:04 +01:00
finally :
await session . close ( )
2024-11-25 16:55:24 +01:00
if __name__ == " __main__ " :
2025-12-29 10:55:57 +01:00
try :
asyncio . run ( main ( ) )
except KeyboardInterrupt :
Console . print_text ( False , Console . text_color . red + " Cancelled execution. " )