def get_apicalls(self, output_file, pid):
        """Download API calls of this process"""
        return self.minio.fget_object(
            self.bucket, f"{self.uid}/apicall/{pid}.json", output_file.name
        )

def get_apicalls ( self , output_file , pid ) : return self . minio . fget_object ( self . bucket , f"{self.uid}/apicall/{pid}.json" , output_file . name )


def get_processed(self, output_file, name):
        """Download post-process results"""
        return self.minio.fget_object(
            self.bucket, f"{self.uid}/{name}.json", output_file.name
        )

def get_processed ( self , output_file , name ) : return self . minio . fget_object ( self . bucket , f"{self.uid}/{name}.json" , output_file . name )


def list_logs(self):
        """List DRAKVUF logs"""
        objects = self.minio.list_objects_v2(self.bucket, f"{self.uid}/")
        return [x.object_name for x in objects if x.object_name.endswith(".log")]

def list_logs ( self ) : objects = self . minio . list_objects_v2 ( self . bucket , f"{self.uid}/" ) return [ x . object_name for x in objects if x . object_name . endswith ( ".log" ) ]


def get_log(self, log_type, output_file, headers=None):
        """Download DRAKVUF log"""
        return self.minio.fget_object(
            self.bucket,
            f"{self.uid}/{log_type}.log",
            output_file.name,
            request_headers=headers,
        )

def get_log ( self , log_type , output_file , headers = None ) : return self . minio . fget_object ( self . bucket , f"{self.uid}/{log_type}.log" , output_file . name , request_headers = headers , )


Download log index useful for quickly accessing n - th log line

def get_log_index(self, log_type, output_file):
        """
        Download log index, useful for quickly accessing n-th
        log line
        """
        return self.minio.fget_object(
            self.bucket, f"{self.uid}/index/{log_type}", output_file.name
        )

def get_log_index ( self , log_type , output_file ) : return self . minio . fget_object ( self . bucket , f"{self.uid}/index/{log_type}" , output_file . name )


def get_pcap_dump(self, output_file):
        """Download dump.pcap file."""
        return self.minio.fget_object(
            self.bucket, f"{self.uid}/dump.pcap", output_file.name
        )

def get_pcap_dump ( self , output_file ) : return self . minio . fget_object ( self . bucket , f"{self.uid}/dump.pcap" , output_file . name )


Download tls session keys in format that is accepted by wireshark .

def get_wireshark_key_file(self, output_file):
        """
        Download tls session keys in format that is accepted by wireshark.
        """
        return self.minio.fget_object(
            self.bucket, f"{self.uid}/wireshark_key_file.txt", output_file.name
        )

def get_wireshark_key_file ( self , output_file ) : return self . minio . fget_object ( self . bucket , f"{self.uid}/wireshark_key_file.txt" , output_file . name )


def get_dumps(self, output_file):
        """Download memory dumps"""
        return self.minio.fget_object(
            self.bucket, f"{self.uid}/dumps.zip", output_file.name
        )

def get_dumps ( self , output_file ) : return self . minio . fget_object ( self . bucket , f"{self.uid}/dumps.zip" , output_file . name )


def get_graph(self, output_file):
        """Download ProcDOT graph"""
        return self.minio.fget_object(
            self.bucket, f"{self.uid}/graph.dot", output_file.name
        )

def get_graph ( self , output_file ) : return self . minio . fget_object ( self . bucket , f"{self.uid}/graph.dot" , output_file . name )


def get_metadata(self):
        """Download metadata.json"""
        try:
            response = None
            response = self.minio.get_object(self.bucket, f"{self.uid}/metadata.json")
            return json.load(response)
        finally:
            # release network resources
            if response is not None:
                response.close()
                response.release_conn()

def
get_metadata
(
self
)
:
try
:
response
=
response
=
self
.
minio
.
get_object
(
self
.
bucket
,
f"{self.uid}/metadata.json"
)
return
json
.
load
(
response
)
finally
:
# release network resources
if
response
is
not
:
response
.
close
(
)
response
.
release_conn
(
)


def enumerate(self):
        """Return iterator over all analyses stored in the bucket"""
        return map(
            lambda obj: AnalysisProxy(self.minio, obj.object_name.strip("/")),
            self.minio.list_objects_v2(self.bucket),
        )

def enumerate ( self ) : return map ( lambda obj : AnalysisProxy ( self . minio , obj . object_name . strip ( "/" ) ) , self . minio . list_objects_v2 ( self . bucket ) , )


def update_metadata_cache():
    """Scans whole MinIO bucket and fetch missing metadata files"""
    for analysis in AnalysisProxy(minio, None).enumerate():
        try:
            get_analysis_metadata(analysis.uid)
        except NoSuchKey:
            # Well, we tried. Too bad
            pass

def
update_metadata_cache
(
)
:
for
analysis
in
AnalysisProxy
(
minio
,
)
.
enumerate
(
)
:
try
:
get_analysis_metadata
(
analysis
.
uid
)
except
NoSuchKey
:
# Well, we tried. Too bad
pass


Return archaive containing dump . pcap along with extracted tls sessions keys in format acceptable by wireshark .

def pcap_dump(task_uid):
    """
    Return archaive containing dump.pcap along with extracted tls sessions
    keys in format acceptable by wireshark.
    """
    analysis = AnalysisProxy(minio, task_uid)
    try:
        with NamedTemporaryFile() as f_pcap, NamedTemporaryFile() as f_keys, NamedTemporaryFile() as f_archive:
            with ZipFile(f_archive, "w", ZIP_DEFLATED) as archive:
                analysis.get_pcap_dump(f_pcap)
                archive.write(f_pcap.name, "dump.pcap")
                try:
                    analysis.get_wireshark_key_file(f_keys)
                    archive.write(f_keys.name, "dump.keys")
                except NoSuchKey:
                    # No dumped keys.
                    pass
            f_archive.seek(0)
            return send_file(f_archive.name, mimetype="application/zip")
    except NoSuchKey:
        abort(404, description="No network traffic avaible.")

def
pcap_dump
(
task_uid
)
:
analysis
=
AnalysisProxy
(
minio
,
task_uid
)
try
:
with
NamedTemporaryFile
(
)
as
f_pcap
,
NamedTemporaryFile
(
)
as
f_keys
,
NamedTemporaryFile
(
)
as
f_archive
:
with
ZipFile
(
f_archive
,
"w"
,
ZIP_DEFLATED
)
as
archive
:
analysis
.
get_pcap_dump
(
f_pcap
)
archive
.
write
(
f_pcap
.
name
,
"dump.pcap"
)
try
:
analysis
.
get_wireshark_key_file
(
f_keys
)
archive
.
write
(
f_keys
.
name
,
"dump.keys"
)
except
NoSuchKey
:
# No dumped keys.
pass
f_archive
.
seek
(
0
)
return
send_file
(
f_archive
.
name
,
mimetype
=
"application/zip"
)
except
NoSuchKey
:
abort
(
404
,
description
=
"No network traffic avaible."
)


Retrieves all processes with given pid that were alive in provided time period : [ ts_from ts_to ] .

def get_processes(self, pid: int, ts_from: float, ts_to: float) -> List[Process]:
        """
        Retrieves all processes with given pid that were alive in provided time period: [ts_from, ts_to].
        """
        processes = []
        for p in self.processes:
            if (
                pid == p.pid
                and (p.ts_to is None or ts_from <= p.ts_to)
                and p.ts_from <= ts_to
            ):
                processes.append(p)
        return processes

def get_processes ( self , pid : int , ts_from : float , ts_to : float ) -> List [ Process ] : processes = [ ] for p in self . processes : if ( pid == p . pid and ( p . ts_to is None or ts_from <= p . ts_to ) and p . ts_from <= ts_to ) : processes . append ( p ) return processes


Retrieves process with given pid that was alive in provided time period . Fails if there is more than 1 process that fulfills this condidions .

