# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

"""Gather miscellaneous information about the agent's operating system."""

import os
import re
import time
import logging
import sys

if os.name == "nt":
    from win32com.client import GetObject
    import pythoncom

if __name__ == "__main__":
    try:
        import sigma
    except ImportError:
        sys.path.append(os.path.join(os.pardir, os.pardir))
    del sys

import platform
import string

from sigma.util import ostypes, config, timeskew

options = config.Options()

logger = logging.getLogger("gather.gather_osinfo")


def normalize_arch(ostype, machine):
    """Normalize a CPU machine string to the canonical arch token used for
    per-architecture agent self-upgrade matching. Tokens are OS-appropriate so
    they match the build/manifest labels: Windows -> x64/x86/arm64; Linux and
    other Unix -> x86_64/aarch64/arm; macOS -> x86_64/arm64."""
    s = (ostype or "").lower()
    m = (machine or "").lower().strip()
    if "windows" in s or "microsoft" in s:
        if m in ("amd64", "x86_64", "x64"):
            return "x64"
        if m in ("arm64", "aarch64"):
            return "arm64"
        if m in ("x86", "i386", "i486", "i586", "i686"):
            return "x86"
        return m
    if "darwin" in s or "mac" in s:
        if m in ("x86_64", "amd64"):
            return "x86_64"
        if m in ("arm64", "aarch64"):
            return "arm64"
        return m
    # linux and other unix
    if m in ("x86_64", "amd64"):
        return "x86_64"
    if m in ("aarch64", "arm64"):
        return "aarch64"
    if m in ("i386", "i486", "i586", "i686"):
        return "x86"
    return m


translate_vendor = {"redhat": "Red Hat",
                    "centos": "CentOS",
                    "debian": "Debian",
                    "canonical": "Canonical",
                    "Solaris": "Sun Microsystems",
                    "SunOS": "Sun Microsystems",
                    "Windows": "Microsoft",
                    "HP-UX": "Hewlett-Packard",
                    "Microsoft Windows": "Microsoft",
                    "Microsoft": "Microsoft",
                    "Darwin": "Apple",
                    "AIX": "IBM",
                   }

def translate(table, key):
    return table.get(key) or key
 
def getpwmode():
    if not options.poliwall_enforce:
        return "disabled"
    pwstatefile = options.datapath("pwstate.txt")
    try:
        f = open(pwstatefile)
    except IOError as err:
        return "unknown"
    kernel = False
    try:
        for line in f:
            if line.startswith("kernel="):
                kernel = line[7:].strip().lower() == "true"
    finally:
        f.close()
    if kernel:
        return "enabled"
    else:
        return "unavailable"

# Convert 10.x to Mac Code name. 
# TODO: Consider reading the codename from the OS X Terms of Service
def mac_name(version=""):
    versions = {
            '10.0':  'Cheetah',
            '10.1':  'Puma',
            '10.2':  'Jaguar',
            '10.3':  'Panther',
            '10.4':  'Tiger',
            '10.5':  'Leopard',
            '10.6':  'Snow Leopard',
            '10.7':  'Lion',
            '10.8':  'Mountain Lion',
            '10.9':  'Mavericks',
            '10.10': 'Yosemite',
            '10.11': 'El Capitan',
            '10.12': 'Sierra',
            '10.13': 'High Sierra',
            '10.14': 'Mojave'
            }
    return versions.get(version) or version

def sw_vers(arg=""):
    pipe = os.popen("/usr/bin/sw_vers %s" % arg, "r")
    try:
        return pipe.read().strip()
    finally:
        pipe.close()

def linux_dist():
    """Return (distname, version, codename) for the running Linux distribution.

    Replaces platform.dist(), which was deprecated in Python 3.5 and removed in
    Python 3.8; this agent now runs on Python 3.12. Values are derived from
    /etc/os-release via platform.freedesktop_os_release() (Python 3.10+). The
    caller overrides these from /etc/redhat-release or /etc/os-release below, so
    a best-effort read with a safe empty-tuple fallback preserves prior behavior
    on hosts lacking those files or running an older interpreter.
    """
    try:
        osr = platform.freedesktop_os_release()
        return (osr.get("ID", ""),
                osr.get("VERSION_ID", ""),
                osr.get("VERSION_CODENAME", ""))
    except (OSError, AttributeError):
        return ("", "", "")

