"""
Summary
-----------------
This module inspects Windows processes to determine whether their executable
files are digitally signed and trustworthy. It gathers the process path,
checks which services (if any) the process hosts, and verifies the file's
signature using PowerShell's Get-AuthenticodeSignature. Everything is read-only
and designed to work without admin rights. The main function, `authentic(pid)`,
returns a structured verdict describing the process's authenticity, signature
details, and any errors encountered along the way.

Detailed Notes 
------------------------------------------
Windows does not provide a single, reliable API for retrieving a process's
executable path when running without elevated permissions. This module uses a
layered strategy to extract the path safely:

1. **WMI Query (Win32_Process.ExecutablePath)**  
   The first attempt uses WMI, which often works even for non-admin users.
   However, WMI can return `None` for protected or system processes, so we
   treat this as a best-effort method rather than a guarantee.

2. **Service Control Manager (SCM) Fallback**  
   If WMI fails but the process hosts one or more Windows services, we query
   the Service Control Manager for the service's configured binary path.  
   This works because SCM exposes service configuration even to limited users,
   and many system processes are service-backed.

3. **Command-Line Cleaning and Normalization**  
   Windows service paths and WMI paths can contain quotes, arguments, or
   mixed slashes. The helper `clean_executable_path()` uses the Win32 API
   `CommandLineToArgvW` to safely extract the true executable portion of a
   command line. This avoids brittle string-splitting and handles edge cases
   like embedded spaces or flags.

4. **Signature Retrieval via PowerShell**  
   Once we have a clean path, we call PowerShell's
   `Get-AuthenticodeSignature` to retrieve the file's digital signature.
   PowerShell is used because it provides a stable, high-level wrapper over
   Windows trust APIs and works without admin rights. Results are cached in
   memory to avoid repeated PowerShell launches.

5. **Trust Interpretation**  
   The signature metadata is normalized into a simple dictionary containing:
   - whether the signature is valid  
   - the signer's subject and issuer  
   - a trust code and message  
   - a metadata status (e.g., missing or malformed)

6. **Final Verdict**  
   The `authentic(pid)` function combines all of this into a single result:
   the cleaned path, hosted services, signature details, and an authenticity
   label (“authentic”, “tampered”, or “inaccessible”).  
   No system settings are changed—this module only reads information.

This layered approach allows the agent to inspect most processes on a Windows
machine without requiring elevated privileges, while still providing clear,
structured diagnostics for debugging and security analysis.
"""

import ctypes
import ctypes.wintypes as wt
import win32service
import wmi
import ntpath
import subprocess
import json
import shlex
from typing import Any


PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
PROCESS_VM_READ = 0x0010

CERT_QUERY_OBJECT_FILE = 1
CERT_QUERY_OBJECT_BLOB = 2
CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED = 1 << 10
CERT_QUERY_FORMAT_FLAG_BINARY = 1 << 1
CERT_NAME_SIMPLE_DISPLAY_TYPE = 4

WTD_UI_NONE = 2
WTD_REVOKE_NONE = 0
WTD_CHOICE_FILE = 1
WTD_STATEACTION_VERIFY = 0x00000001
WTD_STATEACTION_CLOSE = 0x00000002


class GUID(ctypes.Structure):
    _fields_ = [
        ("Data1", wt.DWORD),
        ("Data2", wt.WORD),
        ("Data3", wt.WORD),
        ("Data4", ctypes.c_ubyte * 8),
    ]

    def __init__(self, guid_string: str):
        super().__init__()
        import uuid

        u = uuid.UUID(guid_string)
        self.Data1 = u.time_low
        self.Data2 = u.time_mid
        self.Data3 = u.time_hi_version
        self.Data4 = (ctypes.c_ubyte * 8).from_buffer_copy(u.bytes[8:])


WINTRUST_ACTION_GENERIC_VERIFY_V2 = GUID("{00AAC56B-CD44-11d0-8CC2-00C04FC295EE}")


class WINTRUST_FILE_INFO(ctypes.Structure):
    _fields_ = [
        ("cbStruct", wt.DWORD),
        ("pcwszFilePath", wt.LPCWSTR),
        ("hFile", wt.HANDLE),
        ("pgKnownSubject", ctypes.c_void_p),
    ]


class WINTRUST_DATA(ctypes.Structure):
    _fields_ = [
        ("cbStruct", wt.DWORD),
        ("pPolicyCallbackData", ctypes.c_void_p),
        ("pSIPClientData", ctypes.c_void_p),
        ("dwUIChoice", wt.DWORD),
        ("fdwRevocationChecks", wt.DWORD),
        ("dwUnionChoice", wt.DWORD),
        ("pFile", ctypes.POINTER(WINTRUST_FILE_INFO)),
        ("dwStateAction", wt.DWORD),
        ("hWVTStateData", wt.HANDLE),
        ("pwszURLReference", wt.LPCWSTR),
        ("dwProvFlags", wt.DWORD),
        ("dwUIContext", wt.DWORD),
    ]


CryptQueryObject = ctypes.windll.crypt32.CryptQueryObject
CertEnumCertificatesInStore = ctypes.windll.crypt32.CertEnumCertificatesInStore
CertFreeCertificateContext = ctypes.windll.crypt32.CertFreeCertificateContext
CertCloseStore = ctypes.windll.crypt32.CertCloseStore
CertGetNameStringW = ctypes.windll.crypt32.CertGetNameStringW


def debug_print(label, value):
    print(f"[DEBUG] {label}: {value!r}")