def get_single_process(self, pid, ts_from, ts_to) -> Optional[Process]:
        """
        Retrieves process with given pid that was alive in provided time period.
        Fails if there is more than 1 process that fulfills this condidions.
        """
        processes = self.get_processes(pid, ts_from, ts_to)
        if len(processes) > 1:
            raise MultipleProcessesReturned(processes)
        return next(iter(processes), None)

def get_single_process ( self , pid , ts_from , ts_to ) -> Optional [ Process ] : processes = self . get_processes ( pid , ts_from , ts_to ) if len ( processes ) > 1 : raise MultipleProcessesReturned ( processes ) return next ( iter ( processes ) , None )


Why is this required? Currently there s no easy way to notify web application about the analysis being finished . In order for it to find out user has to explicitly ask an endpoint for metadata of given analysis . Otherwise we re be forced to query MinIO every time user requests a list of analyses .

def insert_metadata(task: Task, resources: Dict[str, RemoteResource], minio):
    """
    Why is this required?
    Currently there's no easy way to notify web application about the analysis
    being finished. In order for it to find out, user has to explicitly ask
    an endpoint for metadata of given analysis. Otherwise we're be forced to
    query MinIO every time user requests a list of analyses.
    """
    analysis_uid = task.payload["analysis_uid"]
    # Trigger metadata request, thus pulling it into cache
    get_analysis_metadata(analysis_uid)


Runs subprocess . run in a try except with some default arguments ( which can be overriden by supplying kwargs )

def try_run(
    list_args: list, msg: str, reraise=True, **kwargs
) -> subprocess.CompletedProcess:
    """
    Runs subprocess.run in a try except with some default arguments ( which can be overriden by supplying kwargs )

            Parameters:
                    list_args (list): Subprocess list which will be passed to subprocess.run
                    msg (str): A meaningful error message to be raised during non-zero exit code
                    reraise (bool):
                        True: will raise an Exception with traceback on error
                        False: will log a warning on error
                    **kwargs: additional parameters to be passed to subprocess.run

            Defaults:
                    subprocess.run is called with the following arguments by default
                    stderr = subprocess.PIPE
                    stdout = subprocess.PIPE # to print to console, pass kwargs ( stdout = None )
                    check = True

            Returns:
                    sub (subprocess.CompletedProcess): Object with the completed process
                        or
                    None: if the subprocess failed and reraise=False
    """

    try:
        kwargs["stdout"]
    except KeyError:
        kwargs["stdout"] = subprocess.PIPE

    if kwargs.get("stderr") is None:
        kwargs["stderr"] = subprocess.PIPE
    if kwargs.get("check") is None:
        kwargs["check"] = True

    try:
        sub = subprocess.run(list_args, **kwargs)
    except (FileNotFoundError, TypeError) as e:
        logging.debug("arguments to subprocess")
        logging.debug(list_args)
        logging.debug(msg)
        logging.debug(kwargs)
        raise Exception("Command not found") from e
    except subprocess.CalledProcessError as e:
        if e.stdout is not None:
            logging.debug("stdout: \n{}".format(e.stdout.decode()))
        if e.stderr is not None:
            logging.debug("stderr: \n{}".format(e.stderr.decode()))
        logging.debug("returncode: {}".format(e.returncode))
        if reraise:
            raise Exception(msg) from e
        else:
            logging.warning(msg)
            logging.debug(traceback.format_exc())
            return None
    return sub

def
try_run
(
list_args
:
list
,
msg
:
str
,
reraise
=
True
,
*
*
kwargs
)
->
subprocess
.
CompletedProcess
:
try
:
kwargs
[
"stdout"
]
except
KeyError
:
kwargs
[
"stdout"
]
=
subprocess
.
PIPE
if
kwargs
.
get
(
"stderr"
)
is
:
kwargs
[
"stderr"
]
=
subprocess
.
PIPE
if
kwargs
.
get
(
"check"
)
is
:
kwargs
[
"check"
]
=
True
try
:
sub
=
subprocess
.
run
(
list_args
,
*
*
kwargs
)
except
(
FileNotFoundError
,
TypeError
)
as
e
:
logging
.
debug
(
"arguments to subprocess"
)
logging
.
debug
(
list_args
)
logging
.
debug
(
msg
)
logging
.
debug
(
kwargs
)
raise
Exception
(
"Command not found"
)
from
e
except
subprocess
.
CalledProcessError
as
e
:
if
e
.
stdout
is
not
:
logging
.
debug
(
"stdout: \n{}"
.
format
(
e
.
stdout
.
decode
(
)
)
)
if
e
.
stderr
is
not
:
logging
.
debug
(
"stderr: \n{}"
.
format
(
e
.
stderr
.
decode
(
)
)
)
logging
.
debug
(
"returncode: {}"
.
format
(
e
.
returncode
)
)
if
reraise
:
raise
Exception
(
msg
)
from
e
else
:
logging
.
warning
(
msg
)
logging
.
debug
(
traceback
.
format_exc
(
)
)
return
return
sub


Restore virtual machine from snapshot . : raises : subprocess . CalledProcessError

def restore(self, snapshot_path=None, pause=False, **kwargs) -> None:
        """Restore virtual machine from snapshot.
        :raises: subprocess.CalledProcessError
        """
        if self.is_running:
            self.destroy()

        args = ["xl", "restore"]

        if snapshot_path is None:
            snapshot_path = Path(VOLUME_DIR) / "snapshot.sav"

        if pause is True:
            args += ["-p"]

        if kwargs.get("stderr") is None:
            kwargs["stderr"] = subprocess.STDOUT

        # we are explicitely passing stdout=None so that things can be print to terminal
        kwargs["stdout"] = kwargs.get("stdout")

        # No need to rollback vm-0. Since the state of vm-0
        # is correct by definition.
        if self.vm_id != 0 and self.backend is not None and self.vm_id is not None:
            self.backend.rollback_vm_storage(self.vm_id)

        args += [self._cfg_path, snapshot_path]
        logging.info(f"Restoring VM {self.vm_name}")
        try_run(args, msg=f"Failed to restore VM {self.vm_name}", **kwargs)


Destroy a running virtual machine . : raises : subprocess . CalledProcessError

def destroy(self, **kwargs):
        """Destroy a running virtual machine.
        :raises: subprocess.CalledProcessError
        """
        if self.is_running:
            logging.info(f"Destroying {self.vm_name}")
            try_run(
                ["xl", "destroy", self.vm_name],
                f"Failed to destroy VM {self.vm_name}",
                **kwargs,
            )

def
destroy
(
self
,
*
*
kwargs
)
:
if
self
.
is_running
:
logging
.
info
(
f"Destroying {self.vm_name}"
)
try_run
(
[
"xl"
,
"destroy"
,
self
.
vm_name
]
,
f"Failed to destroy VM {self.vm_name}"
,
*
*
kwargs
,
)


Dump raw memory from running vm using vmi - dump - memory and compress it with gzip : raises : subprocess . CalledProcessError

def memory_dump(self, compressed_filepath):
        """Dump raw memory from running vm using vmi-dump-memory and compress it with gzip
        :raises: subprocess.CalledProcessError
        """

        with tempfile.NamedTemporaryFile() as raw_memdump, open(
            compressed_filepath, "wb"
        ) as compressed_file:

            log.info(f"Dumping raw memory from {self.vm_name} guest...")
            try:
                subprocess.run(
                    ["vmi-dump-memory", self.vm_name, raw_memdump.name], check=True
                )
            except subprocess.CalledProcessError as e:
                log.error(f"Dumping raw memory from {self.vm_name} failed.")
                raise e

            log.info(f"Compressing {self.vm_name} guest memory dump...")
            try:
                subprocess.run(
                    ["gzip", "-c", raw_memdump.name], check=True, stdout=compressed_file
                )
            except subprocess.CalledProcessError as e:
                log.error(f"Compressing raw memory from {self.vm_name} failed.")
                raise e