def Gather(collector):
    type = platform.system()
    if type in [ "Microsoft Windows", "Microsoft" ]:
        type = "Windows"

    system, node, release, version, machine, processor = platform.uname()
    system, release, version = platform.system_alias(system, release, version)
    kernelversion = ""
    majorversion = ""
    fullversion = release
    vendor = system
    if system == 'Linux':
        dist = linux_dist()
        fullversion = dist[1]
        vendor = dist[0]
        # redhat-release is used for both RHEL and CentOS
        if os.path.isfile('/etc/redhat-release'):
            try:
                releasefile = open('/etc/redhat-release');
                try:
                    releasetext = releasefile.read()
                finally:
                    releasefile.close()
                # check which os version of redhat-release we have
                # combine and remove the non-version information in the file
                if releasetext.split(' ')[0] == 'Red':
                    vendor = 'redhat'
                    fullversion = releasetext[0:30]
                    # majorversion = fullversion.split('.')[0]
                elif releasetext.split(' ')[0] == 'CentOS':
                    vendor = 'centos'
                    fullversion = releasetext[0:30]
                    # majorversion = fullversion.split('.')[0]
                else:
                    logger.warning("Malformed text from /etc/redhat-release: %r", releasetext)
            except:
                logger.warning("Malformed text from /etc/redhat-release: %r", releasefile)
        # os-release is used for Debian and Ubuntu
        elif os.path.isfile('/etc/os-release'):
            try:
                f = open('/etc/os-release')
                debianlines = f.readlines()
                debianlines = [line.strip().replace('"', '').split('=') for line in debianlines]
                for key, value in debianlines:
                    if key == "PRETTY_NAME":
                        fullversion = value[0:30]
                    elif key == "NAME":
                        majorversion = value
                    elif key == "VERSION_ID":
                        majorversion = majorversion + ' ' + value
                    elif key == "ID" and value == "debian":
                        vendor = "debian"
                    elif key == "ID" and value == "ubuntu":
                        vendor = "canonical"
            except:
                logger.warning("Malformed text from /etc/redhat-release: %r", f)

        try:
            pipe = open("/proc/version", "r")
            try:
                line = pipe.read()
            finally:
                pipe.close()
            split = line.split()
            if len(split) >= 3 and split[0] == "Linux" and split[1] == "version":
                kernelversion = split[2]
            else:
                logger.warning("Malformed text from /proc/version: %r", line)
        except:
            typ, val = sys.exc_info()[:2]
            logger.warning("Failed to open /proc/version: %s: %s", typ, val)
    elif system == "Solaris":
        try:
            pipe = os.popen("/usr/bin/uname -a")
            try:
                line = pipe.read()
            finally:
                pipe.close()
            split = line.split()
            if len(split) >= 4:
                kernelversion = split[3]
            else:
                logger.warning("Malformed text from uname: %r", line)
        except:
            typ, val = sys.exc_info()[:2]
            logger.warning("Failed to run uname: %s: %s", typ, val)
    elif system == "Darwin":
        kernelversion = release
        mac_version = sw_vers("-productVersion").split('.')
        type = sw_vers("-productName")
        majorversion = "Mac OS X " + mac_name('.'.join(mac_version[:2]))
        fullversion = "Mac OS X " + '.'.join(mac_version) + " " + sw_vers("-buildVersion")
    elif system == "AIX":
        fullversion = version + "." + release

        try:
            oslevel = os.popen('oslevel -s')
            try:
                output = oslevel.read()
            finally:
                oslevel.close()
            version_information = [ int(digit) for digit in output.strip().split('-')]
            
            fullversion = "AIX " + version_information[0][0] + '.' + version_information[0][1]
            fullversion = fullversion + " Technology Level " + version_information[1]
            fullversion = fullversion +  " Service Pack " + version_information[2]
        except:
            logger.warning('oslevel -s not functioning properly %s', output)

    elif system == "Windows":
        kernelversion = version

    # Only modify majorversion if it hasn't already been defined.
    if len(majorversion) == 0:
        dotpos = fullversion.find(".")
        if dotpos < 0:
            majorversion = fullversion
        else:
            majorversion = fullversion[:dotpos]
    skewer = timeskew.TimeSkew(options)
    clock_skew = -int(skewer.get_skew()) # positive if agent is ahead of server
    lastboottime = -1 
    ostype = ostypes.guess_ostype()
    if vendor.lower() == "centos":
        ostype = vendor.lower();
    # add server or workstation in major and full versions
    if system in [ "Windows", "Microsoft" ]:
        # win2003s already returns 2003Server in full and majorversions
        if ostype == "win2ks":
            fullversion = fullversion + " Server"
            majorversion = fullversion
        elif ostype == "win2kws":
            fullversion = fullversion + " Workstation"
            majorversion = fullversion
        else:
            type = "Windows"
            vendor = system
            import sys
            major, minor, build, platForm, servicePack = sys.getwindowsversion()
            kernelversion = version
            if major >= 5:
                import winreg as _winreg
                isServer = False
                fullversion = ""
                edition = ""
                servicePack = ""
                currentMajorVersion = -1
                currentMinorVersion = 'x'
                currentBuildNumber = 'x'
                try:
                   rootkey = _winreg.OpenKey(getattr(_winreg, "HKEY_LOCAL_MACHINE"),
                       r"Software\Microsoft\Windows NT\CurrentVersion")
                   try:
                       nsubkeys, nvalues, mtime = _winreg.QueryInfoKey(rootkey)
                       for i in range(nvalues):
                           nm, val, typ = _winreg.EnumValue(rootkey, i)
                           if nm.lower() == "productname":
                               fullversion = val
                               if val.lower().find("server") >= 0:
                                  isServer = True
                           elif nm.lower() == "editionid":
                               edition = val
                           elif nm.lower() == "csdversion":
                               servicePack = val
                               if len(servicePack) == 0:
                                   servicePack = "No Attribute"
                           elif nm.lower() == "currentmajorversionnumber":
                               currentMajorVersion = val
                           elif nm.lower() == "currentminorversionnumber":
                               currentMinorVersion = val
                           elif nm.lower() == "currentbuildnumber":
                               currentBuildNumber = val
                           elif nm.lower() == "currentversion":
                               if val == "6.1":
                                   major = 6
                                   minor = 1
                               elif val == "6.2":
                                   major = 6
                                   minor = 2
                               elif val == "6.3":
                                   major = 6
                                   minor = 3
                   finally:
                       _winreg.CloseKey(rootkey)
                except OSError as err:
                   pass

                if currentMinorVersion != 'x':
                    minor = currentMinorVersion

                tmpMajorVersion = 'Unknown'
                if currentMajorVersion >= 10:
                    kernelversion = str(currentMajorVersion) + '.' + str(currentMinorVersion) + '.' + currentBuildNumber
                    if isServer:
                        if "2019" in fullversion:
                            tmpMajorVersion = "Server 2019"
                        else:
                            tmpMajorVersion = 'Server 2016'
                    else:
                        # CurrentBuildNumber is a REG_SZ string (e.g. "22631"); coerce
                        # to int before the numeric Win11 threshold test. Python 2
                        # allowed str >= int, Python 3 raises TypeError — which aborted
                        # the whole osinfo gather on Win11, so the host's OS never
                        # reached the server.
                        try:
                            buildNum = int(currentBuildNumber)
                        except (TypeError, ValueError):
                            buildNum = 0
                        if buildNum >= 22000:
                            tmpMajorVersion = 'Windows 11'
                            fullversion = tmpMajorVersion
                            if edition.lower().startswith("pro"):
                                fullversion = fullversion + " Pro"
                            elif edition.lower().startswith("ent"):
                                fullversion = fullversion + " Enterprise"
                            elif edition.lower().startswith("home"):
                                fullversion = fullversion + " Home"
                        else:
                            tmpMajorVersion = 'Windows 10'
                elif major == 6:
                    if minor == 0:
                        if isServer:
                            tmpMajorVersion = 'Server 2008'
                        else:
                            tmpMajorVersion = 'Windows Vista'
                    elif minor == 1:
                        if isServer:
                            tmpMajorVersion = 'Server 2008 R2'
                        else:
                            tmpMajorVersion = 'Windows 7'
                    elif minor == 2:
                        if isServer:
                            if fullversion.lower().find("r2") >= 0:
                                tmpMajorVersion = 'Server 2012 R2'
                            else:
                                tmpMajorVersion = 'Server 2012'
                        else:
                           if fullversion.lower().find("8.1") >= 0:
                               tmpMajorVersion = 'Windows 8.1'
                           else:
                               tmpMajorVersion = 'Windows 8'
                    elif minor == 3:
                        kernelversion = str(major) + '.' + str(minor) + '.' + currentBuildNumber
                        if isServer:
                            tmpMajorVersion = 'Server 2012 R2'
                        else:
                           tmpMajorVersion = 'Windows 8.1'
                elif major == 5:
                    if minor == 1:
                        tmpMajorVersion = 'Windows XP'
                    if minor == 2:
                        if isServer:
                            if fullversion.lower().find('r2'):
                                tmpMajorVersion = 'Server 2003 R2'
                            else:
                                tmpMajorVersion = 'Server 2003'
                        else:
                            tmpMajorVersion = 'Windows XP'
                majorversion = tmpMajorVersion
    try:
        timezone = time.tzname[time.localtime().tm_isdst]
    except:
        timezone = ""
    pwmode = getpwmode()
    arch = normalize_arch(type, platform.machine())
    t = collector.table("osinfo",
                        ["vendor", "type", "majorversion", "fullversion",
                         "ostype", "lastboottime", "kernelversion",
                         "timezone", "pwmode", "clock_skew", "arch"])
    t.add([translate(translate_vendor, vendor), type, majorversion,
           fullversion, ostype, lastboottime, kernelversion, timezone, pwmode,
           clock_skew, arch])

def _test():
    from sigma.gather import gathering
    gathering.test(Gather)

if __name__ == "__main__":
    _test()
