netinvent/windows_tools

View on GitHub
windows_tools/powershell/__init__.py

Summary

Maintainability
A
3 hrs
Test Coverage
#! /usr/bin/env python
#  -*- coding: utf-8 -*-
#
# Copyright 2020 Orsiris de Jong
# This file is part of command_runner module

"""
PowerShellRunner is a class that allows to run powershell scripts / commands without hassle

Versioning semantics:
    Major version: backward compatibility breaking changes
    Minor version: New functionality
    Patch version: Backwards compatible bug fixes

"""

__intname__ = "windows_tools.powershell"
__author__ = "Orsiris de Jong"
__copyright__ = "Copyright (C) 2019-2021 Orsiris de Jong"
__description__ = "PowerShell interpreter wrapper"
__licence__ = "BSD 3 Clause"
__version__ = "0.3.1"
__build__ = "2021021701"

import os

from command_runner import command_runner
import windows_tools.registry


class PowerShellRunner:
    """
    Identify powershell interpreter and allow running scripts / commands with ExecutionPolicy ByPass
    """

    def __init__(self, powershell_interpreter=None):
        self.powershell_interpreter = powershell_interpreter

        if powershell_interpreter is not None and os.path.isfile(
            powershell_interpreter
        ):
            return

        # Try to guess powershell path if no valid path given
        interpreter_executable = "powershell.exe"
        for syspath in ["sysnative", "system32"]:
            try:
                # Let's try native powershell (64 bit) first or else
                # Import-Module may fail when running 32 bit powershell on 64 bit arch
                best_guess = os.path.join(
                    os.environ.get("SYSTEMROOT", "C:"),
                    syspath,
                    "WindowsPowerShell",
                    "v1.0",
                    interpreter_executable,
                )
                if os.path.isfile(best_guess):
                    self.powershell_interpreter = best_guess
                    break
            except KeyError:
                pass
        if self.powershell_interpreter is None:
            try:
                ps_paths = os.path.dirname(os.environ["PSModulePath"]).split(";")
                for ps_path in ps_paths:
                    if ps_path.endswith("Modules"):
                        ps_path = ps_path.strip("Modules")
                    possible_ps_path = os.path.join(ps_path, interpreter_executable)
                    if os.path.isfile(possible_ps_path):
                        self.powershell_interpreter = possible_ps_path
                        break
            except KeyError:
                pass

        if self.powershell_interpreter is None:
            raise OSError("Could not find any valid powershell interpreter")

    def get_version(self):
        """
        Get major / minor version as tuple

        """
        if self.powershell_interpreter is None:
            return 0, 0

        try:
            exit_code, output = self.run_command("$PSVersionTable.PSVersion.ToString()")
            if exit_code != 0:
                # If the above method does not work, let's try registry method
                try:
                    # Yes, registry path ...\PowerShell\3\... may contain REG_SZ PowerShelVersion "4.0" or else
                    output = windows_tools.registry.get_value(
                        hive=windows_tools.registry.HKEY_LOCAL_MACHINE,
                        key=r"SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine",
                        value="PowerShellVersion",
                    )
                except FileNotFoundError:
                    output = "-1.-1"
            try:
                # output = major_version.minor_version.build.revision for newer powershells
                major_version, minor_version, _, _ = output.split(".")
            except (ValueError, TypeError):
                # output = major_version.minor_version for some powershells (v3.0)
                major_version, minor_version = output.split(".")
            return int(major_version), int(minor_version)
        except (ValueError, TypeError):
            return -1, 0

    def run_command(self, command, **kwargs):
        """
        Accepts subprocess.check_output arguments
        Accepts command_runner arguments like timeout, encoding and valid_exit_codes

        """
        if self.powershell_interpreter is None:
            return False

        timeout = kwargs.pop("timeout", 300)
        valid_exit_codes = kwargs.pop("valid_exit_codes", [0])

        # Do not add -NoProfile so we don't end up in a path we're not supposed to
        command = self.powershell_interpreter + " -NonInteractive -NoLogo %s" % command
        exit_code, output = command_runner(
            command,
            timeout=timeout,
            valid_exit_codes=valid_exit_codes,
            encoding="unicode_escape",
            **kwargs
        )
        return exit_code, output

    def run_script(self, script, *args, **kwargs):
        """
        Accepts subprocess.check_output arguments
        """

        if self.powershell_interpreter is None:
            return False

        timeout = kwargs.pop("timeout", 300)
        valid_exit_codes = kwargs.pop("valid_exit_codes", [0])

        # Welcome in Powershell hell where running a script with -Command argument returns exit
        # codes 0 or 1 whereas as running with -File argument returns your script exit code
        command = (
            self.powershell_interpreter
            + " -executionPolicy Bypass -NonInteractive -NoLogo -NoProfile -File "
            + script
            + (" " if len(args) > 0 else " ")
            + " ".join('"' + arg + '"' for arg in args)
        )

        exit_code, output = command_runner(
            command,
            timeout=timeout,
            valid_exit_codes=valid_exit_codes,
            encoding="unicode_escape",
            **kwargs
        )
        return exit_code, output