CommandLineToArgvW = ctypes.windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [wt.LPCWSTR, ctypes.POINTER(ctypes.c_int)]
CommandLineToArgvW.restype = ctypes.POINTER(wt.LPWSTR)


def clean_executable_path(raw: str | None) -> str | None:
    if not raw:
        return None

    s = raw.strip()

    # Heuristic: if it's a pure path, don't parse as a command line
    is_quoted = s.startswith('"') and s.endswith('"')
    looks_like_cmdline = any(tok in s for tok in (" --", " /", " -", "="))

    if not is_quoted and not looks_like_cmdline:
        # Treat as pure path
        exe = s.strip('"').strip("'")
        return ntpath.normpath(exe.replace("/", "\\"))

    # Otherwise treat as command line
    argc = ctypes.c_int()
    argv = CommandLineToArgvW(s, ctypes.byref(argc))
    if not argv or argc.value == 0:
        return None

    exe = argv[0]
    ctypes.windll.kernel32.LocalFree(argv)

    return ntpath.normpath(exe.replace("/", "\\"))


def get_services_for_pid(pid: int) -> list[str]:
    try:
        c = wmi.WMI()
        return [s.Name for s in c.Win32_Service(ProcessId=pid)]
    except:
        return []


def get_path_via_wmi(pid: int):
    try:
        c = wmi.WMI()
        procs = c.Win32_Process(ProcessId=pid)
        if procs:
            return procs[0].ExecutablePath
    except:
        pass
    return None


def get_path_via_scm(service_name: str):
    try:
        scm = win32service.OpenSCManager(None, None, win32service.SC_MANAGER_CONNECT)
        svc = win32service.OpenService(
            scm, service_name, win32service.SERVICE_QUERY_CONFIG
        )
        config = win32service.QueryServiceConfig(svc)
        return config[3]
    except:
        return None


# Simple in-memory cache
_signature_cache = {}


def get_signature_via_powershell(path: str) -> dict:
    """
    Returns signature metadata using PowerShell's Get-AuthenticodeSignature.
    Results are cached per-path for performance and stability.
    """

    # Check cache first
    if path in _signature_cache:
        return _signature_cache[path]

    ps_script = f"""
    $sig = Get-AuthenticodeSignature -FilePath {shlex.quote(path)};
    $obj = [PSCustomObject]@{{
        Status        = $sig.Status
        StatusMessage = $sig.StatusMessage
        Subject       = $sig.SignerCertificate.Subject
        Issuer        = $sig.SignerCertificate.Issuer
    }};
    $obj | ConvertTo-Json -Compress
    """

    try:
        output = subprocess.check_output(
            ["pwsh", "-NoProfile", "-Command", ps_script],
            stderr=subprocess.STDOUT,
            text=True,
        )
    except subprocess.CalledProcessError as e:
        result = {
            "status": "error",
            "error": e.output.strip() if e.output else str(e),
            "subject": None,
            "issuer": None,
            "source": "powershell",
        }
        _signature_cache[path] = result
        return result
    except Exception as e:
        result = {
            "status": "error",
            "error": str(e),
            "subject": None,
            "issuer": None,
            "source": "powershell",
        }
        _signature_cache[path] = result
        return result

    # Parse JSON
    try:
        data = json.loads(output)
    except Exception as e:
        result = {
            "status": "error",
            "error": f"JSON parse error: {e}",
            "subject": None,
            "issuer": None,
            "source": "powershell",
        }
        _signature_cache[path] = result
        return result

    # Normalize
    result = {
        "status": data.get("Status"),
        "status_message": data.get("StatusMessage"),
        "subject": data.get("Subject"),
        "issuer": data.get("Issuer"),
        "source": "powershell",
    }

    # Store in cache
    _signature_cache[path] = result
    return result


def get_signature(path: str) -> dict:
    ps = get_signature_via_powershell(path)
    # Map PowerShell status to boolean
    print("PS Signature Status:", ps)
    valid = ps.get("status") == 0

    return {
        "valid": valid,
        "trust_code": ps.get("status"),
        "trust_message": ps.get("status_message"),
        "subject": ps.get("subject"),
        "issuer": ps.get("issuer"),
        "metadata_status": "ok" if ps.get("subject") else "no_metadata",
        "metadata_error": None,
        "source": "powershell",
    }


def authentic(pid: int) -> dict:
    result = {
        "pid": pid,
        "path": None,
        "hosted_services": get_services_for_pid(pid),
        "authenticity": "inaccessible",
        "reason": "",
        "signature": None,
        "memory_accessible": False,
        "errors": [],
    }

    # Path
    path = get_path_via_wmi(pid)
    if not path and result["hosted_services"]:
        path = get_path_via_scm(result["hosted_services"][0])

    clean_path = clean_executable_path(path)
    result["path"] = clean_path

    if not clean_path:
        result["reason"] = "Executable path could not be parsed"
        return result

    # Signature
    sig = get_signature(clean_path)
    result["signature"] = sig

    if sig["valid"]:
        result["authenticity"] = "authentic"
        result["reason"] = "Signature validated from disk"
    else:
        result["authenticity"] = "tampered"
        result["reason"] = "Signature invalid or missing"

    return result


def authentics(pids: list[int]) -> list[dict[str, Any]]:
    return [authentic(pid) for pid in pids]


if __name__ == "__main__":
    print(authentic(2088))
    # print(authentic(2572))
    # print(authentics([2572, 34524, 13236, 8344]))
