""" Windows Audit Hardening Tool - v2.0 Usage: AuditTool.exe -p audit_cis_windows.ps1 AuditTool.exe (uses bundled .ps1 file) Build: python -m PyInstaller --onefile --console -n AuditTool ^ --add-data "audit_cis_windows.ps1;." ^ windows_audit.py Environment: Python 3.8+ (3.8 for Windows Server 2012 compatibility) """ import argparse import base64 import os import socket import subprocess import sys import tempfile import traceback from Crypto.Cipher import PKCS1_OAEP, AES from Crypto.PublicKey import RSA from Crypto.Util.Padding import pad from colorama import Fore, init as colorama_init from unidecode import unidecode # ---------------------------------------------------------- # Globals # ---------------------------------------------------------- colorama_init(strip=False, autoreset=True) HOSTNAME = socket.gethostname() try: IP_ADDR = socket.gethostbyname(HOSTNAME) except Exception: IP_ADDR = "127.0.0.1" BUNDLED_SCRIPT = "audit_cis_windows.ps1" PUBLIC_KEY_FILE = "public_key.pem" BANNER = r""" _ _ _____ _____ _______ _ _ _____ _____ ______ _ _ _____ _ _ _____ /\ | | | | __ \_ _|__ __| | | | | /\ | __ \| __ \| ____| \ | |_ _| \ | |/ ____| / \ | | | | | | || | | | ______ | |__| | / \ | |__) | | | | |__ | \| | | | | \| | | __ / /\ \| | | | | | || | | | |______| | __ | / /\ \ | _ /| | | | __| | . ` | | | | . ` | | |_ | / ____ \ |__| | |__| || |_ | | | | | |/ ____ \| | \ \| |__| | |____| |\ |_| |_| |\ | |__| | /_/ \_\____/|_____/_____| |_| |_| |_/_/ \_\_| \_\_____/|______|_| \_|_____|_| \_|\_____| """ # ---------------------------------------------------------- # Path resolution (supports PyInstaller bundle) # ---------------------------------------------------------- def resource_path(relative_path): """Get absolute path to resource, works for dev and PyInstaller.""" if getattr(sys, "frozen", False): base = sys._MEIPASS else: base = os.path.dirname(os.path.abspath(__file__)) return os.path.join(base, relative_path) def resolve_script_path(user_path=None): """ Resolve the .ps1 script path. Priority: user-supplied arg > bundled file > same-dir file. Returns absolute path or None. """ if user_path: if os.path.isfile(user_path): return os.path.abspath(user_path) print(Fore.YELLOW + "[WARNING] Provided path not found: {}".format(user_path) + Fore.RESET) # Try PyInstaller bundled location bundled = resource_path(BUNDLED_SCRIPT) if os.path.isfile(bundled): return bundled # Try same directory same_dir = os.path.join(os.getcwd(), BUNDLED_SCRIPT) if os.path.isfile(same_dir): return same_dir return None def resolve_public_key(): """Find public_key.pem - bundled, same dir, or current dir.""" for loc in [ resource_path(PUBLIC_KEY_FILE), os.path.join(os.getcwd(), PUBLIC_KEY_FILE), ]: if os.path.isfile(loc): return loc return None # ---------------------------------------------------------- # Crypto helpers # ---------------------------------------------------------- def encrypt_output_rsa(content): """Encrypt output with Hybrid AES-256-CBC + RSA (HYBRID_V1 format).""" pk_path = resolve_public_key() if pk_path is None: return "PLAINTEXT:" + content try: raw = content.encode("utf-8") aes_key = os.urandom(32) iv = os.urandom(16) padded = pad(raw, AES.block_size, style="pkcs7") cipher_aes = AES.new(aes_key, AES.MODE_CBC, iv) ciphertext = cipher_aes.encrypt(padded) pub_key = RSA.import_key(open(pk_path, "rb").read()) cipher_rsa = PKCS1_OAEP.new(pub_key) enc_key = cipher_rsa.encrypt(aes_key) b64_enc_key = base64.b64encode(enc_key).decode("utf-8") b64_iv = base64.b64encode(iv).decode("utf-8") b64_ct = base64.b64encode(ciphertext).decode("utf-8") return f"HYBRID_V1:{b64_enc_key}:{b64_iv}:{b64_ct}" except Exception as e: print(Fore.YELLOW + "[WARNING] RSA encrypt failed: {}".format(e) + Fore.RESET) return "PLAINTEXT:" + content # ---------------------------------------------------------- # PowerShell runner # ---------------------------------------------------------- def run_powershell_script(script_content, timeout_sec=600): """ Write script to temp .ps1 file, execute via 'powershell -File', return (stdout, stderr, returncode). Handles PowerShell's UTF-16LE output encoding correctly. """ tmp_path = None try: fd, tmp_path = tempfile.mkstemp(suffix=".ps1", prefix="audit_") with os.fdopen(fd, "w", encoding="utf-8-sig") as fh: fh.write(script_content) proc = subprocess.run( [ "powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", tmp_path, ], capture_output=True, timeout=timeout_sec, ) # PowerShell outputs UTF-16LE by default stdout = proc.stdout.decode("utf-16-le", errors="replace") if proc.stdout else "" stderr = proc.stderr.decode("utf-16-le", errors="replace") if proc.stderr else "" return stdout, stderr, proc.returncode except subprocess.TimeoutExpired: return "", "PowerShell execution timed out ({}s)".format(timeout_sec), -1 except Exception: return "", traceback.format_exc(), -1 finally: if tmp_path and os.path.exists(tmp_path): try: os.remove(tmp_path) except Exception: pass # ---------------------------------------------------------- # Output parsing # ---------------------------------------------------------- def extract_audit_info(output): """Extract hostname + timestamp from audit output lines.""" hostname = HOSTNAME timestamp = "unknown" for line in output.splitlines(): if line.startswith("Hostname:"): val = line.replace("Hostname:", "").strip() hostname = val if val else HOSTNAME elif line.startswith("Time:"): val = line.replace("Time:", "").strip() timestamp = val.replace("-", "_").replace(":", "_").replace(" ", "-") if val else "unknown" return hostname, timestamp def count_pass_fail(output): """Count PASSED/FAILED from JSON lines.""" passed = failed = 0 for line in output.splitlines(): line = line.strip() if line.startswith("{") and line.endswith("}"): if '"PASSED"' in line or "'PASSED'" in line: passed += 1 elif '"FAILED"' in line or "'FAILED'" in line: failed += 1 return passed, failed # ---------------------------------------------------------- # Main audit flow # ---------------------------------------------------------- def run_audit(script_path=None): """Read .ps1, run PowerShell audit, encrypt+save results.""" print(Fore.BLUE + BANNER + Fore.RESET) print(Fore.RED + "Running Audit Hardening v2.0" + Fore.RESET) print(" Hostname : {}".format(HOSTNAME)) print(" IP : {}".format(IP_ADDR)) print("-" * 70) # Step 0 – Resolve input script script_path = resolve_script_path(script_path) if script_path is None: print(Fore.RED + "ERROR: No .ps1 file found.") print(" Provide path: AuditTool.exe -p audit_cis_windows.ps1") print(" Or place {} in the same directory.".format(BUNDLED_SCRIPT) + Fore.RESET) return print(" Source : {}".format(script_path)) # Step 1 – Read PowerShell script print(" [1/4] Reading script...") try: with open(script_path, "r", encoding="utf-8") as fh: ps_content = fh.read() except Exception as e: print(Fore.RED + " FAILED: {}".format(e) + Fore.RESET) return print(" [1/4] OK - {} bytes".format(len(ps_content))) # Step 2 – Execute PowerShell print(" [2/4] Running PowerShell...") stdout, stderr, rc = run_powershell_script(ps_content) if rc != 0: print(Fore.YELLOW + " [2/4] PowerShell rc={}: {}".format(rc, (stderr or "")[:200]) + Fore.RESET) else: print(" [2/4] OK - {} output lines".format(len(stdout.splitlines()))) # Step 3 – Combine full_output = stdout if stderr: full_output += "\n[STDERR]\n" + stderr if not full_output.strip(): print(Fore.RED + "ERROR: No output from PowerShell" + Fore.RESET) return passed, failed = count_pass_fail(full_output) print(" [3/4] Audit: {} PASSED / {} FAILED".format(passed, failed)) # Step 4 – Encrypt & save hostname_out, timestamp = extract_audit_info(stdout) out_filename = "{}_{}.txt.enc".format(hostname_out, timestamp) print(" [4/4] Encrypting -> {}".format(out_filename)) encrypted = encrypt_output_rsa(full_output) with open(out_filename, "w", encoding="utf-8") as fh: fh.write(encrypted) if os.path.isfile(out_filename): print(Fore.GREEN + "=" * 70 + Fore.RESET) print(Fore.GREEN + " SUCCESS: {} | {} PASS / {} FAIL".format( out_filename, passed, failed) + Fore.RESET) print(Fore.GREEN + "=" * 70 + Fore.RESET) else: print(Fore.RED + " FAILED: Could not write output file" + Fore.RESET) # ---------------------------------------------------------- # CLI # ---------------------------------------------------------- def main(): parser = argparse.ArgumentParser(description="Audit Hardening v2.0") parser.add_argument( "-p", "--path", help="Path to PowerShell .ps1 script (optional if bundled or in same dir)", default=None, ) return parser.parse_args() if __name__ == "__main__": args = main() run_audit(args.path)