Determine final plugin list that will be used during analysis .

def get_plugin_list(self, quality, requested_plugins):
        """
        Determine final plugin list that will be used during analysis.
        """
        plugin_list = self.active_plugins["_all_"]
        if quality in self.active_plugins:
            plugin_list = self.active_plugins[quality]
        plugin_list = list(set(plugin_list) & set(requested_plugins))

        if "ipt" in plugin_list and "codemon" not in plugin_list:
            self.log.info("Using ipt plugin implies using codemon")
            plugin_list.append("codemon")
        return plugin_list


def reconfigure(cls, config: Dict[str, str]):
        """Reconfigure DrakrunKarton class"""

        def load_json(config, key):
            try:
                return json.loads(config.get(key)) if key in config else None
            except json.JSONDecodeError:
                raise RuntimeError(
                    f"Key '{key}' in section [drakrun] is not valid JSON"
                )

        cls.identity = config.get("identity", cls.DEFAULT_IDENTITY)
        cls.filters = load_json(config, "filters") or cls.DEFAULT_FILTERS
        cls.headers = load_json(config, "headers") or cls.DEFAULT_HEADERS
        cls.test_headers = load_json(config, "test_headers") or cls.DEFAULT_TEST_HEADERS
        cls.test_filters = load_json(config, "test_filters") or cls.DEFAULT_TEST_FILTERS

        # If testing is enabled, add additional test filters from the configuration
        # or fall back to hardcoded
        if config.getboolean("sample_testing", fallback=False):
            cls.filters.extend(cls.test_filters)


Drakrun dumps come in form : <base > _<hash > e . g . 405000_688f58c58d798ecb that can be read as a dump from address 0x405000 with a content hash equal to 688f58c58d798ecb .

def _get_base_from_drakrun_dump(self, dump_name):
        """
        Drakrun dumps come in form: <base>_<hash> e.g. 405000_688f58c58d798ecb,
        that can be read as a dump from address 0x405000 with a content hash
        equal to 688f58c58d798ecb.
        """
        return hex(int(dump_name.split("_")[0], 16))

def _get_base_from_drakrun_dump ( self , dump_name ) : return hex ( int ( dump_name . split ( "_" ) [ 0 ] , 16 ) )


Put analysis ID - > drakrun node mapping into Redis . Required to know where to connect VNC client

def update_vnc_info(self):
        """
        Put analysis ID -> drakrun node mapping into Redis.
        Required to know where to connect VNC client
        """
        self.backend.redis.set(
            f"drakvnc:{self.analysis_uid}", self.instance_id, ex=3600  # 1h
        )

def
update_vnc_info
(
self
)
:
self
.
backend
.
redis
.
set
(
f"drakvnc:{self.analysis_uid}"
,
self
.
instance_id
,
ex
=
3600
# 1h
)


Offload drakrun - prod by sending raw analysis output to be processed by drakrun . processor .

def send_raw_analysis(self, sample, outdir, metadata, dumps_metadata, quality):
        """
        Offload drakrun-prod by sending raw analysis output to be processed by
        drakrun.processor.
        """

        if self.test_run:
            headers = dict(self.test_headers)
        else:
            headers = dict(self.headers)

        headers["quality"] = quality

        task = Task(headers, payload=metadata)
        task.add_payload("sample", sample)
        task.add_payload("dumps_metadata", dumps_metadata)

        if self.test_run:
            task.add_payload("testcase", self.current_task.payload["testcase"])

        if self.config.config.getboolean("drakrun", "attach_profiles", fallback=False):
            self.log.info("Uploading profiles...")
            task.add_payload("profiles", self.build_profile_payload())

        if self.config.config.getboolean(
            "drakrun", "attach_apiscout_profile", fallback=False
        ):
            self.log.info("Uploading static ApiScout profile...")
            task.add_payload(
                "static_apiscout_profile.json",
                LocalResource(
                    name="static_apiscout_profile.json",
                    path=Path(APISCOUT_PROFILE_DIR) / "static_apiscout_profile.json",
                ),
            )

        self.log.info("Uploading artifacts...")
        for resource in self.upload_artifacts(self.analysis_uid, outdir):
            task.add_payload(resource.name, resource)

        self.send_task(task)

def
send_raw_analysis
(
self
,
sample
,
outdir
,
metadata
,
dumps_metadata
,
quality
)
:
if
self
.
test_run
:
headers
=
dict
(
self
.
test_headers
)
else
:
headers
=
dict
(
self
.
headers
)
headers
[
"quality"
]
=
quality
task
=
Task
(
headers
,
payload
=
metadata
)
task
.
add_payload
(
"sample"
,
sample
)
task
.
add_payload
(
"dumps_metadata"
,
dumps_metadata
)
if
self
.
test_run
:
task
.
add_payload
(
"testcase"
,
self
.
current_task
.
payload
[
"testcase"
]
)
if
self
.
config
.
config
.
getboolean
(
"drakrun"
,
"attach_profiles"
,
fallback
=
False
)
:
self
.
log
.
info
(
"Uploading profiles..."
)
task
.
add_payload
(
"profiles"
,
self
.
build_profile_payload
(
)
)
if
self
.
config
.
config
.
getboolean
(
"drakrun"
,
"attach_apiscout_profile"
,
fallback
=
False
)
:
self
.
log
.
info
(
"Uploading static ApiScout profile..."
)
task
.
add_payload
(
"static_apiscout_profile.json"
,
LocalResource
(
name
=
"static_apiscout_profile.json"
,
path
=
Path
(
APISCOUT_PROFILE_DIR
)
/
"static_apiscout_profile.json"
,
)
,
)
self
.
log
.
info
(
"Uploading artifacts..."
)
for
resource
in
self
.
upload_artifacts
(
self
.
analysis_uid
,
outdir
)
:
task
.
add_payload
(
resource
.
name
,
resource
)
self
.
send_task
(
task
)


def _get_cmdline_generic(self, method: str) -> List[str]:
        """Build base command line for all injection methods"""
        return [
            "injector",
            "-o",
            "json",
            "-d",
            self.vm_name,
            "-r",
            self.kernel_profile,
            "-i",
            str(self.runtime_info.inject_pid),
            "-k",
            hex(self.runtime_info.vmi_offsets.kpgd),
            "-m",
            method,
        ]

def _get_cmdline_generic ( self , method : str ) -> List [ str ] : return [ "injector" , "-o" , "json" , "-d" , self . vm_name , "-r" , self . kernel_profile , "-i" , str ( self . runtime_info . inject_pid ) , "-k" , hex ( self . runtime_info . vmi_offsets . kpgd ) , "-m" , method , ]


def write_file(
        self, local_path: str, remote_path: str, timeout: int = 60
    ) -> subprocess.CompletedProcess:
        """Copy local file to the VM"""
        injector_cmd = self._get_cmdline_writefile(local_path, remote_path)
        return subprocess.run(
            injector_cmd, stdout=subprocess.PIPE, timeout=timeout, check=True
        )

def write_file ( self , local_path : str , remote_path : str , timeout : int = 60 ) -> subprocess . CompletedProcess : injector_cmd = self . _get_cmdline_writefile ( local_path , remote_path ) return subprocess . run ( injector_cmd , stdout = subprocess . PIPE , timeout = timeout , check = True )


def read_file(
        self, remote_path: str, local_path: str, timeout: int = 60
    ) -> subprocess.CompletedProcess:
        """Copy VM file to local"""
        injector_cmd = self._get_cmdline_readfile(remote_path, local_path)
        return subprocess.run(injector_cmd, timeout=timeout, capture_output=True)

def read_file ( self , remote_path : str , local_path : str , timeout : int = 60 ) -> subprocess . CompletedProcess : injector_cmd = self . _get_cmdline_readfile ( remote_path , local_path ) return subprocess . run ( injector_cmd , timeout = timeout , capture_output = True )


