diff --git a/virtualbox/vbox-adapter-check.py b/virtualbox/vbox-adapter-check.py index ff3428f..c0defec 100755 --- a/virtualbox/vbox-adapter-check.py +++ b/virtualbox/vbox-adapter-check.py @@ -20,15 +20,31 @@ import textwrap import gi +from vboxcommon import ensure_hostonlyif_exists, get_vm_state, run_vboxmanage gi.require_version("Notify", "0.7") -from gi.repository import Notify -from vboxcommon import * +from gi.repository import Notify # noqa: E402 DYNAMIC_VM_NAME = ".dynamic" DISABLED_ADAPTER_TYPE = "hostonly" ALLOWED_ADAPTER_TYPES = ("hostonly", "intnet", "none") +DESCRIPTION = f"""Print the status of all internet adapters of all VMs in VirtualBox. +Notify if any VM with {DYNAMIC_VM_NAME} in the name has an adapter whose type is not allowed. +This is useful to detect internet access which is undesirable for dynamic malware analysis. +Optionally change the type of the adapters with non-allowed type to Host-Only.""" + +EPILOG = textwrap.dedent( + f""" + Example usage: + # Print status of all interfaces and disable internet access in VMs whose name contain {DYNAMIC_VM_NAME} + vbox-adapter-check.vm + + # Print status of all interfaces without modifying any of them + vbox-adapter-check.vm --do_not_modify + """ +) + def get_vm_uuids(dynamic_only): """Gets the machine UUID(s) for a given VM name using 'VBoxManage list vms'.""" @@ -46,7 +62,7 @@ def get_vm_uuids(dynamic_only): if (not dynamic_only) or DYNAMIC_VM_NAME in vm_name: vm_uuids.append((vm_name, vm_uuid)) except Exception as e: - raise Exception(f"Error finding machines UUIDs") from e + raise Exception("Error finding machines UUIDs") from e return vm_uuids @@ -69,7 +85,7 @@ def change_network_adapters_to_hostonly(vm_uuid, vm_name, hostonly_ifname, do_no # nic8="none" vminfo = run_vboxmanage(["showvminfo", vm_uuid, "--machinereadable"]) - for nic_number, nic_value in re.findall('^nic(\d+)="(\S+)"', vminfo, flags=re.M): + for nic_number, nic_value in re.findall(r'^nic(\d+)="(\S+)"', vminfo, flags=re.M): if nic_value not in ALLOWED_ADAPTER_TYPES: nics_with_internet.append(f"nic{nic_number}") invalid_nics_msg += f"{nic_number} " @@ -80,7 +96,11 @@ def change_network_adapters_to_hostonly(vm_uuid, vm_name, hostonly_ifname, do_no if do_not_modify: message = f"{vm_name} may be connected to the internet on adapter(s): {nic}. Please double check your VMs settings." else: - message = f"{vm_name} may be connected to the internet on adapter(s): {nic}. The network adapter(s) have been disabled automatically to prevent an undesired internet connectivity. Please double check your VMs settings." + message = ( + f"{vm_name} may be connected to the internet on adapter(s): {nic}." + "The network adapter(s) have been disabled automatically to prevent an undesired internet connectivity." + "Please double check your VMs settings." + ) # different commands are necessary if the machine is running. if get_vm_state(vm_uuid) == "poweroff": run_vboxmanage( @@ -106,7 +126,11 @@ def change_network_adapters_to_hostonly(vm_uuid, vm_name, hostonly_ifname, do_no if do_not_modify: message = f"{vm_name} may be connected to the internet on adapter(s): {invalid_nics_msg}. Please double check your VMs settings." else: - message = f"{vm_name} may be connected to the internet on adapter(s): {invalid_nics_msg}. The network adapter(s) have been disabled automatically to prevent an undesired internet connectivity. Please double check your VMs settings." + message = ( + f"{vm_name} may be connected to the internet on adapter(s): {invalid_nics_msg}." + "The network adapter(s) have been disabled automatically to prevent an undesired internet connectivity." + "Please double check your VMs settings." + ) # Show notification using PyGObject Notify.init("VirtualBox adapter check") @@ -128,19 +152,9 @@ def main(argv=None): if argv is None: argv = sys.argv[1:] - epilog = textwrap.dedent( - f""" - Example usage: - # Print status of all internet adapters and disable the adapters with internet access in VMs with {DYNAMIC_VM_NAME} in the name - vbox-adapter-check.vm - - # Print status of all internet adapters without modifying any of them - vbox-adapter-check.vm --do_not_modify - """ - ) parser = argparse.ArgumentParser( - description=f"Print the status of all internet adapters of all VMs in VirtualBox. Notify if any VM with {DYNAMIC_VM_NAME} in the name has an adapter whose type is not allowed (internet access is undesirable for dynamic malware analysis)i. Optionally change the type of the adapters with non-allowed type to Host-Only.", - epilog=epilog, + description=DESCRIPTION, + epilog=EPILOG, formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( @@ -162,7 +176,7 @@ def main(argv=None): for vm_name, vm_uuid in vm_uuids: change_network_adapters_to_hostonly(vm_uuid, vm_name, hostonly_ifname, args.do_not_modify) else: - print(f"[Warning ⚠️] No VMs found") + print("[Warning ⚠️] No VMs found") except Exception as e: print(f"Error verifying dynamic VM hostonly configuration: {e}") diff --git a/virtualbox/vbox-clean-snapshots.py b/virtualbox/vbox-clean-snapshots.py index f8dfb06..0ec13ee 100755 --- a/virtualbox/vbox-clean-snapshots.py +++ b/virtualbox/vbox-clean-snapshots.py @@ -19,7 +19,30 @@ import sys import textwrap -from vboxcommon import * +from vboxcommon import get_vm_state, run_vboxmanage + +DESCRIPTION = "Clean a VirtualBox VM up by deleting a snapshot and its children recursively skipping snapshots with a substring in the name." + +EPILOG = textwrap.dedent( + """ + Example usage: + # Delete all snapshots excluding the default protected ones (with 'clean' or 'done' in the name, case insensitive) in the 'FLARE-VM.20240604' VM + vbox-clean-snapshots.py FLARE-VM.20240604 + + # Delete all snapshots that do not include 'clean', 'done', or 'important' (case insensitive) in the name in the 'FLARE-VM.20240604' VM + vbox-clean-snapshots.py FLARE-VM.20240604 --protected_snapshots "clean,done,important" + + # Delete the 'Snapshot 3' snapshot and its children recursively skipping the default protected ones in the 'FLARE-VM.20240604' VM + vbox-clean-snapshots.py FLARE-VM.20240604 --root_snapshot "Snapshot 3" + + # Delete the 'CLEAN with IDA 8.4"' children snapshots recursively skipping the default protected ones in the 'FLARE-VM.20240604' VM + # NOTE: the 'CLEAN with IDA 8.4' root snapshot is skipped in this case + vbox-clean-snapshots.py FLARE-VM.20240604 --root_snapshot "CLEAN with IDA 8.4" + + # Delete all snapshots in the 'FLARE-VM.20240604' VM + vbox-clean-snapshots.py FLARE-VM.20240604 --protected_snapshots "" + """ +) def is_protected(protected_snapshots, snapshot_name): @@ -60,7 +83,7 @@ def get_snapshot_children(vm_name, root_snapshot_name, protected_snapshots): root_snapshot_index = "" if root_snapshot_name: # Find root snapshot: first snapshot with name root_snapshot_name (case sensitive) - root_snapshot_regex = f'^SnapshotName(?P(?:-\d+)*)="{root_snapshot_name}"\n' + root_snapshot_regex = rf'^SnapshotName(?P(?:-\d+)*)="{root_snapshot_name}"\n' root_snapshot = re.search(root_snapshot_regex, snapshots_info, flags=re.M) if root_snapshot: root_snapshot_index = root_snapshot["index"] @@ -69,7 +92,7 @@ def get_snapshot_children(vm_name, root_snapshot_name, protected_snapshots): # Find all root and child snapshots as (snapshot_name, snapshot_id) # Children of a snapshot share the same prefix index - index_regex = f"{root_snapshot_index}(?:-\d+)*" + index_regex = rf"{root_snapshot_index}(?:-\d+)*" snapshot_regex = f'^SnapshotName{index_regex}="(.*?)"\nSnapshotUUID{index_regex}="(.*?)"' snapshots = re.findall(snapshot_regex, snapshots_info, flags=re.M) @@ -112,41 +135,25 @@ def main(argv=None): if argv is None: argv = sys.argv[1:] - epilog = textwrap.dedent( - """ - Example usage: - # Delete all snapshots that do not include 'clean' or 'done' in the name (case insensitive) in the 'FLARE-VM.20240604' VM - vbox-clean-snapshots.py FLARE-VM.20240604 - - # Delete all snapshots that do not include 'clean', 'done', or 'important in the name in the 'FLARE-VM.20240604' VM - vbox-clean-snapshots.py FLARE-VM.20240604 --protected_snapshots "clean,done,important" - - # Delete the 'CLEAN with IDA 8.4' children snapshots recursively skipping the ones that include 'clean' or 'done' in the name (case insensitive) in the 'FLARE-VM.20240604' VM - # NOTE: the 'CLEAN with IDA 8.4' root snapshot is skipped in this case - vbox-clean-snapshots.py FLARE-VM.20240604 --root_snapshot CLEAN with IDA 8.4 - - # Delete the 'Snapshot 3' snapshot and its children recursively skipping the ones that include 'clean' or 'done' in the name (case insensitive) in the 'FLARE-VM.20240604' VM - vbox-clean-snapshots.py FLARE-VM.20240604 --root_snapshot Snapshot 3 - - # Delete all snapshots in the 'FLARE-VM.20240604' VM - vbox-clean-snapshots.py FLARE-VM.20240604 --protected_snapshots "" - """ - ) + epilog = EPILOG parser = argparse.ArgumentParser( - description="Clean a VirtualBox VM up by deleting a snapshot and its children recursively skipping snapshots with a substring in the name.", + description=DESCRIPTION, epilog=epilog, formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument("vm_name", help="Name of the VM to clean up") parser.add_argument( "--root_snapshot", - help="Snapshot name (case sensitive) to delete (and its children recursively). Leave empty to clean all snapshots in the VM.", + help="""Snapshot name (case sensitive) to delete (and its children recursively). + Leave empty to clean all snapshots in the VM.""", ) parser.add_argument( "--protected_snapshots", default="clean,done", type=lambda s: s.split(","), - help='Comma-separated list of strings. Snapshots with any of the strings included in the name (case insensitive) are not deleted. Default: "clean,done"', + help='''Comma-separated list of strings. + Snapshots with any of the strings included in the name (case insensitive) are not deleted. + Default: "clean,done"''', ) args = parser.parse_args(args=argv) diff --git a/virtualbox/vbox-export-snapshots.py b/virtualbox/vbox-export-snapshots.py index 4bbc829..90ce8be 100755 --- a/virtualbox/vbox-export-snapshots.py +++ b/virtualbox/vbox-export-snapshots.py @@ -20,12 +20,14 @@ import re import sys import textwrap +import time from datetime import datetime import jsonschema -from vboxcommon import * +from vboxcommon import ensure_hostonlyif_exists, ensure_vm_running, ensure_vm_shutdown, run_vboxmanage -DESCRIPTION = "Export one or more snapshots in the same VirtualBox VM as .ova, changing the network to a single Host-Only interface. Generate a file with the SHA256 of the exported OVA(s)." +DESCRIPTION = """Export one or more snapshots in the same VirtualBox VM as .ova, changing the network to a single Host-Only interface. +Generate a file with the SHA256 of the exported OVA(s).""" EPILOG = textwrap.dedent( """ @@ -201,20 +203,18 @@ def main(argv=None): ) parser.add_argument( "config_path", - help=textwrap.dedent( - """ - path of the JSON configuration file. + help=""" path of the JSON configuration file. "VM_NAME" is the name of the VM to export snapshots from. Example: "FLARE-VM.testing". "EXPORTED_VM_NAME" is the name of the exported VMs. Example: "FLARE-VM". - "SNAPSHOTS" is a list of lists with information of the snapshots to export: ["SNAPSHOT_NAME", "EXPORTED_VM_EXTENSION", "DESCRIPTION"]. + "SNAPSHOTS" is a list of lists with information of the snapshots to export: + ["SNAPSHOT_NAME", "EXPORTED_VM_EXTENSION", "DESCRIPTION"]. Example: ["FLARE-VM", ".dynamic", "Windows 10 VM with FLARE-VM default configuration"]. "EXPORT_DIR_NAME" (optional) is the name of the directory in HOME to export the VMs. The directory is created if it does not exist. Default: "EXPORTED VMS". - """ - ), + """, ) args = parser.parse_args(args=argv) diff --git a/virtualbox/vboxcommon.py b/virtualbox/vboxcommon.py index 7491758..22a8bd4 100644 --- a/virtualbox/vboxcommon.py +++ b/virtualbox/vboxcommon.py @@ -56,7 +56,7 @@ def get_hostonlyif_name(): # Name: vboxnet0 hostonlyifs_info = run_vboxmanage(["list", "hostonlyifs"]) - match = re.search(f"^Name: *(?P\S+)", hostonlyifs_info, flags=re.M) + match = re.search(r"^Name: *(?P\S+)", hostonlyifs_info, flags=re.M) if match: return match["hostonlyif_name"] @@ -73,7 +73,7 @@ def ensure_hostonlyif_exists(): if not hostonlyif_name: raise RuntimeError("Failed to create new hostonly interface.") - print(f"VM {vm_uuid} Created hostonly interface: {hostonlyif_name}") + print(f"Hostonly interface created: {hostonlyif_name}") return hostonlyif_name @@ -84,7 +84,7 @@ def get_vm_state(vm_uuid): # VMState="poweroff" vm_info = run_vboxmanage(["showvminfo", vm_uuid, "--machinereadable"]) - match = re.search(f'^VMState="(?P\S+)"', vm_info, flags=re.M) + match = re.search(r'^VMState="(?P\S+)"', vm_info, flags=re.M) if match: return match["state"]