Files
audit-web/audit_run_script/windows_audit.py
T
2026-08-26 14:11:37 +07:00

287 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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)