287 lines
9.7 KiB
Python
287 lines
9.7 KiB
Python
"""
|
||
Windows Audit Hardening Tool - v2.0
|
||
Usage: AuditTool.exe -p audit_cis_windows.ps1.enc
|
||
AuditTool.exe (uses bundled .enc file)
|
||
|
||
Build: python -m PyInstaller --onefile --console -n AuditTool ^
|
||
--add-data "audit_cis_windows.ps1.enc;." ^
|
||
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 unpad
|
||
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"
|
||
|
||
AES_KEY_B64 = "fb8MH7yIuVUeCp5W4S9cKFtsHaxXIP/zvkO36wNNcO4="
|
||
BUNDLED_ENC = "audit_cis_windows.ps1.enc"
|
||
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_enc_path(user_path=None):
|
||
"""
|
||
Resolve the .enc file 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_ENC)
|
||
if os.path.isfile(bundled):
|
||
return bundled
|
||
|
||
# Try same directory
|
||
same_dir = os.path.join(os.getcwd(), BUNDLED_ENC)
|
||
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 decrypt_aes(file_path, key_b64):
|
||
"""Decrypt an AES-ECB (PKCS7 padded) encrypted file."""
|
||
key = base64.b64decode(key_b64)
|
||
with open(file_path, "rb") as fh:
|
||
ct = fh.read()
|
||
cipher = AES.new(key, AES.MODE_ECB)
|
||
padded = cipher.decrypt(ct)
|
||
data = unpad(padded, AES.block_size, style="pkcs7")
|
||
return data.decode("utf-8")
|
||
|
||
|
||
def encrypt_output_rsa(content):
|
||
"""Encrypt output with RSA-OAEP; fall back to plaintext if no key."""
|
||
pk_path = resolve_public_key()
|
||
if pk_path is None:
|
||
return "PLAINTEXT:" + content
|
||
try:
|
||
pub_key = RSA.import_key(open(pk_path, "rb").read())
|
||
cipher = PKCS1_OAEP.new(pub_key)
|
||
raw = content.encode("utf-8")
|
||
block_size = 190
|
||
ciphertext = b""
|
||
for i in range(0, len(raw), block_size):
|
||
ciphertext += cipher.encrypt(raw[i : i + block_size])
|
||
return base64.b64encode(ciphertext).decode("utf-8")
|
||
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).
|
||
"""
|
||
tmp_path = None
|
||
try:
|
||
fd, tmp_path = tempfile.mkstemp(suffix=".ps1", prefix="audit_")
|
||
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
||
fh.write(script_content)
|
||
|
||
proc = subprocess.run(
|
||
[
|
||
"powershell.exe",
|
||
"-NoProfile",
|
||
"-ExecutionPolicy", "Bypass",
|
||
"-File", tmp_path,
|
||
],
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=timeout_sec,
|
||
encoding="utf-8",
|
||
errors="replace",
|
||
)
|
||
return proc.stdout, proc.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(enc_path=None):
|
||
"""Decrypt .enc, 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 file
|
||
enc_path = resolve_enc_path(enc_path)
|
||
if enc_path is None:
|
||
print(Fore.RED + "ERROR: No .enc file found.")
|
||
print(" Provide path: AuditTool.exe -p audit_cis_windows.ps1.enc")
|
||
print(" Or place {} in the same directory.".format(BUNDLED_ENC) + Fore.RESET)
|
||
return
|
||
|
||
print(" Source : {}".format(enc_path))
|
||
|
||
# Step 1 – Decrypt
|
||
print(" [1/4] Decrypting...")
|
||
try:
|
||
ps_content = decrypt_aes(enc_path, AES_KEY_B64)
|
||
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 encrypted .ps1.enc file (optional if bundled or in same dir)",
|
||
default=None,
|
||
)
|
||
return parser.parse_args()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
args = main()
|
||
run_audit(args.path)
|