def create_process(
        self, cmdline: str, wait: bool = False, timeout: int = 60
    ) -> subprocess.CompletedProcess:
        injector_cmd = self._get_cmdline_createproc(cmdline, wait=wait)
        """ Create a process inside the VM with given command line """
        return subprocess.run(injector_cmd, check=True)

def create_process ( self , cmdline : str , wait : bool = False , timeout : int = 60 ) -> subprocess . CompletedProcess : injector_cmd = self . _get_cmdline_createproc ( cmdline , wait = wait ) return subprocess . run ( injector_cmd , check = True )


Based on https : // stackoverflow . com / a / 16076661 / 12452744

def get_product_version(pe: pefile.PE) -> str:
    """
    Based on https://stackoverflow.com/a/16076661/12452744
    """

    def LOWORD(dword):
        return dword & 0x0000FFFF

    def HIWORD(dword):
        return dword >> 16

    if len(pe.VS_FIXEDFILEINFO) != 1:
        log.error("Unsupported case: len(pe.VS_FIXEDFILEINFO) != 1")
        return None
    try:
        ms = pe.VS_FIXEDFILEINFO[0].ProductVersionMS
        ls = pe.VS_FIXEDFILEINFO[0].ProductVersionLS
        return "{}.{}.{}.{}".format(HIWORD(ms), LOWORD(ms), HIWORD(ls), LOWORD(ls))
    except AttributeError:
        log.exception("")
        return None


def make_static_apiscout_profile_for_dll(filepath: str) -> Dict[str, Any]:
    """
    Based on https://github.com/danielplohmann/apiscout/blob/0fca2eefa5b557b05eb77ab7a3246825f7aa71c3/apiscout/db_builder/DatabaseBuilder.py#L99-L127
    """
    pe = pefile.PE(filepath, fast_load=True)
    pe.parse_data_directories(
        directories=[
            pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_EXPORT"],
            pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_RESOURCE"],
        ]
    )

    dll_entry = {}
    dll_entry["base_address"] = pe.OPTIONAL_HEADER.ImageBase
    dll_entry["bitness"] = get_bitness(pe)
    dll_entry["version"] = get_product_version(pe) or "0.0.0.0"
    dll_entry["filepath"] = filepath
    dll_entry["aslr_offset"] = 0
    dll_entry["exports"] = []
    if not hasattr(pe, "DIRECTORY_ENTRY_EXPORT"):
        if pe.is_driver():
            return dll_entry
        raise RuntimeError(f"DIRECTORY_ENTRY_EXPORT not found in '{filepath}'")
    for exp in sorted(pe.DIRECTORY_ENTRY_EXPORT.symbols, key=attrgetter("address")):
        export_info = {}

        export_info["address"] = exp.address
        if exp.name is None:
            export_info["name"] = "None"
        else:
            export_info["name"] = exp.name.decode()
        export_info["ordinal"] = exp.ordinal
        dll_entry["exports"].append(export_info)

    return dll_entry


def build_apiscout_dll_key(dll_info: Dict[str, Any]) -> str:
    """
    From https://github.com/danielplohmann/apiscout/blob/0fca2eefa5b557b05eb77ab7a3246825f7aa71c3/apiscout/db_builder/DatabaseBuilder.py#L129-L131
    """
    filename = PureWindowsPath(dll_info["filepath"]).name
    return "{}_{}_{}_0x{:x}".format(
        dll_info["bitness"], dll_info["version"], filename, dll_info["base_address"]
    )

def build_apiscout_dll_key ( dll_info : Dict [ str , Any ] ) -> str : filename = PureWindowsPath ( dll_info [ "filepath" ] ) . name return "{}_{}_{}_0x{:x}" . format ( dll_info [ "bitness" ] , dll_info [ "version" ] , filename , dll_info [ "base_address" ] )


If codeview_struct is an instance of Container it should be returned from CV_RSDS_HEADER . parse () .

def make_symstore_hash(
    codeview_struct: Union[Container, pdbparse.PDBInfoStream]
) -> str:
    """
    If `codeview_struct` is an instance of Container, it should be returned from `CV_RSDS_HEADER.parse()`.
    """
    guid = codeview_struct.GUID
    guid_str = "%08x%04x%04x%s" % (
        guid.Data1,
        guid.Data2,
        guid.Data3,
        guid.Data4.hex(),
    )
    return "%s%x" % (guid_str, codeview_struct.Age)

def make_symstore_hash ( codeview_struct : Union [ Container , pdbparse . PDBInfoStream ] ) -> str : guid = codeview_struct . GUID guid_str = "%08x%04x%04x%s" % ( guid . Data1 , guid . Data2 , guid . Data3 , guid . Data4 . hex ( ) , ) return "%s%x" % ( guid_str , codeview_struct . Age )


def DemangleName(self, mangled_name):
        """Returns the de-mangled name.

        At this stage we don't really do proper demangling since we usually dont
        care about the prototype, nor c++ exports. In the future we should
        though.
        """
        m = self.SIMPLE_X86_CALL.match(mangled_name)
        if m:
            # If we see x86 name mangling (_cdecl, __stdcall) with stack sizes
            # of 4 bytes, this is definitely a 32 bit pdb. Sometimes we dont
            # know the architecture of the pdb file for example if we do not
            # have the original binary, but only the GUID as extracted by
            # version_scan.
            # TODO set arch to i386
            return m.group(1)

        m = self.FUNCTION_NAME_RE.match(mangled_name)
        if m:
            return m.group(1)

        # Strip the first _ from the name. I386 mangled constants have a
        # leading _ but their AMD64 counterparts do not.
        if mangled_name and mangled_name[0] in "_.":
            mangled_name = mangled_name[1:]

        elif mangled_name.startswith("??_C@"):
            return self._UnpackMangledString(mangled_name)

        return mangled_name


def on_create_rekall_profile_failure(
    msg: str,
    should_raise: bool,
    exception: Exception = None,
):
    """
    An exception handler for create_rekall_profile

        Parameters:
            msg (str): Message to raise
            should_raise (bool): Should it raise an exception or log a warning
            exception (Exception): Exception object which is used for tracebacks

        Returns:
            None
    """
    if should_raise:
        raise Exception(f"[REQUIRED DLL] {msg}") from exception
    else:
        logging.warning(f"[SKIPPING DLL] {msg}")
        logging.debug(traceback.format_exc())

def
on_create_rekall_profile_failure
(
msg
:
str
,
should_raise
:
bool
,
exception
:
Exception
=
,
)
:
if
should_raise
:
raise
Exception
(
f"[REQUIRED DLL] {msg}"
)
from
exception
else
:
logging
.
warning
(
f"[SKIPPING DLL] {msg}"
)
logging
.
debug
(
traceback
.
format_exc
(
)
)


Call get - explorer - pid helper and get its PID

def extract_explorer_pid(
    domain: str, kernel_profile: str, offsets: VmiOffsets, timeout: int = 30
) -> int:
    """Call get-explorer-pid helper and get its PID"""
    module_dir = os.path.dirname(os.path.realpath(__file__))
    pid_tool = os.path.join(module_dir, "tools", "get-explorer-pid")
    try:
        explorer_pid_s = subprocess.check_output(
            [pid_tool, domain, kernel_profile, hex(offsets.kpgd)], timeout=timeout
        ).decode("utf-8", "ignore")

        m = re.search(r"explorer\.exe:([0-9]+)", explorer_pid_s)
        if m is not None:
            return int(m.group(1))

    except subprocess.CalledProcessError:
        logging.exception("get-explorer-pid exited with an error")
    except subprocess.TimeoutExpired:
        logging.exception("get-explorer-pid timed out")

    raise RuntimeError("Extracting explorer PID failed")

