# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

"""Simple module for generating certain XML output."""

import sys
import time
import datetime
from html import escape as _html_escape

from sigma.util.xmlparse import ElementClass, Integer, String

MAXMSGS = 10 # Max number of violations kept per rule

class Detail(ElementClass):
    __element__ = "detail"
    __attributes__ = {"key": String, "value": String}

class Enforcement(ElementClass):
    __element__ = "enforcement"
    __attributes__ = {"text": String}

class Violation(ElementClass):
    __element__ = "violation"
    __attributes__ = {"text": String}
    __children__ = {Detail: "details[]"}

class Error(ElementClass):
    __element__ = "error"
    __attributes__ = {"text": String}

class Leave(ElementClass):
    __element__ = "leave"
    __attributes__ = {"text": String}

class Result(ElementClass):
    __element__ = "result"
    __attributes__ = {"rule-id": Integer}
    __children__ = {Violation: "violations[]",
                    Enforcement: "enforcements[]",
                    Error: "error",
                    Leave: "leave"}

    def __render__(self, *args):
        nv = len(self.violations)
        if nv > MAXMSGS+1: # +1 to make this idempotent
            del self.violations[MAXMSGS:]
            text = "[%d more truncated]" % (nv-MAXMSGS)
            self.violations.append(Violation(text=text))
        ne = len(self.violations)
        if ne > MAXMSGS+1: # +1 to make this idempotent
            del self.enforcements[MAXMSGS:]
            text = "[%d more truncated]" % (ne-MAXMSGS)
            self.enforcements.append(Violation(text=text))
        ElementClass.__render__(self, *args)

class Results(ElementClass):
    __element__ = "results"
    __attributes__ = {"directive-id": Integer,
                      "directive-version": Integer}
    __children__ = {Result: "results[]"}

class ResultsOut(object):

    def __init__(self, stream=None):
        if stream is None:
            stream = sys.stdout
        self.stream = stream
        self.root = None

    def flush(self):
        if self.root is not None:
            self.root.__render__(self.stream)
            self.root = None

    def close(self):
        if self.stream is not None:
            self.flush()
            self.stream = None

    __del__ = close

    def results(self, directive_id=None, directive_version=None):
        assert self.root is None
        self.root = Results(directive_id=directive_id,
                            directive_version=directive_version)

    def result(self, id=None):
        if self.root is None:
            self.results()
        self.root.results.append(Result(rule_id=id))

    # Enforce mutual exclusivity between <error>, <leave>, and
    # <violation>+

    def compliance(self, text=None):
        pass # XXX This is a no-op for now

    def error(self, text=None):
        if self.root is None:
            self.results()
        if not self.root.results:
            self.result()
        r = self.root.results[-1]
        r.violations = []
        r.leave = None
        r.error = Error(text=text)

    def leave(self, text=None):
        if self.root is None:
            self.results()
        if not self.root.results:
            self.result()
        r = self.root.results[-1]
        r.violations = []
        r.error = None
        r.leave = Leave(text=text)

    def violation(self, text=None):
        if self.root is None:
            self.results()
        if not self.root.results:
            self.result()
        r = self.root.results[-1]
        r.violations.append(Violation(text=text))
        r.leave = None
        r.error = None

    def detail(self, key, value):
        # XXX This is obsolete, but there's still Fuel that use it.
        if self.root is None:
            self.results()
        if not self.root.results:
            self.result()
        r = self.root.results[-1]
        if not r.violations:
            self.violation()
        v = r.violations[-1]
        v.details.append(Detail(key=str(key), value=str(value)))

    # This one is different; it doesn't negate the others

    def enforcement(self, text=None):
        if self.root is None:
            self.results()
        if not self.root.results:
            self.result()
        r = self.root.results[-1]
        r.enforcements.append(Enforcement(text=text))

    # These attributes are used by engine.py

    def _get_violations(self):
        if self.root is None:
            return 0
        rr = self.root.results
        if not rr:
            return 0
        vv = rr[-1].violations
        return len(vv)
    violations = property(_get_violations)

    def _get_enforcements(self):
        if self.root is None:
            return 0
        rr = self.root.results
        if not rr:
            return 0
        vv = rr[-1].enforcements
        return len(vv)
    enforcements = property(_get_enforcements)

    def status(self):
        """Returns 0 if compliant, 1 if violation, 2 if error, 3 if N/A."""
        if self.root is None:
            return 0
        rr = self.root.results
        if not rr:
            return 0
        if rr[-1].violations:
            return 1
        if rr[-1].error:
            return 2
        if rr[-1].leave:
            return 3
        return 0

def test():
    r = ResultsOut()
    r.result("r1")
    r.enforcement("r1.f1")
    r.violation("r1.v1")
    r.detail("msg", "r1.v1.d1")
    r.detail("msg", "r1.v1.d2")
    r.violation("r1.v2")
    r.detail("msg", "r1.v2.d1")
    r.enforcement("r1.f2")
    r.result("r2")
    r.result("r3")
    r.violation("r3.v1")
    r.violation("r3.v2")
    r.violation("r3.v3")
    r.result("r4")
    r.detail("msg", "r4.d1")
    r.result("r5")
    r.error("r5.e1")
    r.violation("r5.v1")
    r.result("r6")
    r.violation("r6.v1")
    r.error("r6.e1")
    r.flush()

if __name__ == "__main__":
    test()
