# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

"""Read configuration information for Sigma agent components.

We'll have a simple configuration file in $AVCONF/config.

The format is like Windows .ini files.  Specifically:

  [host]
  central-server = mycshost:17747

is the way to specify the host name 'mycshost' and port number 17747
as the default host and port to contact the central server.  You can
leave the :<port> part out.  If the option is not specified, the
central-server host value defaults to None.

This module also exports:

ES_PORT   -- the official SigmaSRC Port
geturl()  -- a function to turn a hostname + optional port into a URL
defaultroot() -- returns the default Sigma root (ignoring $AVROOT!)

"""

import os
import sys
import configparser as ConfigParser

# The ES_PORT constant is for public consumption; it is default the
# central server port.  Note that ES_PORT+1 is also reserved; it is
# the scheduler port (listening on the "localhost" interface only!)
ES_PORT = 17747 # hex for "ES"

LONG_BANNER = """\
\t\tSystem Access Security Notice!!

Authorization is required prior to attempting, or gaining, access
either directly to this host in interactive mode, or indirectly to the
information contained within.

Use of this system constitutes consent to security monitoring. Such
monitoring may include detailed system and user activity including
keystroke capture, host name and IP address specific information.

\t\tSystem Access Security Notice!!
"""

SHORT_BANNER = "Unauthorized access forbidden."

def geturl(cshost):
    """Massage a host/port argument into a URL.

    We accept several forms:
        scheme://whatever  exactly what it says
        host:port          same as http://host:port
        host               same as http://host:17747

    """
    if cshost.find("://") >= 0:
        return cshost
    else:
        if ':' not in cshost:
            cshost += ":%d" % ES_PORT
        return "http://%s" % cshost

def generic_opt(methodname, section, option, default, required):
    if required:
        def getter(self):
            method = getattr(self._configparser, methodname)
            return method(section, option)
    else:
        def getter(self):
            method = getattr(self._configparser, methodname)
            try:
                return method(section, option)
            except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
                return default
    def setter(self, value):
        deleter(self)
        if value is not None:
            if not self._configparser.has_section(section):
                self._configparser.add_section(section)
            self._configparser.set(section, option, str(value))
    def deleter(self):
        if self._configparser.has_section(section):
            self._configparser.remove_option(section, option)
    return property(getter, setter, deleter)

def optstr(section, option, default=None, required=False):
    return generic_opt("get", section, option, default, required)

def optfloat(section, option, default=None, required=False):
    return generic_opt("getfloat", section, option, default, required)

def optint(section, option, default=None, required=False):
    return generic_opt("getint", section, option, default, required)

def optbool(section, option, default=None, required=False):
    return generic_opt("getboolean", section, option, default, required)

def fallback(new, old):
    if os.path.exists(new) or not os.path.isdir(old):
        return new
    else:
        return old

def defaultroot():
    r"""Return the default Sigma agent root directory.

    The default differs per platform, as a convenience to developers:

    - On Windows:
      - If sys.argv[0] has the form ...\active\..., then the part
        before \active\ is used;
      - otherwise, we fall back to %ProgramFiles%\Sigma

    - On Unix:
      - If running as root, use /opt/sigma/agent
      - otherwise, use ~/.sigma

    - For backwards compatibility reasons, if the directory does not
      already exist, and the *old* default *does* exist, we return the
      old default instead.

    """
    if os.name == "nt":
        # abspath, NOT realpath: the agent lives under the 'active' junction
        # (active -> versions\fromrpm); realpath would resolve it away so the
        # \active\ marker below is never found and we fall back to the wrong root.
        path = os.path.abspath(sys.argv[0])
        while True:
            head, tail = os.path.split(path)
            if tail == "active":
                return head
            if head == path:
                break
            path = head
        systemdrive = os.getenv("SystemDrive")
        if not systemdrive:
            systemdrive = "C"
        programfiles = os.getenv("ProgramFiles")
        if not programfiles:
            programfiles = os.path.join(systemdrive, "\\Program Files")
        return os.path.join(programfiles, "Sigma")

    elif os.name == "posix" and os.getuid() == 0:
        return "/opt/sigma/agent"

    else:
        return os.path.expanduser("~/.sigma")