def
extract_explorer_pid
(
domain
:
str
,
kernel_profile
:
str
,
offsets
:
VmiOffsets
,
timeout
:
int
=
30
)
->
int
:
module_dir
=
os
.
path
.
dirname
(
os
.
path
.
realpath
(
__file__
)
)
pid_tool
=
os
.
path
.
join
(
module_dir
,
"tools"
,
"get-explorer-pid"
)
try
:
explorer_pid_s
=
subprocess
.
check_output
(
[
pid_tool
,
domain
,
kernel_profile
,
hex
(
offsets
.
kpgd
)
]
,
timeout
=
timeout
)
.
decode
(
"utf-8"
,
"ignore"
)
m
=
re
.
search
(
r"explorer\.exe:([0-9]+)"
,
explorer_pid_s
)
if
m
is
not
:
return
int
(
m
.
group
(
1
)
)
except
subprocess
.
CalledProcessError
:
logging
.
exception
(
"get-explorer-pid exited with an error"
)
except
subprocess
.
TimeoutExpired
:
logging
.
exception
(
"get-explorer-pid timed out"
)
raise
RuntimeError
(
"Extracting explorer PID failed"
)


Call vmi - win - offsets helper and obtain VmiOffsets values

def extract_vmi_offsets(
    domain: str, kernel_profile: str, timeout: int = 30
) -> VmiOffsets:
    """Call vmi-win-offsets helper and obtain VmiOffsets values"""
    try:
        output = subprocess.check_output(
            ["vmi-win-offsets", "--name", domain, "--json-kernel", kernel_profile],
            timeout=timeout,
        ).decode("utf-8", "ignore")

        return VmiOffsets.from_tool_output(output)
    except TypeError:
        logging.exception("Invalid output of vmi-win-offsets")
    except subprocess.CalledProcessError:
        logging.exception("vmi-win-offsets exited with an error")
    except subprocess.TimeoutExpired:
        logging.exception("vmi-win-offsets timed out")

    raise RuntimeError("Extracting VMI offsets failed")

def
extract_vmi_offsets
(
domain
:
str
,
kernel_profile
:
str
,
timeout
:
int
=
30
)
->
VmiOffsets
:
try
:
output
=
subprocess
.
check_output
(
[
"vmi-win-offsets"
,
"--name"
,
domain
,
"--json-kernel"
,
kernel_profile
]
,
timeout
=
timeout
,
)
.
decode
(
"utf-8"
,
"ignore"
)
return
VmiOffsets
.
from_tool_output
(
output
)
except
TypeError
:
logging
.
exception
(
"Invalid output of vmi-win-offsets"
)
except
subprocess
.
CalledProcessError
:
logging
.
exception
(
"vmi-win-offsets exited with an error"
)
except
subprocess
.
TimeoutExpired
:
logging
.
exception
(
"vmi-win-offsets timed out"
)
raise
RuntimeError
(
"Extracting VMI offsets failed"
)


Creates usermode profiles by restoring vm - 1 and extracting the DLLs . Assumes that injector is configured properly i . e . kernel and runtime profiles exist and that vm - 1 is free to use .

def create_missing_profiles():
    """
    Creates usermode profiles by restoring vm-1 and extracting the DLLs.
    Assumes that injector is configured properly, i.e. kernel and runtime
    profiles exist and that vm-1 is free to use.
    """

    # Prepare injector
    with open(os.path.join(PROFILE_DIR, "runtime.json"), "r") as runtime_f:
        runtime_info = RuntimeInfo.load(runtime_f)
    kernel_profile = os.path.join(PROFILE_DIR, "kernel.json")
    injector = Injector("vm-1", runtime_info, kernel_profile)

    # restore vm-1
    out_interface = conf["drakrun"].get("out_interface", "")
    dns_server = conf["drakrun"].get("dns_server", "")
    install_info = InstallInfo.load()
    backend = get_storage_backend(install_info)

    generate_vm_conf(install_info, 1)
    setup_vm_network(
        vm_id=1, net_enable=False, out_interface=out_interface, dns_server=dns_server
    )
    vm = VirtualMachine(backend, 1)
    vm.restore()

    # Ensure that all declared usermode profiles exist
    # This is important when upgrade defines new entries in dll_file_list and compulsory_dll_file_list
    for profile in compulsory_dll_file_list:
        if not profiles_exist(profile.dest):
            create_rekall_profile(injector, profile, True)

    for profile in dll_file_list:
        if not profiles_exist(profile.dest):
            try:
                create_rekall_profile(injector, profile)
            except Exception:
                log.exception("Unexpected exception from create_rekall_profile!")

    build_os_info(APISCOUT_PROFILE_DIR, vmi_win_guid(vm.vm_name), backend)

    dll_basename_list = [dll.dest for dll in dll_file_list]
    static_apiscout_profile = build_static_apiscout_profile(
        APISCOUT_PROFILE_DIR, dll_basename_list
    )
    with open(Path(APISCOUT_PROFILE_DIR) / "static_apiscout_profile.json", "w") as f:
        json.dump(static_apiscout_profile, f)

    vm.destroy()
    delete_vm_network(
        vm_id=1, net_enable=False, out_interface=out_interface, dns_server=dns_server
    )


Enable or disable additional parallel instances of drakrun service ..

def scale(scale_count):
    """Enable or disable additional parallel instances of drakrun service.."""
    if scale_count < 1:
        raise RuntimeError("Invalid value of scale parameter. Must be at least 1.")

    cur_services = set(list(get_enabled_drakruns()))
    new_services = set([f"drakrun@{i}.service" for i in range(1, scale_count + 1)])

    disable_services = cur_services - new_services
    enable_services = new_services

    wait_processes(
        "disable services",
        [
            subprocess.Popen(
                ["systemctl", "disable", service],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            )
            for service in disable_services
        ],
    )
    wait_processes(
        "enable services",
        [
            subprocess.Popen(
                ["systemctl", "enable", service],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            )
            for service in enable_services
        ],
    )
    wait_processes(
        "start services",
        [
            subprocess.Popen(
                ["systemctl", "start", service],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            )
            for service in enable_services
        ],
    )
    wait_processes(
        "stop services",
        [
            subprocess.Popen(
                ["systemctl", "stop", service],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            )
            for service in disable_services
        ],
    )


Inject ISO file into specified guest vm . Domain can be retrieved by running xl list command on the host .

def mount(iso_path, domain_name):
    """Inject ISO file into specified guest vm.
    Domain can be retrieved by running "xl list" command on the host.
    """
    iso_path_full = os.path.abspath(iso_path)
    insert_cd(domain_name, FIRST_CDROM_DRIVE, iso_path_full)

def mount ( iso_path , domain_name ) : iso_path_full = os . path . abspath ( iso_path ) insert_cd ( domain_name , FIRST_CDROM_DRIVE , iso_path_full )


Perform minimal snapshot export symmetric to do_import_minimal

def do_export_minimal(mc, bucket, name):
    """Perform minimal snapshot export, symmetric to do_import_minimal"""
    logging.info("Uploading installation info")
    install_info = InstallInfo.load()
    install_data = json.dumps(install_info.to_dict()).encode()
    mc.put_object(
        bucket, f"{name}/install.json", io.BytesIO(install_data), len(install_data)
    )

    logging.info("Uploading VM template")
    mc.fput_object(
        bucket, f"{name}/cfg.template", os.path.join(ETC_DIR, "scripts", "cfg.template")
    )

    with tempfile.NamedTemporaryFile() as disk_image:
        logging.info("Exporting VM hard drive")
        storage = get_storage_backend(install_info)
        storage.export_vm0(disk_image.name)

        logging.info("Uploading disk.img")
        mc.fput_object(bucket, f"{name}/disk.img", disk_image.name)

def
do_export_minimal
(
mc
,
bucket
,
name
)
:
logging
.
info
(
"Uploading installation info"
)
install_info
=
InstallInfo
.
load
(
)
install_data
=
json
.
dumps
(
install_info
.
to_dict
(
)
)
.
encode
(
)
mc
.
put_object
(
bucket
,
f"{name}/install.json"
,
io
.
BytesIO
(
install_data
)
,
len
(
install_data
)
)
logging
.
info
(
"Uploading VM template"
)
mc
.
fput_object
(
bucket
,
f"{name}/cfg.template"
,
os
.
path
.
join
(
ETC_DIR
,
"scripts"
,
"cfg.template"
)
)
with
tempfile
.
NamedTemporaryFile
(
)
as
disk_image
:
logging
.
info
(
"Exporting VM hard drive"
)
storage
=
get_storage_backend
(
install_info
)
storage
.
export_vm0
(
disk_image
.
name
)
logging
.
info
(
"Uploading disk.img"
)
mc
.
fput_object
(
bucket
,
f"{name}/disk.img"
,
disk_image
.
name
)


Perform minimal snapshot import symmetric to do_export_minimal

def do_import_minimal(mc, name, bucket, zpool):
    """Perform minimal snapshot import, symmetric to do_export_minimal"""
    logging.info("Downloading installation info")
    mc.fget_object(
        bucket,
        f"{name}/install.json",
        os.path.join(ETC_DIR, InstallInfo._INSTALL_FILENAME),
    )

    logging.info("Downloading VM config")
    mc.fget_object(
        bucket, f"{name}/cfg.template", os.path.join(ETC_DIR, "scripts", "cfg.template")
    )

    # Now we have imported InstallInfo object
    install_info = InstallInfo.load()

    # Patch ZFS pool name
    if zpool is not None:
        install_info.zfs_tank_name = zpool
        # Save patched ZFS dataset name (storage backend has to know it)
        install_info.save()

    storage = get_storage_backend(install_info)

    with tempfile.NamedTemporaryFile() as disk_image:
        logging.info("Downloading VM disk image")
        mc.fget_object(bucket, f"{name}/disk.img", disk_image.name)

        logging.info("Importing VM disk")
        storage.import_vm0(disk_image.name)


Perform full snapshot export symmetric to do_import_full

def do_export_full(mc, bucket, name):
    """Perform full snapshot export, symmetric to do_import_full"""
    do_export_minimal(mc, bucket, name)

    with tempfile.NamedTemporaryFile() as compressed_snapshot:
        # Compress snapshot
        logging.info("Compressing snapshot.sav")
        subprocess.check_call(
            ["gzip", "-c", os.path.join(VOLUME_DIR, "snapshot.sav")],
            stdout=compressed_snapshot,
        )

        logging.info("Uploading snapshot.sav.gz")
        mc.fput_object(bucket, f"{name}/snapshot.sav.gz", compressed_snapshot.name)

    # Upload profiles
    for file in os.listdir(PROFILE_DIR):
        logging.info("Uploading profile %s", file)
        mc.fput_object(
            bucket, f"{name}/profiles/{file}", os.path.join(PROFILE_DIR, file)
        )

    # Upload ApiScout profile
    for file in os.listdir(APISCOUT_PROFILE_DIR):
        logging.info("Uploading file %s", file)
        mc.fput_object(
            bucket,
            f"{name}/apiscout_profile/{file}",
            os.path.join(APISCOUT_PROFILE_DIR, file),
        )

def
do_export_full
(
mc
,
bucket
,
name
)
:
do_export_minimal
(
mc
,
bucket
,
name
)
with
tempfile
.
NamedTemporaryFile
(
)
as
compressed_snapshot
:
# Compress snapshot
logging
.
info
(
"Compressing snapshot.sav"
)
subprocess
.
check_call
(
[
"gzip"
,
"-c"
,
os
.
path
.
join
(
VOLUME_DIR
,
"snapshot.sav"
)
]
,
stdout
=
compressed_snapshot
,
)
logging
.
info
(
"Uploading snapshot.sav.gz"
)
mc
.
fput_object
(
bucket
,
f"{name}/snapshot.sav.gz"
,
compressed_snapshot
.
name
)
# Upload profiles
for
file
in
os
.
listdir
(
PROFILE_DIR
)
:
logging
.
info
(
"Uploading profile %s"
,
file
)
mc
.
fput_object
(
bucket
,
f"{name}/profiles/{file}"
,
os
.
path
.
join
(
PROFILE_DIR
,
file
)
)
# Upload ApiScout profile
for
file
in
os
.
listdir
(
APISCOUT_PROFILE_DIR
)
:
logging
.
info
(
"Uploading file %s"
,
file
)
mc
.
fput_object
(
bucket
,
f"{name}/apiscout_profile/{file}"
,
os
.
path
.
join
(
APISCOUT_PROFILE_DIR
,
file
)
,
)


Perform full snapshot import symmetric to do_export_full

def do_import_full(mc, name, bucket, zpool):
    """Perform full snapshot import, symmetric to do_export_full"""
    do_import_minimal(mc, name, bucket, zpool)

    with tempfile.NamedTemporaryFile() as compressed_snapshot:
        mc.fget_object(bucket, f"{name}/snapshot.sav.gz", compressed_snapshot.name)

        logging.info("Decompressing VM snapshot")
        with open(os.path.join(VOLUME_DIR, "snapshot.sav"), "wb") as snapshot:
            subprocess.run(
                ["zcat", compressed_snapshot.name], stdout=snapshot, check=True
            )

    profiles_prefix = f"{name}/profiles/"
    for object in mc.list_objects(bucket, prefix=profiles_prefix):
        # Strip profiles prefix
        profile_name = object.object_name[len(profiles_prefix) :]
        mc.fget_object(
            bucket, object.object_name, os.path.join(PROFILE_DIR, profile_name)
        )

    apiscout_profile_prefix = f"{name}/apiscout_profile/"
    for object in mc.list_objects(bucket, prefix=apiscout_profile_prefix):
        # Strip apiscout profile prefix
        filename = object.object_name[len(apiscout_profile_prefix) :]
        mc.fget_object(
            bucket, object.object_name, os.path.join(APISCOUT_PROFILE_DIR, filename)
        )

def
do_import_full
(
mc
,
name
,
bucket
,
zpool
)
:
do_import_minimal
(
mc
,
name
,
bucket
,
zpool
)
with
tempfile
.
NamedTemporaryFile
(
)
as
compressed_snapshot
:
mc
.
fget_object
(
bucket
,
f"{name}/snapshot.sav.gz"
,
compressed_snapshot
.
name
)
logging
.
info
(
"Decompressing VM snapshot"
)
with
open
(
os
.
path
.
join
(
VOLUME_DIR
,
"snapshot.sav"
)
,
"wb"
)
as
snapshot
:
subprocess
.
run
(
[
"zcat"
,
compressed_snapshot
.
name
]
,
stdout
=
snapshot
,
check
=
True
)
profiles_prefix
=
f"{name}/profiles/"
for
object
in
mc
.
list_objects
(
bucket
,
prefix
=
profiles_prefix
)
:
# Strip profiles prefix
profile_name
=
object
.
object_name
[
len
(
profiles_prefix
)
:
]
mc
.
fget_object
(
bucket
,
object
.
object_name
,
os
.
path
.
join
(
PROFILE_DIR
,
profile_name
)
)
apiscout_profile_prefix
=
f"{name}/apiscout_profile/"
for
object
in
mc
.
list_objects
(
bucket
,
prefix
=
apiscout_profile_prefix
)
:
# Strip apiscout profile prefix
filename
=
object
.
object_name
[
len
(
apiscout_profile_prefix
)
:
]
mc
.
fget_object
(
bucket
,
object
.
object_name
,
os
.
path
.
join
(
APISCOUT_PROFILE_DIR
,
filename
)
)