class Options(object):

    def __init__(self, configfile=None):
        avroot = os.getenv("AVROOT")
        if avroot:
            self.agenthome = avroot
        else:
            self.agenthome = defaultroot()
        self.agentdata = self._confdir("AVDATA", "data")
        self.agentversions = self._confdir("AVVERSIONS", "versions")
        self.agentactive = self._confdir("AVACTIVE", "active")
        agentdir = self._getagentdir()
        self.agentbin = self._confdir("AVBIN", agentdir, "bin")
        self.agentlib = self._confdir("AVLIB", agentdir, "lib")
        self.agentconf = self._confdir("AVCONF", "conf")

        # Backwards compatibility code for the logs directory.
        self.agentlogs = self._confdir("AVLOGS", "logs")
        if not os.path.isdir(self.agentlogs):
            oldagentlogs = self.datapath("log")
            if os.path.isdir(oldagentlogs):
                self.agentlogs = oldagentlogs

        # Backwards compatibility code for the cert directory
        self.agentcert = self.datapath("cert") # This ignores AVCERT!
        if not os.path.exists(os.path.join(self.agentcert, "key.pem")):
            oldagentcert = self._confdir("AVCERT", "cert")
            if os.path.exists(os.path.join(oldagentcert, "key.pem")):
                self.agentcert = oldagentcert

        self.fuelpath = self._getfuelpath()
        if not configfile:
            configfile = os.path.join(self.agentconf, "config")
        self.configfile = configfile
        if hasattr(ConfigParser, "RawConfigParser"):
            self._configparser = ConfigParser.RawConfigParser()
        else:
            self._configparser = ConfigParser.ConfigParser()
        # This will be silent if there is no config file.
        # That's a feature.
        self._configparser.read([configfile])

    def _confdir(self, envname, *basenames):
        dir = os.getenv(envname)
        if not dir:
            dir = os.path.join(self.agenthome, *basenames)
        return dir

    def _getagentdir(self):
        dirname = os.path.dirname
        # Assumption: __file__ == <agentdir>/bin/sigma/util/config.py
        return dirname(dirname(dirname(dirname(__file__))))

    def _getfuelpath(self):
        fp = os.getenv("FUELPATH")
        if fp:
            return fp.split(os.pathsep)
        return [os.path.join(self.agentlib, "fuel")]

    def datapath(self, *args):
        return os.path.join(self.agentdata, *args)

    def write(self):
        f = open(self.configfile, "w")
        try:
            self._configparser.write(f)
        finally:
            f.close()

    def set(self, section, option, value):
        cf = self._configparser
        if not cf.has_section(section):
            cf.add_section(section)
        cf.set(section, option, value)

    def update(self, triples):
        for section, option, value in triples:
            self.set(section, option, value)
        self.write()

    def updatefromtext(self, text):
        """Merge a text block formatted like config-file into the options.

        Write the result back out.

        Return True if any option actually changed its value as a
        result of the change, False if the changes were redundant.

        """
        cf = self._configparser
        before = [(section, option, cf.get(section, option))
                  for section in sorted(cf.sections())
                      for option in sorted(cf.options(section))]
        from io import StringIO
        f = StringIO(text)
        cf.read_file(f)
        after = [(section, option, cf.get(section, option))
                 for section in sorted(cf.sections())
                     for option in sorted(cf.options(section))]
        if before == after:
            return False
        self.write()
        return True

    # Option definitions

    # *** Please update //Product/Eng/Agent/Infrastructure/AgentConfig.doc
    # when adding or changing options. ***

    central_server = optstr("host", "central-server")
    scheduler = optstr("host", "scheduler",
                       default="127.0.0.1:" + str(ES_PORT+1))
    poliwall_options = optstr("host", "poliwall-options")
    poliwall_enforce = optbool("host", "poliwall-enforce", default=True)
    host_enforce = optbool("host", "host-enforce", default=True)
    slow_rfa_cutoff = optint("host", "slow-rfa-cutoff", default=400)
    rfa_interval = optint("host", "rfa-interval", default=60)
    backoff_factor = optfloat("host", "backoff-factor", default=2.0)
    backoff_limit = optint("host", "backoff-limit", default=3600)
    timeout_per_msg = optfloat("host", "timeout-per-msg", default=300)
    timeout_per_kb = optfloat("host", "timeout-per-kb", default=0.1)
    upload_max_files = optint("host", "upload-max-files", default=50)
    upload_max_kb = optint("host", "upload-max-kb", default=1000)
    service_name = optstr("host", "service-name", default="SigmaAgent")
    win_indicator = optbool("host", "win-indicator", default=True)
    win_balloon = optbool("host", "win-balloon", default=True)
    expert_key = optstr("host", "expert-key", default="avalon")
    cert_renew_days = optint("host", "cert-renew-days", default=30)
    max_upload_attempts = optint("host", "max-upload-attempts", default=2)
    upload_attempt_period = optint("host", "upload-attempt-period",
                                   default=24*3600)
    
    agent_ui_message = optstr("messages", "agent-ui-message")
    long_banner = optstr("messages", "long-banner", default=LONG_BANNER)
    short_banner = optstr("messages", "short-banner", default=SHORT_BANNER)

    log_level = optstr("log", "log-level")
    log_format = optstr("log", "log-format")
    log_keep_days = optint("log", "log-keep-days", default=9)
    log_prefix = "log" # Log files are named log.YYYY-MM-DD.txt
    debug_prefix = "debug" # Debug files are named debug.YYYY-MM-DD.txt
    action_prefix = "action" # Action  files are named action.YYYY-MM-DD.xml
    # For developers only:

    limit_gathers = optbool("gather", "limit-gathers",default=False)

def _test():
    op = Options()
    # This used to just print repr(op.central_server) without any
    # text.  I want to have a chance of finding the printed value even
    # if there is a lot of chatter around and the value isn't what I
    # expected, so I put the "op.central server is" preamble in
    # there.  Tim Freeman
    print("op.central_server is %s." % (repr(op.central_server),))

if __name__ == "__main__":
    _test()