def get_storage_backend(install_info: InstallInfo) -> StorageBackendBase:
    """Return installed storage backend or throw InvalidStorageBackend"""
    if install_info.storage_backend not in REGISTERED_BACKEND_NAMES:
        raise InvalidStorageBackend

    return REGISTERED_BACKENDS[install_info.storage_backend](install_info)

def get_storage_backend ( install_info : InstallInfo ) -> StorageBackendBase : if install_info . storage_backend not in REGISTERED_BACKEND_NAMES : raise InvalidStorageBackend return REGISTERED_BACKENDS [ install_info . storage_backend ] ( install_info )


Create base volume for vm - 0 with given size

def initialize_vm0_volume(self, disk_size: str):
        """Create base volume for vm-0 with given size

        disk_size - string representing volume size with M/G/T suffix, eg. 100G
        """
        raise NotImplementedError

def initialize_vm0_volume ( self , disk_size : str ) : raise NotImplementedError


Saves or snapshots base vm - 0 volume for later use by other VMs

def snapshot_vm0_volume(self):
        """Saves or snapshots base vm-0 volume for later use by other VMs"""
        raise NotImplementedError

def snapshot_vm0_volume ( self ) : raise NotImplementedError


def get_vm_disk_path(self, vm_id: int) -> str:
        """Returns disk path for given VM as defined by XL configuration"""
        raise NotImplementedError

def get_vm_disk_path ( self , vm_id : int ) -> str : raise NotImplementedError


def rollback_vm_storage(self, vm_id: int):
        """Rolls back changes and prepares fresh storage for new run of this VM"""
        raise NotImplementedError

def rollback_vm_storage ( self , vm_id : int ) : raise NotImplementedError


Get UNIX timestamp of when vm - 0 snapshot was last modified

def get_vm0_snapshot_time(self):
        """Get UNIX timestamp of when vm-0 snapshot was last modified"""
        raise NotImplementedError

def get_vm0_snapshot_time ( self ) : raise NotImplementedError


Export vm - 0 disk into a file ( symmetric to import_vm0 )

def export_vm0(self, file):
        """Export vm-0 disk into a file (symmetric to import_vm0)"""
        raise NotImplementedError

def export_vm0 ( self , file ) : raise NotImplementedError


Import vm - 0 disk from a file ( symmetric to export_vm0 )

def import_vm0(self, file):
        """Import vm-0 disk from a file (symmetric to export_vm0)"""
        raise NotImplementedError

def import_vm0 ( self , file ) : raise NotImplementedError


def delete_vm_volume(self, vm_id: int):
        """Delete vm_id disk volume"""
        raise NotImplementedError

def delete_vm_volume ( self , vm_id : int ) : raise NotImplementedError


def check_tools():
        """Verify existence of zfs command utility"""
        try:
            subprocess.check_output("zfs -?", shell=True)
        except subprocess.CalledProcessError:
            raise RuntimeError(
                "Failed to execute zfs command. "
                "Make sure you have ZFS support installed."
            )


def check_tools():
        """Verify existence of qemu-img"""
        try:
            subprocess.check_output("qemu-img --version", shell=True)
        except subprocess.CalledProcessError:
            raise RuntimeError(
                "Failed to determine qemu-img version. "
                "Make sure you have qemu-utils installed."
            )


def check_tools(self):
        """Verify existence of lvm command utility"""
        try:
            subprocess.run(
                ["vgs", self.lvm_volume_group], check=True, stdout=subprocess.DEVNULL
            )
        except subprocess.CalledProcessError:
            raise RuntimeError(
                "Failed to execute vgs command"
                f"Make sure you have LVM support installed with {self.lvm_volume_group} as a volume group"
            )


def initialize_vm0_volume(self, disk_size: str):
        """Create base volume for vm-0 with given size

        disk_size - string representing volume size with M/G/T suffix, eg. 100G
        """
        try:
            logging.info("Deleting existing logical volume and snapshot")
            subprocess.check_output(
                [
                    "lvremove",
                    "-v",
                    "-y",
                    # "--noudevsync",
                    f"{self.lvm_volume_group}/vm-0",
                ],
                stderr=subprocess.STDOUT,
            )
        except subprocess.CalledProcessError as exc:
            if b"Failed to find logical volume" not in exc.output:
                raise RuntimeError(
                    f"Failed to destroy logical volume {self.lvm_volume_group}/vm-0"
                )
        try:
            logging.info("Creating new volume vm-0")
            subprocess.run(
                [
                    "lvcreate",
                    "-y",
                    "-L",
                    disk_size,
                    "-n",
                    "vm-0",
                    self.lvm_volume_group,
                ],
                check=True,
            )
        except subprocess.CalledProcessError:
            raise RuntimeError("Failed to create a new volume using lvcreate.")


def snapshot_vm0_volume(self):
        """Saves or snapshots base vm-0 volume for later use by other VMs"""
        # vm-0 is the original disk being treated as a snapshot
        # vm-0-snap is being created just for the access time of the change in vm snapshot
        subprocess.run(
            ["lvremove", f"{self.lvm_volume_group}/vm-0-snap"],
            stderr=subprocess.DEVNULL,
        )
        try:
            subprocess.check_output(
                [
                    "lvcreate",
                    "-s",
                    "-L",
                    self.snapshot_disksize,
                    "-n",
                    "vm-0-snap",
                    f"{self.lvm_volume_group}/vm-0",
                ],
                stderr=subprocess.STDOUT,
            )
        except subprocess.CalledProcessError as exc:
            logging.debug(exc.output)
            raise RuntimeError("Couldn't create snapshot")


def get_vm_disk_path(self, vm_id: int) -> str:
        """Returns disk path for given VM as defined by XL configuration"""
        return f"phy:/dev/{self.lvm_volume_group}/vm-{vm_id},hda,w"


def rollback_vm_storage(self, vm_id: int):
        """Rolls back changes and prepares fresh storage for new run of this VM"""
        vm_id_vol = os.path.join("/dev", f"{self.lvm_volume_group}", f"vm-{vm_id}")

        if vm_id == 0:
            raise Exception("vm-0 should not be rollbacked")

        logging.info(f"Rolling back changes to vm-{vm_id} disk")
        if os.path.exists(vm_id_vol):
            try:
                subprocess.check_output(
                    [
                        "lvremove",
                        "-v",
                        "-y",
                        # "--noudevsync",
                        f"{self.lvm_volume_group}/vm-{vm_id}",
                    ],
                    stderr=subprocess.STDOUT,
                )
            except subprocess.CalledProcessError as exc:
                logging.debug(exc.output)
                raise RuntimeError(
                    f"Failed to discard previous logical volume {self.lvm_volume_group}/vm-{vm_id}"
                )

        try:
            subprocess.check_output(
                [
                    "lvcreate",
                    "-s",
                    "-L",
                    self.snapshot_disksize,
                    "-n",
                    f"vm-{vm_id}",
                    f"{self.lvm_volume_group}/vm-0",
                ],
                stderr=subprocess.STDOUT,
            )
        except subprocess.CalledProcessError as exc:
            logging.debug(exc.output)
            raise RuntimeError("Couldn't rollback disk")


def get_vm0_snapshot_time(self):
        """Get UNIX timestamp of when vm-0 snapshot was last modified"""

        p = subprocess.run(
            ["lvs", "-o", "lv_name,lv_time", "--reportformat", "json"],
            capture_output=True,
            check=True,
        )

        lvs = json.loads(p.stdout.decode("utf-8"))["report"][0]["lv"]
        target_lvs = list(filter(lambda x: x["lv_name"] == "vm-0-snap", lvs))

        if len(target_lvs) > 1:
            raise RuntimeError("Found multiple lvs named vm-0-snap!")

        if len(target_lvs) == 0:
            raise RuntimeError("Failed to find LV vm-0-snap!")

        dt = datetime.datetime.strptime(
            target_lvs[0]["lv_time"], "%Y-%m-%d %H:%M:%S %z"
        )
        return int(dt.timestamp())

def
get_vm0_snapshot_time
(
self
)
:
p
=
subprocess
.
run
(
[
"lvs"
,
"-o"
,
"lv_name,lv_time"
,
"--reportformat"
,
"json"
]
,
capture_output
=
True
,
check
=
True
,
)
lvs
=
json
.
loads
(
p
.
stdout
.
decode
(
"utf-8"
)
)
[
"report"
]
[
0
]
[
"lv"
]
target_lvs
=
list
(
filter
(
lambda
x
:
x
[
"lv_name"
]
==
"vm-0-snap"
,
lvs
)
)
if
len
(
target_lvs
)
>
1
:
raise
RuntimeError
(
"Found multiple lvs named vm-0-snap!"
)
if
len
(
target_lvs
)
==
0
:
raise
RuntimeError
(
"Failed to find LV vm-0-snap!"
)
dt
=
datetime
.
datetime
.
strptime
(
target_lvs
[
0
]
[
"lv_time"
]
,
"%Y-%m-%d %H:%M:%S %z"
)
return
int
(
dt
.
timestamp
(
)
)


def export_vm0(self, path: str):
        """Export vm-0 disk into a file (symmetric to import_vm0)"""
        # As dd copies empty spaces also
        # Should we use compressions in this? Will it have any issues while importing?
        subprocess.run(
            [
                "dd",
                f"if=/dev/{self.lvm_volume_group}/vm-0",
                f"of={path}",
                "bs=4k",
                "status=progress",
            ],
            check=True,
        )


def import_vm0(self, path: str):
        """Import vm-0 disk from a file (symmetric to export_vm0)"""
        subprocess.run(
            [
                "dd",
                f"of=/dev/{self.lvm_volume_group}/vm-0",
                f"if={path}",
                "bs=4k",
                "status=progress",
            ],
            check=True,
        )

def import_vm0 ( self , path : str ) : subprocess . run ( [ "dd" , f"of=/dev/{self.lvm_volume_group}/vm-0" , f"if={path}" , "bs=4k" , "status=progress" , ] , check = True , )


Returns true when install . json is present

def is_installed() -> bool:
    """Returns true when install.json is present"""
    return InstallInfo.try_load() is not None

def is_installed ( ) -> bool : return InstallInfo . try_load ( ) is not None


def vba_extract_functions(vba_content_lines):
    """Seperates the input VBA code into functions

    Args:
        vba_content_lines (string[]): VBA code lines without comments, metadata or spaces

    Returns:
        dict[func_name]=func_code: Dictionary of VBA functions found
    """
    vba_func_dict = {}
    inside_function = False
    func_name = ""

    for vba_line in vba_content_lines:
        # I) Handle External Function Declaration.

        # Create dummpy empty function with func_name:
        # mcvWGqJifEVHwB (URLDownloadToFileA)
        # Examples:
        #   Private Declare Function NyKQpQhtmrFfWX Lib "kernel32" Alias "lstrcmpA" (ByVal pCaller As Long,..
        #   - would become: NyKQpQhtmrFfWX (lstrcmpA) (External)
        #   Private Declare PtrSafe Function mcvWGqJifEVHwB Lib "urlmon" Alias "URLDownloadToFileA" (ByVal pfsseerwseer As Long,...
        #   - would become: mcvWGqJifEVHwB (URLDownloadToFileA) (External)
        if " Lib " in vba_line and " Alias " in vba_line and not inside_function:
            if " Function " in vba_line:
                func_type = " Function "
            else:
                func_type = " Sub "

            declared_func_name = vba_line[
                vba_line.find(func_type) + len(func_type) : vba_line.find(" Lib ")
            ]
            external_func_name = vba_line[
                vba_line.find(' Alias "')
                + len(' Alias "') : vba_line.find(
                    '" (', vba_line.find(' Alias "') + len(' Alias "')
                )
            ]
            func_name = (
                declared_func_name + " (" + external_func_name + ")" + " (External)"
            )

            if "libc.dylib" in vba_line:
                func_name += "(Mac)"

            vba_func_dict[func_name] = ""
            continue

        # Create dummy empty function with func_name that do not have Alias:
        # Examples:
        #   Public Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As LongPtr)
        #   - would become: Sleep
        #   Public Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
        #   - would become: Sleep
        if " Lib " in vba_line and not inside_function:
            if " Function " in vba_line:
                func_type = " Function "
            else:
                func_type = " Sub "
            func_name = (
                vba_line[
                    vba_line.find(func_type) + len(func_type) : vba_line.find(" Lib ")
                ]
                + " (External)"
            )

            if "libc.dylib" in vba_line:
                func_name += "(Mac)"

            vba_func_dict[func_name] = ""
            continue

        # II) Handle Regular Function Declaration.
        # Look for function start keywords.
        func_start_pos = max(vba_line.find("Sub "), vba_line.find("Function "))

        # Some macros have the word "Function" as string inside a code line.
        # This should remove FP funtions, by checking the line start.
        legit_declare_line_start = False
        if (
            vba_line.startswith("Sub")
            or vba_line.startswith("Function")
            or vba_line.startswith("Private")
            or vba_line.startswith("Public")
        ):
            legit_declare_line_start = True

        is_func_end = vba_line.startswith("End Sub") or vba_line.startswith(
            "End Function"
        )

        # Check if we've reached the end of a function.
        if is_func_end:
            inside_function = False
            continue

        # Check if we've hit a new function.
        elif legit_declare_line_start and func_start_pos > -1:
            inside_function = True

            # Extract function name from declaration.
            if "Function " in vba_line:
                func_name = vba_line[
                    (func_start_pos + len("Function ")) : vba_line.find("(")
                ]
            elif "Sub " in vba_line:
                func_name = vba_line[
                    (func_start_pos + len("Sub ")) : vba_line.find("(")
                ]
            else:
                logging.error("Error parsing function name")
                sys.exit(1)

        elif inside_function:
            if func_name in vba_func_dict:
                # Append code to to an existing function.
                vba_func_dict[func_name] += LINE_SEP + vba_line
            else:
                # Create a new function name inside the dict
                # & add the first line of code.
                vba_func_dict[func_name] = vba_line

        # We are in a global section code line.
        else:
            pass
    return vba_func_dict


Find and extract the use of VBA Properties in order to obfuscate macros

def vba_extract_properties(vba_content_lines):
    """Find and extract the use of VBA Properties, in order to obfuscate macros

    Args:
        vba_content_lines (string[]): VBA code lines without comments, metadata or spaces

    Returns:
        dict[property_name]=property_code: Dictionary of VBA Properties found
    """
    vba_prop_dict = {}
    inside_property = False
    prop_name = ""

    for vba_line in vba_content_lines:
        # Look for property start keywords.
        prop_start_pos = max(
            vba_line.find("Property Let "), vba_line.find("Property Get ")
        )

        # Look for property end keywords.
        is_prop_end = vba_line.startswith("End Property")

        # Check if we've reached the end of a property.
        if is_prop_end:
            inside_property = False
            continue

        # Check if we've hit a new property.
        elif prop_start_pos > -1:
            inside_property = True

            # Extract property name from declaration.
            if "Property Let " in vba_line or "Property Get " in vba_line:
                prop_name = (
                    vba_line[
                        (prop_start_pos + len("Property Let ")) : vba_line.find("(")
                    ]
                    + " (Property)"
                )

            else:
                logging.error("Error parsing property name")
                sys.exit(1)

        # Check if we are inside a property code.
        elif inside_property:
            if prop_name in vba_prop_dict:
                # Append code to to an existing property.
                vba_prop_dict[prop_name] += LINE_SEP + vba_line
            else:
                # Create a new property name inside the dict
                # & add the first line of code.
                vba_prop_dict[prop_name] = vba_line

        # We are in a global section code line.
        else:
            pass

    return vba_prop_dict