/**
 * @file mmsnareparse.c
 * @brief NXLog Snare Windows Security parser module.
 *
 * The module consumes Snare-formatted Windows Security events that are either
 * embedded in RFC3164/RFC5424 syslog envelopes or delivered as JSON payloads.
 * Incoming events are normalized and attached to the rsyslog message as a JSON
 * representation that mirrors the structure documented by NXLog and Snare.
 *
 * @note Concurrency & Locking: Module configuration lives in ::instanceData
 *       and becomes immutable after activation. Worker instances maintain only
 *       a pointer to the shared configuration, so no explicit locking is
 *       required.
 */

#include "config.h"
#include "rsyslog.h"

#include <ctype.h>
#include <errno.h>
#include <json.h>
#include <json_object_iterator.h>
#include <limits.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <stdio.h>
#include <time.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <regex.h>

#include "conf.h"
#include "datetime.h"
#include "errmsg.h"
#include "glbl.h"
#include "module-template.h"
#include "msg.h"
#include "syslogd-types.h"
#include "template.h"

MODULE_TYPE_OUTPUT;
MODULE_TYPE_NOKEEP;
MODULE_CNFNAME("mmsnareparse")

/**
 * @brief Default message container that receives parsed JSON output.
 */
#define MMSNAREPARSE_CONTAINER_DEFAULT "!win"

#define SECTION_FLAG_NONE 0u
#define SECTION_FLAG_NETWORK (1u << 0)
#define SECTION_FLAG_LAPS (1u << 1)
#define SECTION_FLAG_TLS (1u << 2)
#define SECTION_FLAG_WDAC (1u << 3)

#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))

/**
 * @brief Describes how a description block behaves while parsing.
 */
typedef enum section_behavior {
    sectionBehaviorStandard = 0,
    sectionBehaviorInlineValue,
    sectionBehaviorSemicolon,
    sectionBehaviorList
} section_behavior_t;

/**
 * @brief Metadata describing a description section inside Snare messages.
 *
 * @var section_descriptor::label Human-readable section label.
 * @var section_descriptor::canonical Canonical name used in the JSON output.
 * @var section_descriptor::behavior Parsing behavior applied to the section.
 * @var section_descriptor::flags Feature flags gating optional sections.
 */
typedef enum field_pattern_sensitivity {
    fieldSensitivityCanonical = 0,
    fieldSensitivityCaseSensitive,
    fieldSensitivityCaseInsensitive
} field_pattern_sensitivity_t;

typedef struct section_descriptor {
    const char *pattern;
    const char *canonical;
    section_behavior_t behavior;
    uint32_t flags;
    int priority;
    field_pattern_sensitivity_t sensitivity;
} section_descriptor_t;

/**
 * @brief Maps numeric Windows logon types to friendly names.
 *
 * @var logon_type_map::type_id Numeric Windows logon type identifier.
 * @var logon_type_map::description Canonical text description used in JSON output.
 */
typedef struct logon_type_map {
    int type_id;
    const char *description;
} logon_type_map_t;

/**
 * @brief Associates selected Event IDs with derived metadata.
 *
 * @var event_mapping::event_id Windows Event ID.
 * @var event_mapping::category High level category such as Logon or Process.
 * @var event_mapping::subtype Specific subtype within the category.
 * @var event_mapping::outcome Default outcome assigned when audit results are
 *      ambiguous.
 */
typedef struct event_mapping {
    int event_id;
    const char *category;
    const char *subtype;
    const char *outcome;
} event_mapping_t;

typedef enum field_value_type {
    fieldValueString = 0,
    fieldValueInt64,
    fieldValueInt64WithRaw,
    fieldValueBool,
    fieldValueJson,
    fieldValueLogonType,
    fieldValueRemoteCredentialGuard,
    fieldValueGuid,
    fieldValueIpAddress,
    fieldValueTimestamp,
    fieldValuePrivilegeList
} field_value_type_t;

typedef struct field_pattern {
    const char *pattern;
    const char *canonical;
    field_value_type_t value_type;
    const char *section;
    int priority;
    field_pattern_sensitivity_t sensitivity;
} field_pattern_t;

typedef struct event_field_mapping {
    int event_id;
    field_pattern_t *patterns;
    size_t pattern_count;
    uint32_t required_flags;
} event_field_mapping_t;

#define MAX_PARSING_ERRORS 64u

typedef enum validation_mode { VALIDATION_STRICT = 0, VALIDATION_MODERATE, VALIDATION_PERMISSIVE } validation_mode_t;

typedef struct validation_context {
    validation_mode_t mode;
    bool enable_debug;
    bool log_parsing_errors;
    bool continue_on_error;
    size_t max_errors_before_fail;
    size_t current_error_count;
} validation_context_t;

typedef struct runtime_config {
    field_pattern_t *custom_patterns;
    size_t custom_pattern_count;
    section_descriptor_t *custom_sections;
    size_t custom_section_count;
    event_field_mapping_t *custom_event_mappings;
    size_t custom_event_mapping_count;
    bool enable_debug;
    bool enable_fallback;
    char *config_file;
} runtime_config_t;

struct parse_context;

typedef struct field_detection_context {
    struct parse_context *parse_ctx;
    validation_context_t *validation;
    struct json_object *errors_array;
    size_t parsing_errors;
    bool enable_debug;
    bool enable_fallback;
} field_detection_context_t;

typedef void (*token_callback_t)(const char *token, size_t len, void *user_data);

#define FIELD_PRIORITY_BASE 10
#define FIELD_PRIORITY_EVENT_OVERRIDE 100

#define SECTION_PRIORITY_DEFAULT 100
#define SECTION_PRIORITY_OVERRIDE 200

#define FIELD_SECTION_EVENT_DATA "EventData"
#define FIELD_SECTION_LOGON "Logon"
#define FIELD_SECTION_ROOT "Root"

static const field_pattern_t g_coreFieldPatterns[] = {
    {"LogonType", "LogonType", fieldValueLogonType, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"SecurityID", "SecurityID", fieldValueString, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"AccountName", "AccountName", fieldValueString, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"AccountDomain", "AccountDomain", fieldValueString, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"LogonID", "LogonID", fieldValueString, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"LinkedLogonID", "LinkedLogonID", fieldValueString, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"NetworkAccountName", "NetworkAccountName", fieldValueString, NULL, FIELD_PRIORITY_BASE,
     fieldSensitivityCanonical},
    {"LogonGUID", "LogonGUID", fieldValueGuid, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"ProcessID", "ProcessID", fieldValueString, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"ProcessName", "ProcessName", fieldValueString, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"ProcessCommandLine", "ProcessCommandLine", fieldValueString, NULL, FIELD_PRIORITY_BASE,
     fieldSensitivityCanonical},
    {"TokenElevationType", "TokenElevationType", fieldValueString, NULL, FIELD_PRIORITY_BASE,
     fieldSensitivityCanonical},
    {"MandatoryLabel", "MandatoryLabel", fieldValueString, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"WorkstationName", "WorkstationName", fieldValueString, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"SourceNetworkAddress", "SourceNetworkAddress", fieldValueIpAddress, NULL, FIELD_PRIORITY_BASE,
     fieldSensitivityCanonical},
    {"SourcePort", "SourcePort", fieldValueInt64, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"ClientPort", "ClientPort", fieldValueInt64, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"DestinationPort", "DestinationPort", fieldValueInt64, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"LogonProcess", "LogonProcess", fieldValueString, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"AuthenticationPackage", "AuthenticationPackage", fieldValueString, NULL, FIELD_PRIORITY_BASE,
     fieldSensitivityCanonical},
    {"TransitedServices", "TransitedServices", fieldValueString, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"PackageName", "PackageName", fieldValueString, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"RestrictedAdminMode", "RestrictedAdminMode", fieldValueString, NULL, FIELD_PRIORITY_BASE,
     fieldSensitivityCanonical},
    {"VirtualAccount", "VirtualAccount", fieldValueString, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"ElevatedToken", "ElevatedToken", fieldValueString, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"ImpersonationLevel", "ImpersonationLevel", fieldValueString, NULL, FIELD_PRIORITY_BASE,
     fieldSensitivityCanonical},
    {"PreviousTime", "PreviousTime", fieldValueTimestamp, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"NewTime", "NewTime", fieldValueTimestamp, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"LastLogon", "LastLogon", fieldValueTimestamp, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"LastLogoff", "LastLogoff", fieldValueTimestamp, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"LastSuccessfulLogon", "LastSuccessfulLogon", fieldValueTimestamp, NULL, FIELD_PRIORITY_BASE,
     fieldSensitivityCanonical},
    {"LastFailedLogon", "LastFailedLogon", fieldValueTimestamp, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"LockoutTime", "LockoutTime", fieldValueTimestamp, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"PasswordLastSet", "PasswordLastSet", fieldValueTimestamp, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"AccountExpires", "AccountExpires", fieldValueTimestamp, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"KeyLength", "KeyLength", fieldValueInt64, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"RemoteCredentialGuard", "RemoteCredentialGuard", fieldValueRemoteCredentialGuard, NULL, FIELD_PRIORITY_BASE,
     fieldSensitivityCanonical},
    {"Privileges", "Privileges", fieldValuePrivilegeList, NULL, FIELD_PRIORITY_BASE, fieldSensitivityCanonical},
    {"SecurityID", "SecurityID", fieldValueString, "Subject", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"AccountName", "AccountName", fieldValueString, "Subject", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"AccountDomain", "AccountDomain", fieldValueString, "Subject", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"LogonID", "LogonID", fieldValueString, "Subject", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"LogonInformation", "LogonInformation", fieldValueString, "LogonInformation", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"LogonType", "LogonType", fieldValueLogonType, "LogonInformation", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"RestrictedAdminMode", "RestrictedAdminMode", fieldValueString, "LogonInformation", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"VirtualAccount", "VirtualAccount", fieldValueString, "LogonInformation", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"ElevatedToken", "ElevatedToken", fieldValueString, "LogonInformation", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"ImpersonationLevel", "ImpersonationLevel", fieldValueString, "LogonInformation", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"SecurityID", "SecurityID", fieldValueString, "NewLogon", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"AccountName", "AccountName", fieldValueString, "NewLogon", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"AccountDomain", "AccountDomain", fieldValueString, "NewLogon", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"LogonID", "LogonID", fieldValueString, "NewLogon", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"NewLogon", "NewLogon", fieldValueString, "NewLogon", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"LinkedLogonID", "LinkedLogonID", fieldValueString, "NewLogon", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"NetworkAccountName", "NetworkAccountName", fieldValueString, "NewLogon", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"NetworkAccountDomain", "NetworkAccountDomain", fieldValueString, "NewLogon", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"LogonGUID", "LogonGUID", fieldValueGuid, "NewLogon", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"NetworkInformation", "NetworkInformation", fieldValueString, "Network", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"WorkstationName", "WorkstationName", fieldValueString, "Network", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"SourceNetworkAddress", "SourceNetworkAddress", fieldValueIpAddress, "Network", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"SourcePort", "SourcePort", fieldValueInt64, "Network", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"NetworkAddress", "NetworkAddress", fieldValueIpAddress, "Network", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"ClientAddress", "ClientAddress", fieldValueIpAddress, "Network", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"ClientPort", "ClientPort", fieldValueInt64, "Network", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"DestinationAddress", "DestinationAddress", fieldValueIpAddress, "Network", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"DestinationPort", "DestinationPort", fieldValueInt64, "Network", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"Protocol", "Protocol", fieldValueString, "Network", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"Direction", "Direction", fieldValueString, "Network", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"LastLogon", "LastLogon", fieldValueTimestamp, "AccountInformation", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"LastLogoff", "LastLogoff", fieldValueTimestamp, "AccountInformation", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"LastSuccessfulLogon", "LastSuccessfulLogon", fieldValueTimestamp, "AccountInformation", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"LastFailedLogon", "LastFailedLogon", fieldValueTimestamp, "AccountInformation", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"LockoutTime", "LockoutTime", fieldValueTimestamp, "AccountInformation", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"PasswordLastSet", "PasswordLastSet", fieldValueTimestamp, "AccountInformation", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"AccountExpires", "AccountExpires", fieldValueTimestamp, "AccountInformation", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"ProcessInformation", "ProcessInformation", fieldValueString, "Process", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"CallerProcessID", "CallerProcessID", fieldValueString, "Process", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"CallerProcessName", "CallerProcessName", fieldValueString, "Process", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"NewProcessID", "NewProcessID", fieldValueString, "Process", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"NewProcessName", "NewProcessName", fieldValueString, "Process", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"CreatorProcessID", "CreatorProcessID", fieldValueString, "Process", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"CreatorProcessName", "CreatorProcessName", fieldValueString, "Process", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"ProcessCommandLine", "ProcessCommandLine", fieldValueString, "Process", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"PreviousTime", "PreviousTime", fieldValueTimestamp, "Process", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"NewTime", "NewTime", fieldValueTimestamp, "Process", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"DetailedAuthenticationInformation", "DetailedAuthenticationInformation", fieldValueString, "Authentication",
     FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"LogonProcess", "LogonProcess", fieldValueString, "Authentication", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"AuthenticationPackage", "AuthenticationPackage", fieldValueString, "Authentication", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"TransitedServices", "TransitedServices", fieldValueString, "Authentication", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"PackageName", "PackageName", fieldValueString, "Authentication", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"KeyLength", "KeyLength", fieldValueInt64, "Authentication", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"RemoteCredentialGuard", "RemoteCredentialGuard", fieldValueRemoteCredentialGuard, "Authentication",
     FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"FailureInformation", "FailureInformation", fieldValueString, "Failure", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"FailureReason", "FailureReason", fieldValueString, "Failure", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"Status", "Status", fieldValueString, "Failure", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"SubStatus", "SubStatus", fieldValueString, "Failure", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"PolicyName", "PolicyName", fieldValueString, "WDAC", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"PolicyVersion", "PolicyVersion", fieldValueString, "WDAC", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"EnforcementMode", "EnforcementMode", fieldValueString, "WDAC", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"User", "User", fieldValueString, "WDAC", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"PID", "PID", fieldValueInt64WithRaw, "WDAC", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"PolicyID", "PolicyID", fieldValueString, "WUFB", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"Ring", "Ring", fieldValueString, "WUFB", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"FromService", "FromService", fieldValueString, "WUFB", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"EnforcementResult", "EnforcementResult", fieldValueString, "WUFB", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"ServiceName", "ServiceName", fieldValueString, "Kerberos", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"ServiceID", "ServiceID", fieldValueString, "Kerberos", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"TicketOptions", "TicketOptions", fieldValueString, "Kerberos", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"ResultCode", "ResultCode", fieldValueString, "Kerberos", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"TicketEncryptionType", "TicketEncryptionType", fieldValueString, "Kerberos", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"PreAuthenticationType", "PreAuthenticationType", fieldValueString, "Kerberos", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"CertificateInfo", "CertificateInfo", fieldValueString, "Kerberos", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"LAPSContext", "LAPSContext", fieldValueString, "LAPS", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"PolicyVersion", "PolicyVersion", fieldValueInt64, "LAPS", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"CredentialRotation", "CredentialRotation", fieldValueBool, "LAPS", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"TLSInspection", "TLSInspection", fieldValueString, "TLSInspection", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"Reason", "Reason", fieldValueString, "TLSInspection", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"Policy", "Policy", fieldValueString, "TLSInspection", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"FilterInformation", "FilterInformation", fieldValueString, "Filter", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"FilterRuntimeID", "FilterRuntimeID", fieldValueString, "Filter", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
    {"LayerName", "LayerName", fieldValueString, "Filter", FIELD_PRIORITY_BASE + 10, fieldSensitivityCanonical},
    {"LayerRuntimeID", "LayerRuntimeID", fieldValueString, "Filter", FIELD_PRIORITY_BASE + 10,
     fieldSensitivityCanonical},
};

static field_pattern_t g_event6281FieldPatterns[] = {
    {"PolicyName", "PolicyName", fieldValueString, "WDAC", FIELD_PRIORITY_EVENT_OVERRIDE, fieldSensitivityCanonical},
    {"PolicyVersion", "PolicyVersion", fieldValueString, "WDAC", FIELD_PRIORITY_EVENT_OVERRIDE,
     fieldSensitivityCanonical},
    {"EnforcementMode", "EnforcementMode", fieldValueString, "WDAC", FIELD_PRIORITY_EVENT_OVERRIDE,
     fieldSensitivityCanonical},
    {"User", "User", fieldValueString, "WDAC", FIELD_PRIORITY_EVENT_OVERRIDE, fieldSensitivityCanonical},
    {"PID", "PID", fieldValueInt64WithRaw, "WDAC", FIELD_PRIORITY_EVENT_OVERRIDE, fieldSensitivityCanonical},
};

static field_pattern_t g_event1243FieldPatterns[] = {
    {"PolicyID", "PolicyID", fieldValueString, "WUFB", FIELD_PRIORITY_EVENT_OVERRIDE, fieldSensitivityCanonical},
    {"Ring", "Ring", fieldValueString, "WUFB", FIELD_PRIORITY_EVENT_OVERRIDE, fieldSensitivityCanonical},
    {"FromService", "FromService", fieldValueString, "WUFB", FIELD_PRIORITY_EVENT_OVERRIDE, fieldSensitivityCanonical},
    {"EnforcementResult", "EnforcementResult", fieldValueString, "WUFB", FIELD_PRIORITY_EVENT_OVERRIDE,
     fieldSensitivityCanonical},
};

static const event_field_mapping_t g_eventFieldMappings[] = {
    {6281, g_event6281FieldPatterns, ARRAY_SIZE(g_event6281FieldPatterns), SECTION_FLAG_WDAC},
    {1243, g_event1243FieldPatterns, ARRAY_SIZE(g_event1243FieldPatterns), SECTION_FLAG_NONE},
};

/**
 * @brief Per-instance configuration shared across workers.
 *
 * @var instanceData::container Target container for parsed JSON output.
 * @var instanceData::enableNetwork Controls whether Network sections are
 *      captured.
 * @var instanceData::enableLaps Controls whether Local Administrator Password
 *      Solution data is parsed.
 * @var instanceData::enableTls Controls whether TLS Inspection sections are
 *      captured.
 * @var instanceData::enableWdac Enables parsing of Windows Defender Application
 *      Control metadata.
 * @var instanceData::emitRawPayload Emits raw Snare text or JSON payload
 *      alongside parsed data when true.
 * @var instanceData::emitDebugJson Forces creation of an empty Unparsed array
 *      for easier diagnostics when true.
 * @var instanceData::validationTemplate Base validation policy copied into
 *      each parse context.
 * @var instanceData::sectionDescriptors Runtime section descriptor table.
 * @var instanceData::sectionDescriptorCount Number of active section
 *      descriptors.
 * @var instanceData::corePatterns Runtime field pattern table shared by all
 *      events.
 * @var instanceData::corePatternCount Number of entries in ::corePatterns.
 * @var instanceData::eventFieldMappings Runtime event-specific pattern table.
 * @var instanceData::eventFieldMappingCount Number of event-specific mappings.
 * @var instanceData::eventMappings Runtime event metadata overrides.
 * @var instanceData::eventMappingCount Number of event metadata entries.
 * @var instanceData::runtimeConfig Runtime configuration loaded from disk.
 */
typedef struct _instanceData {
    uchar *container;
    sbool enableNetwork;
    sbool enableLaps;
    sbool enableTls;
    sbool enableWdac;
    sbool emitRawPayload;
    sbool emitDebugJson;
    uchar *ignoreTrailingPattern;
    regex_t ignoreTrailingPattern_preg; /* compiled regex for ignoreTrailingPattern.regex */
    sbool ignoreTrailingPattern_isRegex; /* flag indicating if regex mode is enabled */
    size_t searchLimit; /* maximum characters to search at end of message for trailing pattern */
    validation_context_t validationTemplate;
    section_descriptor_t *sectionDescriptors;
    size_t sectionDescriptorCount;
    field_pattern_t *corePatterns;
    size_t corePatternCount;
    event_field_mapping_t *eventFieldMappings;
    size_t eventFieldMappingCount;
    event_mapping_t *eventMappings;
    size_t eventMappingCount;
    runtime_config_t runtimeConfig;
} instanceData;

static void free_runtime_tables(instanceData *pData);
static rsRetVal parse_validation_mode(const char *mode, validation_mode_t *modeOut);
static void cleanup_event_field_mapping(event_field_mapping_t *mapping);
static sbool parse_section_behavior_string(const char *text, section_behavior_t *behavior);
static sbool parse_field_value_type_string(const char *text, field_value_type_t *type);
static sbool parse_field_sensitivity_string(const char *text, field_pattern_sensitivity_t *sensitivity);
static uint32_t parse_section_flags_array(struct json_object *value, sbool *ok);
static bool is_guid_format(const char *value) ATTR_UNUSED;
static bool is_ip_address(const char *value) ATTR_UNUSED;
static bool is_timestamp_format(const char *value) ATTR_UNUSED;
static bool is_iso8601_timestamp(const char *value) ATTR_UNUSED;
static bool is_windows_event_timestamp(const char *value) ATTR_UNUSED;
static rsRetVal store_validated_string(field_detection_context_t *fdCtx,
                                       struct json_object *target,
                                       const char *key,
                                       const char *value,
                                       bool (*validator)(const char *),
                                       const char *error_message,
                                       sbool *storedOut);
static const char *section_behavior_to_string(section_behavior_t behavior) ATTR_UNUSED;
static const char *field_value_type_to_string(field_value_type_t type) ATTR_UNUSED;
static const char *field_sensitivity_to_string(field_pattern_sensitivity_t sensitivity) ATTR_UNUSED;
static struct json_object *serialize_section_flags(uint32_t flags) ATTR_UNUSED;
static rsRetVal load_configuration(runtime_config_t *config, const char *config_file);
static rsRetVal save_configuration(const runtime_config_t *config, const char *config_file) ATTR_UNUSED;
static rsRetVal apply_runtime_configuration(instanceData *pData, const runtime_config_t *config);
static rsRetVal handle_parsing_error(field_detection_context_t *ctx, const char *error_message, const char *context);
static void normalize_literal_tabs(char *text);

/** worker data */
typedef struct wrkrInstanceData {
    instanceData *pData;
} wrkrInstanceData_t;

struct modConfData_s {
    rsconf_t *pConf;
    char *definitionFile;
    char *definitionJson;
    char *runtimeConfigFile;
    uchar *ignoreTrailingPattern;
    uchar *ignoreTrailingPatternRegex;
    size_t searchLimit;
    validation_context_t validationTemplate;
};
static modConfData_t *loadModConf = NULL;
static modConfData_t *runModConf = NULL;

static const section_descriptor_t g_builtinSectionDescriptors[] = {
    {"Subject", "Subject", sectionBehaviorStandard, SECTION_FLAG_NONE, SECTION_PRIORITY_DEFAULT,
     fieldSensitivityCaseSensitive},
    {"Logon Information", "LogonInformation", sectionBehaviorStandard, SECTION_FLAG_NONE, SECTION_PRIORITY_DEFAULT,
     fieldSensitivityCaseSensitive},
    {"New Logon", "NewLogon", sectionBehaviorStandard, SECTION_FLAG_NONE, SECTION_PRIORITY_DEFAULT,
     fieldSensitivityCaseSensitive},
    {"Account For Which Logon Failed", "TargetAccount", sectionBehaviorStandard, SECTION_FLAG_NONE,
     SECTION_PRIORITY_DEFAULT, fieldSensitivityCaseSensitive},
    {"Failure Information", "Failure", sectionBehaviorStandard, SECTION_FLAG_NONE, SECTION_PRIORITY_DEFAULT,
     fieldSensitivityCaseSensitive},
    {"Network Information", "Network", sectionBehaviorStandard, SECTION_FLAG_NETWORK, SECTION_PRIORITY_DEFAULT,
     fieldSensitivityCaseSensitive},
    {"Process Information", "Process", sectionBehaviorStandard, SECTION_FLAG_NONE, SECTION_PRIORITY_DEFAULT,
     fieldSensitivityCaseSensitive},
    {"Detailed Authentication Information", "DetailedAuthentication", sectionBehaviorStandard, SECTION_FLAG_NONE,
     SECTION_PRIORITY_DEFAULT, fieldSensitivityCaseSensitive},
    {"Application Information", "Application", sectionBehaviorStandard, SECTION_FLAG_NONE, SECTION_PRIORITY_DEFAULT,
     fieldSensitivityCaseSensitive},
    {"Filter Information", "Filter", sectionBehaviorStandard, SECTION_FLAG_NONE, SECTION_PRIORITY_DEFAULT,
     fieldSensitivityCaseSensitive},
    {"Account Information", "AccountInformation", sectionBehaviorStandard, SECTION_FLAG_NONE, SECTION_PRIORITY_DEFAULT,
     fieldSensitivityCaseSensitive},
    {"Service Information", "Service", sectionBehaviorStandard, SECTION_FLAG_NONE, SECTION_PRIORITY_DEFAULT,
     fieldSensitivityCaseSensitive},
    {"Additional Information", "AdditionalInformation", sectionBehaviorStandard, SECTION_FLAG_NONE,
     SECTION_PRIORITY_DEFAULT, fieldSensitivityCaseSensitive},
    {"Share Information", "Share", sectionBehaviorStandard, SECTION_FLAG_NONE, SECTION_PRIORITY_DEFAULT,
     fieldSensitivityCaseSensitive},
    {"Certificate Information", "Certificate", sectionBehaviorStandard, SECTION_FLAG_NONE, SECTION_PRIORITY_DEFAULT,
     fieldSensitivityCaseSensitive},
    {"Remote Credential Guard", "RemoteCredentialGuard", sectionBehaviorInlineValue, SECTION_FLAG_NONE,
     SECTION_PRIORITY_OVERRIDE, fieldSensitivityCaseInsensitive},
    {"LAPS Context", "LAPS", sectionBehaviorSemicolon, SECTION_FLAG_LAPS, SECTION_PRIORITY_DEFAULT,
     fieldSensitivityCaseSensitive},
    {"TLS Inspection", "TLSInspection", sectionBehaviorStandard, SECTION_FLAG_TLS, SECTION_PRIORITY_DEFAULT,
     fieldSensitivityCaseSensitive},
    {"Privileges", "Privileges", sectionBehaviorList, SECTION_FLAG_NONE, SECTION_PRIORITY_OVERRIDE,
     fieldSensitivityCaseSensitive},
};

static const logon_type_map_t g_logonTypeMap[] = {{0, "System"},
                                                  {1, "System"},
                                                  {2, "Interactive"},
                                                  {3, "Network"},
                                                  {4, "Batch"},
                                                  {5, "Service"},
                                                  {7, "Unlock"},
                                                  {8, "NetworkCleartext"},
                                                  {9, "NewCredentials"},
                                                  {10, "RemoteInteractive"},
                                                  {11, "CachedInteractive"},
                                                  {12, "CachedRemoteInteractive"},
                                                  {13, "CachedUnlock"}};

static const event_mapping_t g_eventMappings[] = {{4624, "Logon", "Success", "success"},
                                                  {4625, "Logon", "Failure", "failure"},
                                                  {4626, "Logon", "Success", "success"},
                                                  {4627, "Logon", "Success", "success"},
                                                  {4672, "Privilege", "Assignment", "success"},
                                                  {4688, "Process", "Creation", "success"},
                                                  {4768, "Kerberos", "TGTRequest", NULL},
                                                  {4769, "Kerberos", "ServiceTicket", NULL},
                                                  {4771, "Kerberos", "PreAuthFailure", NULL},
                                                  {5140, "FileShare", "Access", NULL},
                                                  {5157, "FilteringPlatform", "PacketDrop", "failure"},
                                                  {6281, "WDAC", "Enforcement", NULL},
                                                  {1102, "Audit", "LogCleared", NULL},
                                                  {1243, "WindowsUpdate", "Deployment", NULL},
                                                  /* Account Management events */
                                                  {4720, "AccountManagement", "Creation", "success"},
                                                  {4722, "AccountManagement", "Enabled", "success"},
                                                  {4723, "AccountManagement", "PasswordChangeAttempt", "success"},
                                                  {4724, "AccountManagement", "PasswordReset", "success"},
                                                  {4725, "AccountManagement", "AccountDisabled", "success"},
                                                  {4726, "AccountManagement", "AccountDeleted", "success"},
                                                  {4738, "AccountManagement", "AccountChange", "success"},
                                                  {4781, "AccountManagement", "AccountNameChange", "success"},
                                                  /* IPsec events */
                                                  {4650, "IPsec", "MainModeEstablished", "success"},
                                                  {4651, "IPsec", "MainModeFailure", "failure"},
                                                  {4652, "IPsec", "QuickModeEstablished", "success"},
                                                  {4653, "IPsec", "QuickModeFailure", "failure"},
                                                  {4654, "IPsec", "MainModeAuthFailure", "failure"},
                                                  {4655, "IPsec", "MainModeAuthFailure", "failure"},
                                                  {4656, "IPsec", "MainModeKeyFailure", "failure"},
                                                  {4657, "IPsec", "MainModePeerAuthFailure", "failure"},
                                                  {4658, "IPsec", "MainModeInvalidCookie", "failure"},
                                                  {4659, "IPsec", "MainModeDuplicateSPI", "failure"},
                                                  {4660, "IPsec", "MainModeInvalidProposal", "failure"},
                                                  {4661, "IPsec", "MainModeTimeout", "failure"},
                                                  {4663, "IPsec", "MainModeTimeout", "failure"},
                                                  {4664, "IPsec", "MainModeTimeout", "failure"},
                                                  {4665, "IPsec", "MainModeTimeout", "failure"},
                                                  {4666, "IPsec", "MainModeTimeout", "failure"},
                                                  {4667, "IPsec", "MainModeTimeout", "failure"},
                                                  {4668, "IPsec", "MainModeTimeout", "failure"},
                                                  {4670, "IPsec", "MainModeTimeout", "failure"},
                                                  {4671, "IPsec", "MainModeTimeout", "failure"},
                                                  /* System events */
                                                  {4608, "System", "Startup", "success"},
                                                  {4609, "System", "Shutdown", "success"},
                                                  {4610, "System", "Startup", "success"},
                                                  {4611, "System", "Shutdown", "success"},
                                                  {4612, "System", "AuditLogCleared", "success"},
                                                  {4614, "System", "AuditLogCleared", "success"},
                                                  {4615, "System", "IPCStatusChange", "success"},
                                                  {4616, "System", "SystemTimeChange", "success"},
                                                  {4618, "System", "SecurityStateChange", "success"},
                                                  {4621, "System", "AdminLogon", "success"},
                                                  {4622, "System", "Logon", "success"},
                                                  {4697, "System", "ServiceInstalled", "success"},
                                                  {4821, "System", "CertificateServices", "success"},
                                                  {4822, "System", "CertificateServices", "success"},
                                                  {4823, "System", "CertificateServices", "success"},
                                                  {4824, "System", "CertificateServices", "success"},
                                                  {4830, "System", "CertificateServices", "success"}};

static int is_placeholder(const char *value) {
    if (value == NULL) return 1;
    while (*value && isspace((unsigned char)*value)) ++value;
    if (*value == '\0') return 1;
    if (!strcmp(value, "-")) return 1;
    if (!strcasecmp(value, "N/A")) return 1;
    return 0;
}

static inline char *strdup_range(const char *start, size_t len) {
    char *out;
    out = malloc(len + 1);
    if (out == NULL) return NULL;
    memcpy(out, start, len);
    out[len] = '\0';
    return out;
}

static inline char *trim_copy(const char *start, size_t len) {
    while (len > 0 && isspace((unsigned char)*start)) {
        ++start;
        --len;
    }
    while (len > 0 && isspace((unsigned char)start[len - 1])) {
        --len;
    }
    return strdup_range(start, len);
}

static inline void trim_inplace(char *s) {
    char *start;
    char *end;
    size_t len;
    if (s == NULL) return;
    start = s;
    while (*start && isspace((unsigned char)*start)) ++start;
    if (start != s) memmove(s, start, strlen(start) + 1);
    len = strlen(s);
    end = s + len;
    while (end > s && isspace((unsigned char)*(end - 1))) --end;
    *end = '\0';
}

static inline bool looks_like_label_start(const char *p, const char *end) {
    const char *cursor = p;
    bool hasAlpha = false;
    if (cursor == NULL || cursor >= end) return false;

    /* Labels usually begin with an uppercase letter (e.g. "Security ID:"). */
    if (!isupper((unsigned char)*cursor)) return false;

    while (cursor < end) {
        unsigned char c = (unsigned char)*cursor;
        if (c == ':') return hasAlpha;
        if (isalnum(c)) hasAlpha = true;
        if (!(isalnum(c) || c == ' ' || c == '-' || c == '/' || c == '(' || c == ')' || c == '#')) return false;
        ++cursor;
    }
    return false;
}

static rsRetVal tokenize_on_multispace(const char *str, size_t len, token_callback_t callback, void *user_data) {
    if (callback == NULL) return RS_RET_INVALID_PARAMS;
    if (str == NULL || len == 0) return RS_RET_OK;

    const char *ptr = str;
    size_t i = 0;
    bool in_token = false;
    size_t start = 0;
    bool tokenSawColon = false;
    bool tokenHasValue = false;

    while (i < len) {
        if (ptr[i] == ' ' || ptr[i] == '\t') {
            size_t j = i;
            while (j < len && (ptr[j] == ' ' || ptr[j] == '\t')) ++j;
            size_t spaces = j - i;

            bool treat_as_delim = false;
            if (ptr[i] == '\t') {
                treat_as_delim = true;
            } else if (spaces >= 2) {
                bool colon_precedes = in_token && i > start && ptr[i - 1] == ':';
                if (!colon_precedes) {
                    treat_as_delim = true;
                }
            }

            if (!treat_as_delim && looks_like_label_start(ptr + j, ptr + len)) {
                treat_as_delim = true;
            }

            if (treat_as_delim && (!in_token || !tokenSawColon || !tokenHasValue)) {
                treat_as_delim = false;
            }

            if (treat_as_delim) {
                if (in_token) {
                    callback(ptr + start, i - start, user_data);
                    in_token = false;
                    tokenSawColon = false;
                    tokenHasValue = false;
                }
                i = j;
                continue;
            }

            if (!in_token) {
                start = i;
                in_token = true;
                tokenSawColon = false;
                tokenHasValue = false;
            }
            i = j;
            continue;
        }

        if (!in_token) {
            start = i;
            in_token = true;
            tokenSawColon = false;
            tokenHasValue = false;
        }
        if (ptr[i] == ':' && in_token) tokenSawColon = true;
        if (in_token && tokenSawColon && !isspace((unsigned char)ptr[i]) && ptr[i] != ':') tokenHasValue = true;
        ++i;
    }

    if (in_token) {
        callback(ptr + start, len - start, user_data);
    }

    return RS_RET_OK;
}

static char *trim_whitespace_enhanced(const char *input) {
    if (input == NULL) return NULL;

    size_t len = strlen(input);
    if (len == 0) return strdup("");

    const char *start = input;
    const char *end = input + len - 1;

    while (start <= end && isspace((unsigned char)*start)) {
        start++;
    }

    while (end >= start && isspace((unsigned char)*end)) {
        end--;
    }

    size_t new_len = (size_t)(end - start + 1);
    char *result = malloc(new_len + 1);
    if (result == NULL) return NULL;

    memcpy(result, start, new_len);
    result[new_len] = '\0';

    return result;
}

static bool is_placeholder_value(const char *value) {
    if (value == NULL) return true;

    const char *placeholders[] = {"-",
                                  "N/A",
                                  "n/a",
                                  "NULL",
                                  "null",
                                  "None",
                                  "none",
                                  "Not Available",
                                  "not available",
                                  "Unknown",
                                  "unknown",
                                  "<never>",
                                  "<value not set>",
                                  "<not set>",
                                  ""};

    for (size_t i = 0; i < ARRAY_SIZE(placeholders); ++i) {
        if (strcasecmp(value, placeholders[i]) == 0) {
            return true;
        }
    }

    return false;
}

static bool is_guid_format(const char *value) {
    if (value == NULL) return false;

    size_t len = strlen(value);
    size_t offset = 0;

    if (len == 38) {
        if (value[0] != '{' || value[len - 1] != '}') return false;
        offset = 1;
    } else if (len == 36) {
        offset = 0;
    } else {
        return false;
    }

    static const size_t hyphen_positions[] = {8u, 13u, 18u, 23u};

    for (size_t i = 0; i < 36; ++i) {
        const size_t absolute_index = offset + i;
        bool expect_hyphen = 0;

        for (size_t j = 0; j < ARRAY_SIZE(hyphen_positions); ++j) {
            if (i == hyphen_positions[j]) {
                expect_hyphen = 1;
                break;
            }
        }

        if (expect_hyphen) {
            if (value[absolute_index] != '-') return false;
        } else {
            if (!isxdigit((unsigned char)value[absolute_index])) return false;
        }
    }

    return true;
}

static bool is_ip_address(const char *value) {
    if (value == NULL) return false;

    struct in_addr ipv4_addr;
    if (inet_pton(AF_INET, value, &ipv4_addr) == 1) return true;

#ifdef AF_INET6
    struct in6_addr ipv6_addr;
    if (inet_pton(AF_INET6, value, &ipv6_addr) == 1) return true;
#endif

    return false;
}

static bool parse_int_range(const char *digits, size_t length, int min_value, int max_value, int *out) {
    if (digits == NULL || length == 0) return false;
    int value = 0;
    for (size_t i = 0; i < length; ++i) {
        if (!isdigit((unsigned char)digits[i])) return false;
        if (value > INT_MAX / 10) return false;
        const int digit = digits[i] - '0';
        if (value == INT_MAX / 10 && digit > INT_MAX % 10) return false;
        value = (value * 10) + digit;
    }
    if (value < min_value || value > max_value) return false;
    if (out != NULL) *out = value;
    return true;
}

static bool is_leap_year(int year) {
    if (year % 4 != 0) return false;
    if (year % 100 != 0) return true;
    return (year % 400) == 0;
}

static int days_in_month(int year, int month) {
    static const int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    if (month < 1 || month > 12) return 0;
    if (month == 2 && is_leap_year(year)) return 29;
    return days[month];
}

static bool is_iso8601_timestamp(const char *value) {
    if (value == NULL) return false;

    size_t len = strlen(value);
    if (len < 20) return false;

    if (value[4] != '-' || value[7] != '-' || value[10] != 'T' || value[13] != ':' || value[16] != ':') return false;

    int year = 0;
    int month = 0;
    int day = 0;
    int hour = 0;
    int minute = 0;
    int second = 0;

    if (!parse_int_range(value, 4, 0, 9999, &year)) return false;
    if (!parse_int_range(value + 5, 2, 1, 12, &month)) return false;
    if (!parse_int_range(value + 8, 2, 1, 31, &day)) return false;
    const int max_day = days_in_month(year, month);
    if (max_day == 0 || day > max_day) return false;
    if (!parse_int_range(value + 11, 2, 0, 23, &hour)) return false;
    if (!parse_int_range(value + 14, 2, 0, 59, &minute)) return false;
    if (!parse_int_range(value + 17, 2, 0, 60, &second)) return false;
    if (second == 60 && minute != 59) return false;

    size_t pos = 19;
    if (value[pos] == '.') {
        pos++;
        if (pos >= len || !isdigit((unsigned char)value[pos])) return false;
        while (pos < len && isdigit((unsigned char)value[pos])) pos++;
    }

    if (pos >= len) return false;

    if (value[pos] == 'Z' || value[pos] == 'z') {
        pos++;
    } else if (value[pos] == '+' || value[pos] == '-') {
        pos++;
        if (pos + 1 >= len) return false;
        int tz_hour = 0;
        if (!parse_int_range(value + pos, 2, 0, 23, &tz_hour)) return false;
        pos += 2;
        if (pos < len) {
            int tz_minute = 0;
            if (value[pos] == ':') {
                pos++;
                if (pos + 1 >= len) return false;
                if (!parse_int_range(value + pos, 2, 0, 59, &tz_minute)) return false;
                pos += 2;
            } else if (isdigit((unsigned char)value[pos])) {
                if (pos + 1 >= len) return false;
                if (!parse_int_range(value + pos, 2, 0, 59, &tz_minute)) return false;
                pos += 2;
            }
        }
    } else {
        return false;
    }

    while (pos < len && isspace((unsigned char)value[pos])) pos++;
    return pos == len;
}

static bool token_matches(const char *token, const char *const *table, size_t table_len) {
    for (size_t i = 0; i < table_len; ++i) {
        if (strcasecmp(token, table[i]) == 0) return true;
    }
    return false;
}

static int lookup_month_index(const char *month) {
    static const char *const months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
                                         "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
    for (size_t i = 0; i < ARRAY_SIZE(months); ++i) {
        if (strcasecmp(month, months[i]) == 0) return (int)i + 1;
    }
    return 0;
}

static bool is_windows_event_timestamp(const char *value) {
    if (value == NULL) return false;

    char weekday[4] = {0};
    char month[4] = {0};
    int day = 0;
    int hour = 0;
    int minute = 0;
    int second = 0;
    int year = 0;
    int consumed = 0;

    if (sscanf(value, "%3s %3s %d %d:%d:%d %d%n", weekday, month, &day, &hour, &minute, &second, &year, &consumed) != 7)
        return false;

    const char *tail = value + consumed;
    while (*tail != '\0' && isspace((unsigned char)*tail)) tail++;
    if (*tail != '\0') return false;

    static const char *const weekdays[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};

    if (!token_matches(weekday, weekdays, ARRAY_SIZE(weekdays))) return false;
    const int month_index = lookup_month_index(month);
    if (month_index == 0) return false;
    if (day < 1 || day > days_in_month(year, month_index)) return false;
    if (hour < 0 || hour > 23) return false;
    if (minute < 0 || minute > 59) return false;
    if (second < 0 || second > 60) return false;
    if (second == 60 && minute != 59) return false;
    if (year < 1900) return false;

    return true;
}

static bool is_timestamp_format(const char *value) {
    if (value == NULL) return false;
    return is_iso8601_timestamp(value) || is_windows_event_timestamp(value);
}

static bool is_json_format(const char *value) {
    if (value == NULL) return false;
    size_t len = strlen(value);
    if (len < 2) return false;
    if ((value[0] == '{' && value[len - 1] == '}') || (value[0] == '[' && value[len - 1] == ']')) return true;
    return false;
}

static void unescape_hash_sequences(char *s) {
    char *src = s;
    char *dst = s;
    while (*src != '\0') {
        if (src[0] == '#' && src[1] >= '0' && src[1] <= '7' && src[2] >= '0' && src[2] <= '7' && src[3] >= '0' &&
            src[3] <= '7') {
            int val = ((src[1] - '0') << 6) | ((src[2] - '0') << 3) | (src[3] - '0');
            *dst++ = (char)val;
            src += 4;
        } else {
            *dst++ = *src++;
        }
    }
    *dst = '\0';
}

static void normalize_literal_tabs(char *text) {
    char *src;
    char *dst;

    if (text == NULL) return;

    src = text;
    dst = text;
    while (*src != '\0') {
        if (src[0] == '\\' && src[1] == 't') {
            *dst++ = '\t';
            src += 2;
        } else {
            *dst++ = *src++;
        }
    }
    *dst = '\0';
}

static inline char *normalize_label(const char *label) {
    size_t len = strlen(label);
    char *out = malloc(len + 1);
    size_t j = 0;
    sbool upperNext = 1;
    unsigned int parenDepth = 0;
    if (out == NULL) return NULL;
    for (size_t i = 0; i < len; ++i) {
        unsigned char c = (unsigned char)label[i];
        if (c == '(') {
            ++parenDepth;
            upperNext = 1;
            continue;
        }
        if (c == ')' && parenDepth > 0) {
            --parenDepth;
            upperNext = 1;
            continue;
        }
        if (parenDepth > 0) {
            continue;
        }
        if (isalnum(c)) {
            if (upperNext)
                out[j++] = (char)toupper(c);
            else
                out[j++] = (char)c;
            upperNext = 0;
        } else {
            upperNext = 1;
        }
    }
    out[j] = '\0';
    if (j == 0) {
        free(out);
        out = NULL;
    }
    return out;
}

static void cleanup_section_descriptor(section_descriptor_t *desc) {
    if (desc == NULL) return;
    free((char *)desc->pattern);
    free((char *)desc->canonical);
    desc->pattern = NULL;
    desc->canonical = NULL;
}

static void cleanup_field_pattern(field_pattern_t *pattern) {
    if (pattern == NULL) return;
    free((char *)pattern->pattern);
    free((char *)pattern->canonical);
    free((char *)pattern->section);
    pattern->pattern = NULL;
    pattern->canonical = NULL;
    pattern->section = NULL;
}

static void cleanup_event_mapping(event_mapping_t *mapping) {
    if (mapping == NULL) return;
    free((char *)mapping->category);
    free((char *)mapping->subtype);
    free((char *)mapping->outcome);
    mapping->category = NULL;
    mapping->subtype = NULL;
    mapping->outcome = NULL;
}

static void init_validation_context(validation_context_t *ctx) {
    if (ctx == NULL) return;
    ctx->mode = VALIDATION_PERMISSIVE;
    ctx->enable_debug = false;
    ctx->log_parsing_errors = true;
    ctx->continue_on_error = true;
    ctx->max_errors_before_fail = MAX_PARSING_ERRORS;
    ctx->current_error_count = 0;
}

static void init_runtime_config(runtime_config_t *config) {
    if (config == NULL) return;
    config->custom_patterns = NULL;
    config->custom_pattern_count = 0;
    config->custom_sections = NULL;
    config->custom_section_count = 0;
    config->custom_event_mappings = NULL;
    config->custom_event_mapping_count = 0;
    config->enable_debug = false;
    config->enable_fallback = true;
    config->config_file = NULL;
}

static void free_runtime_config(runtime_config_t *config) {
    if (config == NULL) return;
    for (size_t i = 0; i < config->custom_pattern_count; ++i) {
        cleanup_field_pattern(&config->custom_patterns[i]);
    }
    free(config->custom_patterns);
    config->custom_patterns = NULL;
    config->custom_pattern_count = 0;
    for (size_t i = 0; i < config->custom_section_count; ++i) {
        cleanup_section_descriptor(&config->custom_sections[i]);
    }
    free(config->custom_sections);
    config->custom_sections = NULL;
    config->custom_section_count = 0;
    for (size_t i = 0; i < config->custom_event_mapping_count; ++i) {
        cleanup_event_field_mapping(&config->custom_event_mappings[i]);
    }
    free(config->custom_event_mappings);
    config->custom_event_mappings = NULL;
    config->custom_event_mapping_count = 0;
    free(config->config_file);
    config->config_file = NULL;
}

static void cleanup_event_field_mapping(event_field_mapping_t *mapping) {
    if (mapping == NULL) return;
    if (mapping->patterns != NULL) {
        for (size_t i = 0; i < mapping->pattern_count; ++i) {
            cleanup_field_pattern(&mapping->patterns[i]);
        }
        free(mapping->patterns);
    }
    mapping->patterns = NULL;
    mapping->pattern_count = 0;
}

static rsRetVal copy_section_descriptor(section_descriptor_t *dst, const section_descriptor_t *src) {
    if (dst == NULL || src == NULL) return RS_RET_INVALID_PARAMS;
    memset(dst, 0, sizeof(*dst));
    if (src->pattern != NULL) {
        dst->pattern = strdup(src->pattern);
        if (dst->pattern == NULL) return RS_RET_OUT_OF_MEMORY;
    }
    if (src->canonical != NULL) {
        dst->canonical = strdup(src->canonical);
        if (dst->canonical == NULL) {
            cleanup_section_descriptor(dst);
            return RS_RET_OUT_OF_MEMORY;
        }
    }
    dst->behavior = src->behavior;
    dst->flags = src->flags;
    dst->priority = src->priority;
    dst->sensitivity = src->sensitivity;
    return RS_RET_OK;
}

static rsRetVal copy_field_pattern(field_pattern_t *dst, const field_pattern_t *src) {
    if (dst == NULL || src == NULL) return RS_RET_INVALID_PARAMS;
    memset(dst, 0, sizeof(*dst));
    if (src->pattern != NULL) {
        dst->pattern = strdup(src->pattern);
        if (dst->pattern == NULL) return RS_RET_OUT_OF_MEMORY;
    }
    if (src->canonical != NULL) {
        dst->canonical = strdup(src->canonical);
        if (dst->canonical == NULL) {
            cleanup_field_pattern(dst);
            return RS_RET_OUT_OF_MEMORY;
        }
    }
    if (src->section != NULL) {
        dst->section = strdup(src->section);
        if (dst->section == NULL) {
            cleanup_field_pattern(dst);
            return RS_RET_OUT_OF_MEMORY;
        }
    }
    dst->value_type = src->value_type;
    dst->priority = src->priority;
    dst->sensitivity = src->sensitivity;
    return RS_RET_OK;
}

static rsRetVal copy_event_mapping(event_mapping_t *dst, const event_mapping_t *src) {
    if (dst == NULL || src == NULL) return RS_RET_INVALID_PARAMS;
    memset(dst, 0, sizeof(*dst));
    dst->event_id = src->event_id;
    if (src->category != NULL) {
        dst->category = strdup(src->category);
        if (dst->category == NULL) return RS_RET_OUT_OF_MEMORY;
    }
    if (src->subtype != NULL) {
        dst->subtype = strdup(src->subtype);
        if (dst->subtype == NULL) {
            cleanup_event_mapping(dst);
            return RS_RET_OUT_OF_MEMORY;
        }
    }
    if (src->outcome != NULL) {
        dst->outcome = strdup(src->outcome);
        if (dst->outcome == NULL) {
            cleanup_event_mapping(dst);
            return RS_RET_OUT_OF_MEMORY;
        }
    }
    return RS_RET_OK;
}

static rsRetVal copy_event_field_mapping(event_field_mapping_t *dst, const event_field_mapping_t *src) {
    if (dst == NULL || src == NULL) return RS_RET_INVALID_PARAMS;
    memset(dst, 0, sizeof(*dst));
    dst->event_id = src->event_id;
    dst->required_flags = src->required_flags;
    if (src->pattern_count == 0) return RS_RET_OK;
    dst->patterns = calloc(src->pattern_count, sizeof(field_pattern_t));
    if (dst->patterns == NULL) return RS_RET_OUT_OF_MEMORY;
    dst->pattern_count = src->pattern_count;
    for (size_t i = 0; i < src->pattern_count; ++i) {
        rsRetVal r = copy_field_pattern(&dst->patterns[i], &src->patterns[i]);
        if (r != RS_RET_OK) {
            dst->pattern_count = i;
            cleanup_event_field_mapping(dst);
            return r;
        }
    }
    return RS_RET_OK;
}

static rsRetVal append_section_descriptor_owned(section_descriptor_t **array,
                                                size_t *count,
                                                section_descriptor_t *desc) {
    section_descriptor_t *tmp;
    if (array == NULL || count == NULL || desc == NULL) return RS_RET_INVALID_PARAMS;
    tmp = realloc(*array, (*count + 1) * sizeof(section_descriptor_t));
    if (tmp == NULL) {
        cleanup_section_descriptor(desc);
        return RS_RET_OUT_OF_MEMORY;
    }
    tmp[*count] = *desc;
    *array = tmp;
    ++(*count);
    memset(desc, 0, sizeof(*desc));
    return RS_RET_OK;
}

static rsRetVal append_field_pattern_owned(field_pattern_t **array, size_t *count, field_pattern_t *pattern) {
    field_pattern_t *tmp;
    if (array == NULL || count == NULL || pattern == NULL) return RS_RET_INVALID_PARAMS;
    tmp = realloc(*array, (*count + 1) * sizeof(field_pattern_t));
    if (tmp == NULL) {
        cleanup_field_pattern(pattern);
        return RS_RET_OUT_OF_MEMORY;
    }
    tmp[*count] = *pattern;
    *array = tmp;
    ++(*count);
    memset(pattern, 0, sizeof(*pattern));
    return RS_RET_OK;
}

static rsRetVal append_event_field_mapping_owned(event_field_mapping_t **array,
                                                 size_t *count,
                                                 event_field_mapping_t *mapping) {
    event_field_mapping_t *tmp;
    if (array == NULL || count == NULL || mapping == NULL) return RS_RET_INVALID_PARAMS;
    tmp = realloc(*array, (*count + 1) * sizeof(event_field_mapping_t));
    if (tmp == NULL) {
        cleanup_event_field_mapping(mapping);
        return RS_RET_OUT_OF_MEMORY;
    }
    tmp[*count] = *mapping;
    *array = tmp;
    ++(*count);
    memset(mapping, 0, sizeof(*mapping));
    return RS_RET_OK;
}

static rsRetVal append_event_mapping_owned(event_mapping_t **array, size_t *count, event_mapping_t *mapping) {
    event_mapping_t *tmp;
    if (array == NULL || count == NULL || mapping == NULL) return RS_RET_INVALID_PARAMS;
    tmp = realloc(*array, (*count + 1) * sizeof(event_mapping_t));
    if (tmp == NULL) {
        cleanup_event_mapping(mapping);
        return RS_RET_OUT_OF_MEMORY;
    }
    tmp[*count] = *mapping;
    *array = tmp;
    ++(*count);
    memset(mapping, 0, sizeof(*mapping));
    return RS_RET_OK;
}

static event_field_mapping_t *find_event_field_mapping(instanceData *pData, int eventId) {
    if (pData == NULL) return NULL;
    for (size_t i = 0; i < pData->eventFieldMappingCount; ++i) {
        if (pData->eventFieldMappings[i].event_id == eventId) return &pData->eventFieldMappings[i];
    }
    return NULL;
}

static event_mapping_t *find_event_mapping(instanceData *pData, int eventId) {
    if (pData == NULL) return NULL;
    for (size_t i = 0; i < pData->eventMappingCount; ++i) {
        if (pData->eventMappings[i].event_id == eventId) return &pData->eventMappings[i];
    }
    return NULL;
}

static rsRetVal append_field_pattern_to_mapping(event_field_mapping_t *mapping, field_pattern_t *pattern) {
    field_pattern_t *tmp;
    if (mapping == NULL || pattern == NULL) return RS_RET_INVALID_PARAMS;
    tmp = realloc(mapping->patterns, (mapping->pattern_count + 1) * sizeof(field_pattern_t));
    if (tmp == NULL) {
        cleanup_field_pattern(pattern);
        return RS_RET_OUT_OF_MEMORY;
    }
    tmp[mapping->pattern_count] = *pattern;
    mapping->patterns = tmp;
    ++mapping->pattern_count;
    memset(pattern, 0, sizeof(*pattern));
    return RS_RET_OK;
}

static rsRetVal merge_event_field_mapping(instanceData *pData, event_field_mapping_t *mapping) {
    event_field_mapping_t *existing;
    rsRetVal r;
    if (pData == NULL || mapping == NULL) return RS_RET_INVALID_PARAMS;
    existing = find_event_field_mapping(pData, mapping->event_id);
    if (existing != NULL) {
        if (mapping->required_flags != 0) existing->required_flags |= mapping->required_flags;
        for (size_t i = 0; i < mapping->pattern_count; ++i) {
            r = append_field_pattern_to_mapping(existing, &mapping->patterns[i]);
            if (r != RS_RET_OK) {
                for (size_t j = i; j < mapping->pattern_count; ++j) cleanup_field_pattern(&mapping->patterns[j]);
                free(mapping->patterns);
                mapping->patterns = NULL;
                mapping->pattern_count = 0;
                return r;
            }
        }
        free(mapping->patterns);
        mapping->patterns = NULL;
        mapping->pattern_count = 0;
        return RS_RET_OK;
    }
    return append_event_field_mapping_owned(&pData->eventFieldMappings, &pData->eventFieldMappingCount, mapping);
}

static rsRetVal merge_event_mapping(instanceData *pData, event_mapping_t *mapping) {
    event_mapping_t *existing;
    if (pData == NULL || mapping == NULL) return RS_RET_INVALID_PARAMS;
    existing = find_event_mapping(pData, mapping->event_id);
    if (existing != NULL) {
        cleanup_event_mapping(existing);
        *existing = *mapping;
        memset(mapping, 0, sizeof(*mapping));
        return RS_RET_OK;
    }
    return append_event_mapping_owned(&pData->eventMappings, &pData->eventMappingCount, mapping);
}

static rsRetVal initialize_runtime_tables(instanceData *pData) {
    section_descriptor_t descCopy;
    field_pattern_t patternCopy;
    event_field_mapping_t mappingCopy;
    event_mapping_t eventCopy;
    rsRetVal r;
    if (pData == NULL) return RS_RET_INVALID_PARAMS;
    for (size_t i = 0; i < ARRAY_SIZE(g_builtinSectionDescriptors); ++i) {
        r = copy_section_descriptor(&descCopy, &g_builtinSectionDescriptors[i]);
        if (r != RS_RET_OK) {
            free_runtime_tables(pData);
            return r;
        }
        r = append_section_descriptor_owned(&pData->sectionDescriptors, &pData->sectionDescriptorCount, &descCopy);
        if (r != RS_RET_OK) {
            cleanup_section_descriptor(&descCopy);
            free_runtime_tables(pData);
            return r;
        }
    }
    for (size_t i = 0; i < ARRAY_SIZE(g_coreFieldPatterns); ++i) {
        r = copy_field_pattern(&patternCopy, &g_coreFieldPatterns[i]);
        if (r != RS_RET_OK) {
            free_runtime_tables(pData);
            return r;
        }
        r = append_field_pattern_owned(&pData->corePatterns, &pData->corePatternCount, &patternCopy);
        if (r != RS_RET_OK) {
            cleanup_field_pattern(&patternCopy);
            free_runtime_tables(pData);
            return r;
        }
    }
    for (size_t i = 0; i < ARRAY_SIZE(g_eventFieldMappings); ++i) {
        r = copy_event_field_mapping(&mappingCopy, &g_eventFieldMappings[i]);
        if (r != RS_RET_OK) {
            free_runtime_tables(pData);
            return r;
        }
        r = append_event_field_mapping_owned(&pData->eventFieldMappings, &pData->eventFieldMappingCount, &mappingCopy);
        if (r != RS_RET_OK) {
            cleanup_event_field_mapping(&mappingCopy);
            free_runtime_tables(pData);
            return r;
        }
    }
    for (size_t i = 0; i < ARRAY_SIZE(g_eventMappings); ++i) {
        r = copy_event_mapping(&eventCopy, &g_eventMappings[i]);
        if (r != RS_RET_OK) {
            free_runtime_tables(pData);
            return r;
        }
        r = append_event_mapping_owned(&pData->eventMappings, &pData->eventMappingCount, &eventCopy);
        if (r != RS_RET_OK) {
            cleanup_event_mapping(&eventCopy);
            free_runtime_tables(pData);
            return r;
        }
    }
    return RS_RET_OK;
}

static void free_runtime_tables(instanceData *pData) {
    if (pData == NULL) return;
    if (pData->sectionDescriptors != NULL) {
        for (size_t i = 0; i < pData->sectionDescriptorCount; ++i) {
            cleanup_section_descriptor(&pData->sectionDescriptors[i]);
        }
        free(pData->sectionDescriptors);
        pData->sectionDescriptors = NULL;
        pData->sectionDescriptorCount = 0;
    }
    if (pData->corePatterns != NULL) {
        for (size_t i = 0; i < pData->corePatternCount; ++i) {
            cleanup_field_pattern(&pData->corePatterns[i]);
        }
        free(pData->corePatterns);
        pData->corePatterns = NULL;
        pData->corePatternCount = 0;
    }
    if (pData->eventFieldMappings != NULL) {
        for (size_t i = 0; i < pData->eventFieldMappingCount; ++i) {
            cleanup_event_field_mapping(&pData->eventFieldMappings[i]);
        }
        free(pData->eventFieldMappings);
        pData->eventFieldMappings = NULL;
        pData->eventFieldMappingCount = 0;
    }
    if (pData->eventMappings != NULL) {
        for (size_t i = 0; i < pData->eventMappingCount; ++i) {
            cleanup_event_mapping(&pData->eventMappings[i]);
        }
        free(pData->eventMappings);
        pData->eventMappings = NULL;
        pData->eventMappingCount = 0;
    }
}

static rsRetVal parse_validation_mode(const char *mode, validation_mode_t *modeOut) {
    if (mode == NULL || modeOut == NULL) return RS_RET_INVALID_PARAMS;
    if (!strcasecmp(mode, "strict")) {
        *modeOut = VALIDATION_STRICT;
        return RS_RET_OK;
    }
    if (!strcasecmp(mode, "moderate") || !strcasecmp(mode, "warn")) {
        *modeOut = VALIDATION_MODERATE;
        return RS_RET_OK;
    }
    if (!strcasecmp(mode, "permissive") || !strcasecmp(mode, "lenient") || !strcasecmp(mode, "default")) {
        *modeOut = VALIDATION_PERMISSIVE;
        return RS_RET_OK;
    }
    LogError(0, RS_RET_INVALID_PARAMS, "mmsnareparse: unknown validation.mode '%s'", mode);
    return RS_RET_INVALID_PARAMS;
}

static rsRetVal set_validation_mode(instanceData *pData, const char *mode) {
    validation_mode_t parsedMode;
    rsRetVal r;
    if (pData == NULL || mode == NULL) return RS_RET_INVALID_PARAMS;
    r = parse_validation_mode(mode, &parsedMode);
    if (r != RS_RET_OK) return r;
    pData->validationTemplate.mode = parsedMode;
    switch (parsedMode) {
        case VALIDATION_STRICT:
            pData->validationTemplate.log_parsing_errors = true;
            pData->validationTemplate.enable_debug = true;
            pData->validationTemplate.continue_on_error = false;
            pData->validationTemplate.max_errors_before_fail = MAX_PARSING_ERRORS / 2u;
            if (pData->validationTemplate.max_errors_before_fail == 0)
                pData->validationTemplate.max_errors_before_fail = 1;
            break;
        case VALIDATION_MODERATE:
            pData->validationTemplate.log_parsing_errors = true;
            pData->validationTemplate.enable_debug = false;
            pData->validationTemplate.continue_on_error = true;
            pData->validationTemplate.max_errors_before_fail = MAX_PARSING_ERRORS;
            break;
        case VALIDATION_PERMISSIVE:
        default:
            pData->validationTemplate.log_parsing_errors = false;
            pData->validationTemplate.enable_debug = false;
            pData->validationTemplate.continue_on_error = true;
            pData->validationTemplate.max_errors_before_fail = MAX_PARSING_ERRORS;
            break;
    }
    return RS_RET_OK;
}

static rsRetVal read_text_file(const char *path, char **out) {
    FILE *fp;
    long len;
    size_t readLen;
    char *buf;
    if (out == NULL || path == NULL) return RS_RET_INVALID_PARAMS;
    *out = NULL;
    fp = fopen(path, "rb");
    if (fp == NULL) {
        LogError(errno, RS_RET_IO_ERROR, "mmsnareparse: failed to open definition file '%s'", path);
        return RS_RET_IO_ERROR;
    }
    if (fseek(fp, 0, SEEK_END) != 0) {
        LogError(errno, RS_RET_IO_ERROR, "mmsnareparse: failed to seek definition file '%s'", path);
        fclose(fp);
        return RS_RET_IO_ERROR;
    }
    len = ftell(fp);
    if (len < 0) {
        LogError(errno, RS_RET_IO_ERROR, "mmsnareparse: failed to determine size of definition file '%s'", path);
        fclose(fp);
        return RS_RET_IO_ERROR;
    }
    if (fseek(fp, 0, SEEK_SET) != 0) {
        LogError(errno, RS_RET_IO_ERROR, "mmsnareparse: failed to rewind definition file '%s'", path);
        fclose(fp);
        return RS_RET_IO_ERROR;
    }
    buf = malloc((size_t)len + 1u);
    if (buf == NULL) {
        fclose(fp);
        return RS_RET_OUT_OF_MEMORY;
    }
    readLen = fread(buf, 1, (size_t)len, fp);
    if (readLen != (size_t)len) {
        if (ferror(fp)) {
            LogError(errno, RS_RET_IO_ERROR, "mmsnareparse: error reading definition file '%s'", path);
        } else {
            LogError(0, RS_RET_IO_ERROR, "mmsnareparse: unexpected end of file reading definition file '%s'", path);
        }
        free(buf);
        fclose(fp);
        return RS_RET_IO_ERROR;
    }
    buf[len] = '\0';
    fclose(fp);
    *out = buf;
    return RS_RET_OK;
}

static rsRetVal load_configuration(runtime_config_t *config, const char *config_file) {
    if (config == NULL) return RS_RET_INVALID_PARAMS;
    if (config_file == NULL) return RS_RET_OK;

    char *text = NULL;
    rsRetVal r = read_text_file(config_file, &text);
    if (r != RS_RET_OK) return r;

    struct json_tokener *tokener = json_tokener_new();
    if (tokener == NULL) {
        free(text);
        return RS_RET_OUT_OF_MEMORY;
    }
    struct json_object *root = json_tokener_parse_ex(tokener, text, (int)strlen(text));
    enum json_tokener_error err = fjson_tokener_get_error(tokener);
    json_tokener_free(tokener);
    if (err != fjson_tokener_success || root == NULL) {
        free(text);
        if (root != NULL) json_object_put(root);
        return RS_RET_COULD_NOT_PARSE;
    }
    if (!json_object_is_type(root, json_type_object)) {
        json_object_put(root);
        free(text);
        return RS_RET_INVALID_PARAMS;
    }

    free_runtime_config(config);
    init_runtime_config(config);
    config->config_file = strdup(config_file);
    if (config->config_file == NULL) {
        json_object_put(root);
        free(text);
        return RS_RET_OUT_OF_MEMORY;
    }

    struct json_object *options;
    if (json_object_object_get_ex(root, "options", &options) && json_object_is_type(options, json_type_object)) {
        struct json_object *opt;
        if (json_object_object_get_ex(options, "enable_debug", &opt) && json_object_is_type(opt, json_type_boolean))
            config->enable_debug = json_object_get_boolean(opt) ? true : false;
        if (json_object_object_get_ex(options, "enable_fallback", &opt) && json_object_is_type(opt, json_type_boolean))
            config->enable_fallback = json_object_get_boolean(opt) ? true : false;
    }

    struct json_object *sections;
    if (json_object_object_get_ex(root, "sections", &sections)) {
        if (!json_object_is_type(sections, json_type_array)) {
            LogError(0, RS_RET_INVALID_PARAMS, "mmsnareparse: runtime config sections must be an array");
        } else {
            size_t len = json_object_array_length(sections);
            for (size_t i = 0; i < len; ++i) {
                struct json_object *entry = json_object_array_get_idx(sections, (int)i);
                if (!json_object_is_type(entry, json_type_object)) {
                    LogError(0, RS_RET_INVALID_PARAMS, "mmsnareparse: runtime config section entry must be object");
                    continue;
                }
                section_descriptor_t desc;
                memset(&desc, 0, sizeof(desc));
                desc.behavior = sectionBehaviorStandard;
                desc.priority = SECTION_PRIORITY_DEFAULT;
                desc.sensitivity = fieldSensitivityCaseSensitive;

                struct json_object *value;
                if (!json_object_object_get_ex(entry, "pattern", &value) ||
                    !json_object_is_type(value, json_type_string)) {
                    LogError(0, RS_RET_INVALID_PARAMS, "mmsnareparse: runtime config section missing pattern");
                    continue;
                }
                desc.pattern = strdup(json_object_get_string(value));
                if (desc.pattern == NULL) {
                    json_object_put(root);
                    free(text);
                    return RS_RET_OUT_OF_MEMORY;
                }
                if (json_object_object_get_ex(entry, "canonical", &value) &&
                    json_object_is_type(value, json_type_string)) {
                    desc.canonical = strdup(json_object_get_string(value));
                } else {
                    desc.canonical = normalize_label(desc.pattern);
                }
                if (desc.canonical == NULL) {
                    cleanup_section_descriptor(&desc);
                    LogError(0, RS_RET_INVALID_PARAMS, "mmsnareparse: runtime config section missing canonical");
                    continue;
                }
                if (json_object_object_get_ex(entry, "behavior", &value) &&
                    json_object_is_type(value, json_type_string)) {
                    if (!parse_section_behavior_string(json_object_get_string(value), &desc.behavior)) {
                        cleanup_section_descriptor(&desc);
                        LogError(0, RS_RET_INVALID_PARAMS, "mmsnareparse: runtime config unknown section behavior");
                        continue;
                    }
                }
                if (json_object_object_get_ex(entry, "priority", &value) && json_object_is_type(value, json_type_int))
                    desc.priority = json_object_get_int(value);
                if (json_object_object_get_ex(entry, "sensitivity", &value) &&
                    json_object_is_type(value, json_type_string)) {
                    if (!parse_field_sensitivity_string(json_object_get_string(value), &desc.sensitivity)) {
                        cleanup_section_descriptor(&desc);
                        LogError(0, RS_RET_INVALID_PARAMS, "mmsnareparse: runtime config unknown sensitivity");
                        continue;
                    }
                }
                if (json_object_object_get_ex(entry, "flags", &value)) {
                    sbool ok = 0;
                    desc.flags = parse_section_flags_array(value, &ok);
                    if (!ok) {
                        cleanup_section_descriptor(&desc);
                        LogError(0, RS_RET_INVALID_PARAMS, "mmsnareparse: runtime config unknown section flag");
                        continue;
                    }
                }
                r = append_section_descriptor_owned(&config->custom_sections, &config->custom_section_count, &desc);
                if (r != RS_RET_OK) {
                    cleanup_section_descriptor(&desc);
                    json_object_put(root);
                    free(text);
                    return r;
                }
            }
        }
    }

    struct json_object *fields;
    if (json_object_object_get_ex(root, "fields", &fields)) {
        if (!json_object_is_type(fields, json_type_array)) {
            LogError(0, RS_RET_INVALID_PARAMS, "mmsnareparse: runtime config fields must be an array");
        } else {
            size_t len = json_object_array_length(fields);
            for (size_t i = 0; i < len; ++i) {
                struct json_object *entry = json_object_array_get_idx(fields, (int)i);
                if (!json_object_is_type(entry, json_type_object)) {
                    LogError(0, RS_RET_INVALID_PARAMS, "mmsnareparse: runtime config field entry must be object");
                    continue;
                }
                field_pattern_t pattern;
                memset(&pattern, 0, sizeof(pattern));
                pattern.value_type = fieldValueString;
                pattern.priority = FIELD_PRIORITY_BASE + 10;
                pattern.sensitivity = fieldSensitivityCaseSensitive;

                struct json_object *value;
                if (!json_object_object_get_ex(entry, "pattern", &value) ||
                    !json_object_is_type(value, json_type_string)) {
                    LogError(0, RS_RET_INVALID_PARAMS, "mmsnareparse: runtime config field missing pattern");
                    continue;
                }
                pattern.pattern = strdup(json_object_get_string(value));
                if (pattern.pattern == NULL) {
                    json_object_put(root);
                    free(text);
                    return RS_RET_OUT_OF_MEMORY;
                }
                if (json_object_object_get_ex(entry, "canonical", &value) &&
                    json_object_is_type(value, json_type_string)) {
                    pattern.canonical = strdup(json_object_get_string(value));
                } else {
                    pattern.canonical = normalize_label(pattern.pattern);
                }
                if (pattern.canonical == NULL) {
                    cleanup_field_pattern(&pattern);
                    LogError(0, RS_RET_INVALID_PARAMS, "mmsnareparse: runtime config field missing canonical");
                    continue;
                }
                if (json_object_object_get_ex(entry, "section", &value) &&
                    json_object_is_type(value, json_type_string)) {
                    pattern.section = strdup(json_object_get_string(value));
                    if (pattern.section == NULL) {
                        cleanup_field_pattern(&pattern);
                        json_object_put(root);
                        free(text);
                        return RS_RET_OUT_OF_MEMORY;
                    }
                }
                if (json_object_object_get_ex(entry, "priority", &value) && json_object_is_type(value, json_type_int))
                    pattern.priority = json_object_get_int(value);
                if (json_object_object_get_ex(entry, "value_type", &value) &&
                    json_object_is_type(value, json_type_string)) {
                    if (!parse_field_value_type_string(json_object_get_string(value), &pattern.value_type)) {
                        cleanup_field_pattern(&pattern);
                        LogError(0, RS_RET_INVALID_PARAMS, "mmsnareparse: runtime config unknown field value type");
                        continue;
                    }
                }
                if (json_object_object_get_ex(entry, "sensitivity", &value) &&
                    json_object_is_type(value, json_type_string)) {
                    if (!parse_field_sensitivity_string(json_object_get_string(value), &pattern.sensitivity)) {
                        cleanup_field_pattern(&pattern);
                        LogError(0, RS_RET_INVALID_PARAMS, "mmsnareparse: runtime config unknown sensitivity");
                        continue;
                    }
                }
                r = append_field_pattern_owned(&config->custom_patterns, &config->custom_pattern_count, &pattern);
                if (r != RS_RET_OK) {
                    cleanup_field_pattern(&pattern);
                    json_object_put(root);
                    free(text);
                    return r;
                }
            }
        }
    }

    struct json_object *eventFields;
    if (json_object_object_get_ex(root, "eventFields", &eventFields)) {
        if (!json_object_is_type(eventFields, json_type_array)) {
            LogError(0, RS_RET_INVALID_PARAMS, "mmsnareparse: runtime config eventFields must be an array");
        } else {
            size_t len = json_object_array_length(eventFields);
            for (size_t i = 0; i < len; ++i) {
                struct json_object *entry = json_object_array_get_idx(eventFields, (int)i);
                if (!json_object_is_type(entry, json_type_object)) {
                    LogError(0, RS_RET_INVALID_PARAMS, "mmsnareparse: runtime config eventFields entry must be object");
                    continue;
                }
                event_field_mapping_t mapping;
                memset(&mapping, 0, sizeof(mapping));
                struct json_object *value;
                if (!json_object_object_get_ex(entry, "event_id", &value) ||
                    !json_object_is_type(value, json_type_int)) {
                    LogError(0, RS_RET_INVALID_PARAMS, "mmsnareparse: runtime config eventFields missing event_id");
                    continue;
                }
                mapping.event_id = json_object_get_int(value);
                if (json_object_object_get_ex(entry, "required_flags", &value)) {
                    sbool ok = 0;
                    mapping.required_flags = parse_section_flags_array(value, &ok);
                    if (!ok) {
                        LogError(0, RS_RET_INVALID_PARAMS, "mmsnareparse: runtime config unknown required flag");
                        continue;
                    }
                }
                if (!json_object_object_get_ex(entry, "patterns", &value) ||
                    !json_object_is_type(value, json_type_array)) {
                    LogError(0, RS_RET_INVALID_PARAMS, "mmsnareparse: runtime config eventFields missing patterns");
                    continue;
                }
                size_t plen = json_object_array_length(value);
                for (size_t j = 0; j < plen; ++j) {
                    struct json_object *patternObj = json_object_array_get_idx(value, (int)j);
                    if (!json_object_is_type(patternObj, json_type_object)) {
                        LogError(0, RS_RET_INVALID_PARAMS, "mmsnareparse: runtime config pattern entry must be object");
                        continue;
                    }
                    field_pattern_t patternEntry;
                    memset(&patternEntry, 0, sizeof(patternEntry));
                    patternEntry.value_type = fieldValueString;
                    patternEntry.priority = FIELD_PRIORITY_EVENT_OVERRIDE;
                    patternEntry.sensitivity = fieldSensitivityCaseSensitive;
                    struct json_object *pv;
                    if (!json_object_object_get_ex(patternObj, "pattern", &pv) ||
                        !json_object_is_type(pv, json_type_string)) {
                        LogError(0, RS_RET_INVALID_PARAMS, "mmsnareparse: runtime config pattern missing pattern");
                        continue;
                    }
                    patternEntry.pattern = strdup(json_object_get_string(pv));
                    if (patternEntry.pattern == NULL) {
                        cleanup_event_field_mapping(&mapping);
                        json_object_put(root);
                        free(text);
                        return RS_RET_OUT_OF_MEMORY;
                    }
                    if (json_object_object_get_ex(patternObj, "canonical", &pv) &&
                        json_object_is_type(pv, json_type_string)) {
                        patternEntry.canonical = strdup(json_object_get_string(pv));
                    } else {
                        patternEntry.canonical = normalize_label(patternEntry.pattern);
                    }
                    if (patternEntry.canonical == NULL) {
                        cleanup_field_pattern(&patternEntry);
                        LogError(0, RS_RET_INVALID_PARAMS, "mmsnareparse: runtime config pattern missing canonical");
                        continue;
                    }
                    if (json_object_object_get_ex(patternObj, "section", &pv) &&
                        json_object_is_type(pv, json_type_string)) {
                        patternEntry.section = strdup(json_object_get_string(pv));
                        if (patternEntry.section == NULL) {
                            cleanup_field_pattern(&patternEntry);
                            cleanup_event_field_mapping(&mapping);
                            json_object_put(root);
                            free(text);
                            return RS_RET_OUT_OF_MEMORY;
                        }
                    }
                    if (json_object_object_get_ex(patternObj, "priority", &pv) &&
                        json_object_is_type(pv, json_type_int))
                        patternEntry.priority = json_object_get_int(pv);
                    if (json_object_object_get_ex(patternObj, "value_type", &pv) &&
                        json_object_is_type(pv, json_type_string)) {
                        if (!parse_field_value_type_string(json_object_get_string(pv), &patternEntry.value_type)) {
                            cleanup_field_pattern(&patternEntry);
                            LogError(0, RS_RET_INVALID_PARAMS,
                                     "mmsnareparse: runtime config pattern unknown value type");
                            continue;
                        }
                    }
                    if (json_object_object_get_ex(patternObj, "sensitivity", &pv) &&
                        json_object_is_type(pv, json_type_string)) {
                        if (!parse_field_sensitivity_string(json_object_get_string(pv), &patternEntry.sensitivity)) {
                            cleanup_field_pattern(&patternEntry);
                            LogError(0, RS_RET_INVALID_PARAMS,
                                     "mmsnareparse: runtime config pattern unknown sensitivity");
                            continue;
                        }
                    }
                    r = append_field_pattern_owned(&mapping.patterns, &mapping.pattern_count, &patternEntry);
                    if (r != RS_RET_OK) {
                        cleanup_field_pattern(&patternEntry);
                        cleanup_event_field_mapping(&mapping);
                        json_object_put(root);
                        free(text);
                        return r;
                    }
                }
                r = append_event_field_mapping_owned(&config->custom_event_mappings,
                                                     &config->custom_event_mapping_count, &mapping);
                if (r != RS_RET_OK) {
                    cleanup_event_field_mapping(&mapping);
                    json_object_put(root);
                    free(text);
                    return r;
                }
            }
        }
    }

    json_object_put(root);
    free(text);
    return RS_RET_OK;
}

static rsRetVal apply_runtime_configuration(instanceData *pData, const runtime_config_t *config) {
    if (pData == NULL || config == NULL) return RS_RET_INVALID_PARAMS;

    rsRetVal r;
    for (size_t i = 0; i < config->custom_section_count; ++i) {
        section_descriptor_t descCopy;
        r = copy_section_descriptor(&descCopy, &config->custom_sections[i]);
        if (r != RS_RET_OK) return r;
        r = append_section_descriptor_owned(&pData->sectionDescriptors, &pData->sectionDescriptorCount, &descCopy);
        if (r != RS_RET_OK) {
            cleanup_section_descriptor(&descCopy);
            return r;
        }
    }
    for (size_t i = 0; i < config->custom_pattern_count; ++i) {
        field_pattern_t patternCopy;
        r = copy_field_pattern(&patternCopy, &config->custom_patterns[i]);
        if (r != RS_RET_OK) return r;
        r = append_field_pattern_owned(&pData->corePatterns, &pData->corePatternCount, &patternCopy);
        if (r != RS_RET_OK) {
            cleanup_field_pattern(&patternCopy);
            return r;
        }
    }
    for (size_t i = 0; i < config->custom_event_mapping_count; ++i) {
        event_field_mapping_t mappingCopy;
        r = copy_event_field_mapping(&mappingCopy, &config->custom_event_mappings[i]);
        if (r != RS_RET_OK) return r;
        r = merge_event_field_mapping(pData, &mappingCopy);
        if (r != RS_RET_OK) {
            cleanup_event_field_mapping(&mappingCopy);
            return r;
        }
    }

    if (config->enable_debug) pData->validationTemplate.enable_debug = true;
    pData->runtimeConfig.enable_debug = config->enable_debug;
    pData->runtimeConfig.enable_fallback = config->enable_fallback;
    return RS_RET_OK;
}

static rsRetVal save_configuration(const runtime_config_t *config, const char *config_file) {
    if (config == NULL || config_file == NULL) return RS_RET_OK;

    struct json_object *root = json_object_new_object();
    if (root == NULL) return RS_RET_OUT_OF_MEMORY;

    struct json_object *options = json_object_new_object();
    if (options != NULL) {
        json_object_object_add(options, "enable_debug", json_object_new_boolean(config->enable_debug ? 1 : 0));
        json_object_object_add(options, "enable_fallback", json_object_new_boolean(config->enable_fallback ? 1 : 0));
        json_object_object_add(root, "options", options);
    }

    if (config->custom_section_count > 0) {
        struct json_object *arr = json_object_new_array();
        if (arr == NULL) {
            json_object_put(root);
            return RS_RET_OUT_OF_MEMORY;
        }
        for (size_t i = 0; i < config->custom_section_count; ++i) {
            const section_descriptor_t *src = &config->custom_sections[i];
            struct json_object *obj = json_object_new_object();
            if (obj == NULL) {
                json_object_put(arr);
                json_object_put(root);
                return RS_RET_OUT_OF_MEMORY;
            }
            json_object_object_add(obj, "pattern", json_object_new_string(src->pattern));
            if (src->canonical != NULL)
                json_object_object_add(obj, "canonical", json_object_new_string(src->canonical));
            json_object_object_add(obj, "behavior", json_object_new_string(section_behavior_to_string(src->behavior)));
            json_object_object_add(obj, "priority", json_object_new_int(src->priority));
            json_object_object_add(obj, "sensitivity",
                                   json_object_new_string(field_sensitivity_to_string(src->sensitivity)));
            struct json_object *flags = serialize_section_flags(src->flags);
            if (flags != NULL) json_object_object_add(obj, "flags", flags);
            json_object_array_add(arr, obj);
        }
        json_object_object_add(root, "sections", arr);
    }

    if (config->custom_pattern_count > 0) {
        struct json_object *arr = json_object_new_array();
        if (arr == NULL) {
            json_object_put(root);
            return RS_RET_OUT_OF_MEMORY;
        }
        for (size_t i = 0; i < config->custom_pattern_count; ++i) {
            const field_pattern_t *src = &config->custom_patterns[i];
            struct json_object *obj = json_object_new_object();
            if (obj == NULL) {
                json_object_put(arr);
                json_object_put(root);
                return RS_RET_OUT_OF_MEMORY;
            }
            json_object_object_add(obj, "pattern", json_object_new_string(src->pattern));
            if (src->canonical != NULL)
                json_object_object_add(obj, "canonical", json_object_new_string(src->canonical));
            if (src->section != NULL) json_object_object_add(obj, "section", json_object_new_string(src->section));
            json_object_object_add(obj, "priority", json_object_new_int(src->priority));
            json_object_object_add(obj, "value_type",
                                   json_object_new_string(field_value_type_to_string(src->value_type)));
            json_object_object_add(obj, "sensitivity",
                                   json_object_new_string(field_sensitivity_to_string(src->sensitivity)));
            json_object_array_add(arr, obj);
        }
        json_object_object_add(root, "fields", arr);
    }

    if (config->custom_event_mapping_count > 0) {
        struct json_object *arr = json_object_new_array();
        if (arr == NULL) {
            json_object_put(root);
            return RS_RET_OUT_OF_MEMORY;
        }
        for (size_t i = 0; i < config->custom_event_mapping_count; ++i) {
            const event_field_mapping_t *mapping = &config->custom_event_mappings[i];
            struct json_object *obj = json_object_new_object();
            if (obj == NULL) {
                json_object_put(arr);
                json_object_put(root);
                return RS_RET_OUT_OF_MEMORY;
            }
            json_object_object_add(obj, "event_id", json_object_new_int(mapping->event_id));
            struct json_object *flags = serialize_section_flags(mapping->required_flags);
            if (flags != NULL) json_object_object_add(obj, "required_flags", flags);
            struct json_object *patterns = json_object_new_array();
            if (patterns == NULL) {
                json_object_put(obj);
                json_object_put(arr);
                json_object_put(root);
                return RS_RET_OUT_OF_MEMORY;
            }
            for (size_t j = 0; j < mapping->pattern_count; ++j) {
                const field_pattern_t *src = &mapping->patterns[j];
                struct json_object *pobj = json_object_new_object();
                if (pobj == NULL) {
                    json_object_put(patterns);
                    json_object_put(obj);
                    json_object_put(arr);
                    json_object_put(root);
                    return RS_RET_OUT_OF_MEMORY;
                }
                json_object_object_add(pobj, "pattern", json_object_new_string(src->pattern));
                if (src->canonical != NULL)
                    json_object_object_add(pobj, "canonical", json_object_new_string(src->canonical));
                if (src->section != NULL) json_object_object_add(pobj, "section", json_object_new_string(src->section));
                json_object_object_add(pobj, "priority", json_object_new_int(src->priority));
                json_object_object_add(pobj, "value_type",
                                       json_object_new_string(field_value_type_to_string(src->value_type)));
                json_object_object_add(pobj, "sensitivity",
                                       json_object_new_string(field_sensitivity_to_string(src->sensitivity)));
                json_object_array_add(patterns, pobj);
            }
            json_object_object_add(obj, "patterns", patterns);
            json_object_array_add(arr, obj);
        }
        json_object_object_add(root, "eventFields", arr);
    }

    const char *jsonText = json_object_to_json_string_ext(root, JSON_C_TO_STRING_PRETTY);
    FILE *fp = fopen(config_file, "w");
    if (fp == NULL) {
        int savedErrno = errno;
        json_object_put(root);
        LogError(savedErrno, RS_RET_IO_ERROR, "mmsnareparse: failed to open runtime config '%s'", config_file);
        return RS_RET_IO_ERROR;
    }
    size_t len = strlen(jsonText);
    size_t written = fwrite(jsonText, 1, len, fp);
    if (written != len) {
        int savedErrno = errno;
        fclose(fp);
        json_object_put(root);
        LogError(savedErrno, RS_RET_IO_ERROR, "mmsnareparse: failed to write runtime config '%s'", config_file);
        return RS_RET_IO_ERROR;
    }
    fclose(fp);
    json_object_put(root);
    return RS_RET_OK;
}
static rsRetVal report_validation_issue(instanceData *pData, const char *source, const char *message) {
    if (pData == NULL) return RS_RET_INVALID_PARAMS;
    validation_context_t *ctx = &pData->validationTemplate;
    switch (ctx->mode) {
        case VALIDATION_STRICT:
            LogError(0, RS_RET_INVALID_PARAMS, "mmsnareparse: %s rejected: %s", source, message);
            return RS_RET_INVALID_PARAMS;
        case VALIDATION_MODERATE:
            if (ctx->log_parsing_errors)
                LogError(0, RS_RET_INVALID_PARAMS, "mmsnareparse: %s warning: %s", source, message);
            return RS_RET_OK;
        case VALIDATION_PERMISSIVE:
        default:
            if (ctx->enable_debug)
                LogError(0, RS_RET_INVALID_PARAMS, "mmsnareparse: %s ignored (permissive mode): %s", source, message);
            return RS_RET_OK;
    }
    return RS_RET_OK;
}

static sbool parse_section_behavior_string(const char *text, section_behavior_t *behavior) {
    if (text == NULL || behavior == NULL) return 0;
    if (!strcasecmp(text, "standard")) {
        *behavior = sectionBehaviorStandard;
        return 1;
    }
    if (!strcasecmp(text, "inline") || !strcasecmp(text, "inline_value") || !strcasecmp(text, "inline-value")) {
        *behavior = sectionBehaviorInlineValue;
        return 1;
    }
    if (!strcasecmp(text, "semicolon") || !strcasecmp(text, "kv")) {
        *behavior = sectionBehaviorSemicolon;
        return 1;
    }
    if (!strcasecmp(text, "list")) {
        *behavior = sectionBehaviorList;
        return 1;
    }
    return 0;
}

static sbool parse_field_value_type_string(const char *text, field_value_type_t *type) {
    if (text == NULL || type == NULL) return 0;
    if (!strcasecmp(text, "string")) {
        *type = fieldValueString;
        return 1;
    }
    if (!strcasecmp(text, "int64") || !strcasecmp(text, "integer")) {
        *type = fieldValueInt64;
        return 1;
    }
    if (!strcasecmp(text, "int64_with_raw") || !strcasecmp(text, "int64-with-raw")) {
        *type = fieldValueInt64WithRaw;
        return 1;
    }
    if (!strcasecmp(text, "bool") || !strcasecmp(text, "boolean")) {
        *type = fieldValueBool;
        return 1;
    }
    if (!strcasecmp(text, "json")) {
        *type = fieldValueJson;
        return 1;
    }
    if (!strcasecmp(text, "logon_type") || !strcasecmp(text, "logon-type")) {
        *type = fieldValueLogonType;
        return 1;
    }
    if (!strcasecmp(text, "remote_credential_guard") || !strcasecmp(text, "remote-credential-guard")) {
        *type = fieldValueRemoteCredentialGuard;
        return 1;
    }
    if (!strcasecmp(text, "guid")) {
        *type = fieldValueGuid;
        return 1;
    }
    if (!strcasecmp(text, "ip") || !strcasecmp(text, "ip_address") || !strcasecmp(text, "ip-address")) {
        *type = fieldValueIpAddress;
        return 1;
    }
    if (!strcasecmp(text, "timestamp")) {
        *type = fieldValueTimestamp;
        return 1;
    }
    if (!strcasecmp(text, "privilege_list") || !strcasecmp(text, "privilege-list")) {
        *type = fieldValuePrivilegeList;
        return 1;
    }
    return 0;
}

static sbool parse_field_sensitivity_string(const char *text, field_pattern_sensitivity_t *sensitivity) {
    if (text == NULL || sensitivity == NULL) return 0;
    if (!strcasecmp(text, "canonical")) {
        *sensitivity = fieldSensitivityCanonical;
        return 1;
    }
    if (!strcasecmp(text, "case_sensitive") || !strcasecmp(text, "case-sensitive") || !strcasecmp(text, "sensitive")) {
        *sensitivity = fieldSensitivityCaseSensitive;
        return 1;
    }
    if (!strcasecmp(text, "case_insensitive") || !strcasecmp(text, "case-insensitive") ||
        !strcasecmp(text, "insensitive")) {
        *sensitivity = fieldSensitivityCaseInsensitive;
        return 1;
    }
    return 0;
}

static const char *section_behavior_to_string(section_behavior_t behavior) {
    switch (behavior) {
        case sectionBehaviorStandard:
            return "standard";
        case sectionBehaviorInlineValue:
            return "inline";
        case sectionBehaviorSemicolon:
            return "semicolon";
        case sectionBehaviorList:
            return "list";
        default:
            return "standard";
    }
}

static const char *field_value_type_to_string(field_value_type_t type) {
    switch (type) {
        case fieldValueString:
            return "string";
        case fieldValueInt64:
            return "int64";
        case fieldValueInt64WithRaw:
            return "int64_with_raw";
        case fieldValueBool:
            return "bool";
        case fieldValueJson:
            return "json";
        case fieldValueLogonType:
            return "logon_type";
        case fieldValueRemoteCredentialGuard:
            return "remote_credential_guard";
        case fieldValueGuid:
            return "guid";
        case fieldValueIpAddress:
            return "ip_address";
        case fieldValueTimestamp:
            return "timestamp";
        case fieldValuePrivilegeList:
            return "privilege_list";
        default:
            return "string";
    }
}

static const char *field_sensitivity_to_string(field_pattern_sensitivity_t sensitivity) {
    switch (sensitivity) {
        case fieldSensitivityCanonical:
            return "canonical";
        case fieldSensitivityCaseSensitive:
            return "case_sensitive";
        case fieldSensitivityCaseInsensitive:
            return "case_insensitive";
        default:
            return "canonical";
    }
}

static struct json_object *serialize_section_flags(uint32_t flags) {
    struct json_object *arr = json_object_new_array();
    if (arr == NULL) return NULL;
    if (flags & SECTION_FLAG_NETWORK) json_object_array_add(arr, json_object_new_string("network"));
    if (flags & SECTION_FLAG_LAPS) json_object_array_add(arr, json_object_new_string("laps"));
    if (flags & SECTION_FLAG_TLS) json_object_array_add(arr, json_object_new_string("tls"));
    if (flags & SECTION_FLAG_WDAC) json_object_array_add(arr, json_object_new_string("wdac"));
    if (json_object_array_length(arr) == 0) {
        json_object_put(arr);
        return NULL;
    }
    return arr;
}

static uint32_t parse_section_flags_array(struct json_object *value, sbool *ok) {
    uint32_t flags = SECTION_FLAG_NONE;
    if (ok != NULL) *ok = 1;
    if (value == NULL) return flags;
    if (!json_object_is_type(value, json_type_array)) {
        if (ok != NULL) *ok = 0;
        return SECTION_FLAG_NONE;
    }
    size_t len = json_object_array_length(value);
    for (size_t i = 0; i < len; ++i) {
        struct json_object *entry = json_object_array_get_idx(value, (int)i);
        const char *flag = json_object_get_string(entry);
        if (flag == NULL) continue;
        if (!strcasecmp(flag, "network"))
            flags |= SECTION_FLAG_NETWORK;
        else if (!strcasecmp(flag, "laps"))
            flags |= SECTION_FLAG_LAPS;
        else if (!strcasecmp(flag, "tls"))
            flags |= SECTION_FLAG_TLS;
        else if (!strcasecmp(flag, "wdac"))
            flags |= SECTION_FLAG_WDAC;
        else if (ok != NULL)
            *ok = 0;
    }
    return flags;
}

static rsRetVal load_section_definitions(instanceData *pData, struct json_object *array, const char *source) {
    size_t len;
    if (array == NULL) return RS_RET_OK;
    if (!json_object_is_type(array, json_type_array))
        return report_validation_issue(pData, source, "sections must be an array");
    len = json_object_array_length(array);
    for (size_t i = 0; i < len; ++i) {
        struct json_object *entry = json_object_array_get_idx(array, (int)i);
        section_descriptor_t desc;
        struct json_object *value;
        const char *pattern;
        const char *canonical = NULL;
        char context[128];
        rsRetVal r;
        snprintf(context, sizeof(context), "%s sections[%zu]", source, i);
        if (!json_object_is_type(entry, json_type_object)) {
            r = report_validation_issue(pData, context, "entry must be an object");
            if (r != RS_RET_OK) return r;
            continue;
        }
        memset(&desc, 0, sizeof(desc));
        desc.behavior = sectionBehaviorStandard;
        desc.priority = SECTION_PRIORITY_DEFAULT;
        desc.sensitivity = fieldSensitivityCaseSensitive;
        if (!json_object_object_get_ex(entry, "pattern", &value) || !json_object_is_type(value, json_type_string)) {
            r = report_validation_issue(pData, context, "missing pattern");
            if (r != RS_RET_OK) return r;
            continue;
        }
        pattern = json_object_get_string(value);
        desc.pattern = strdup(pattern);
        if (desc.pattern == NULL) return RS_RET_OUT_OF_MEMORY;
        if (json_object_object_get_ex(entry, "canonical", &value) && json_object_is_type(value, json_type_string)) {
            canonical = json_object_get_string(value);
        }
        if (canonical != NULL) {
            desc.canonical = strdup(canonical);
            if (desc.canonical == NULL) {
                cleanup_section_descriptor(&desc);
                return RS_RET_OUT_OF_MEMORY;
            }
        } else {
            char *normalized = normalize_label(pattern);
            if (normalized == NULL) {
                cleanup_section_descriptor(&desc);
                r = report_validation_issue(pData, context, "could not derive canonical name");
                if (r != RS_RET_OK) return r;
                continue;
            }
            desc.canonical = normalized;
        }
        if (json_object_object_get_ex(entry, "behavior", &value) && json_object_is_type(value, json_type_string)) {
            const char *behaviorText = json_object_get_string(value);
            if (!parse_section_behavior_string(behaviorText, &desc.behavior)) {
                cleanup_section_descriptor(&desc);
                r = report_validation_issue(pData, context, "unknown behavior");
                if (r != RS_RET_OK) return r;
                continue;
            }
        }
        if (json_object_object_get_ex(entry, "priority", &value) && json_object_is_type(value, json_type_int)) {
            desc.priority = json_object_get_int(value);
        }
        if (json_object_object_get_ex(entry, "sensitivity", &value) && json_object_is_type(value, json_type_string)) {
            const char *sens = json_object_get_string(value);
            if (!parse_field_sensitivity_string(sens, &desc.sensitivity)) {
                cleanup_section_descriptor(&desc);
                r = report_validation_issue(pData, context, "unknown sensitivity");
                if (r != RS_RET_OK) return r;
                continue;
            }
        }
        if (json_object_object_get_ex(entry, "flags", &value)) {
            sbool ok = 0;
            desc.flags = parse_section_flags_array(value, &ok);
            if (!ok) {
                cleanup_section_descriptor(&desc);
                r = report_validation_issue(pData, context, "unknown flag value");
                if (r != RS_RET_OK) return r;
                continue;
            }
        }
        r = append_section_descriptor_owned(&pData->sectionDescriptors, &pData->sectionDescriptorCount, &desc);
        if (r != RS_RET_OK) {
            cleanup_section_descriptor(&desc);
            return r;
        }
    }
    return RS_RET_OK;
}

static rsRetVal load_field_definitions(instanceData *pData, struct json_object *array, const char *source) {
    size_t len;
    if (array == NULL) return RS_RET_OK;
    if (!json_object_is_type(array, json_type_array))
        return report_validation_issue(pData, source, "fields must be an array");
    len = json_object_array_length(array);
    for (size_t i = 0; i < len; ++i) {
        struct json_object *entry = json_object_array_get_idx(array, (int)i);
        struct json_object *value;
        field_pattern_t pattern;
        const char *patternText;
        const char *canonical = NULL;
        const char *section = NULL;
        char context[128];
        rsRetVal r;
        snprintf(context, sizeof(context), "%s fields[%zu]", source, i);
        if (!json_object_is_type(entry, json_type_object)) {
            r = report_validation_issue(pData, context, "entry must be an object");
            if (r != RS_RET_OK) return r;
            continue;
        }
        memset(&pattern, 0, sizeof(pattern));
        pattern.value_type = fieldValueString;
        pattern.priority = FIELD_PRIORITY_BASE;
        pattern.sensitivity = fieldSensitivityCaseSensitive;
        if (!json_object_object_get_ex(entry, "pattern", &value) || !json_object_is_type(value, json_type_string)) {
            r = report_validation_issue(pData, context, "missing pattern");
            if (r != RS_RET_OK) return r;
            continue;
        }
        patternText = json_object_get_string(value);
        pattern.pattern = strdup(patternText);
        if (pattern.pattern == NULL) return RS_RET_OUT_OF_MEMORY;
        if (json_object_object_get_ex(entry, "canonical", &value) && json_object_is_type(value, json_type_string))
            canonical = json_object_get_string(value);
        if (canonical != NULL) {
            pattern.canonical = strdup(canonical);
            if (pattern.canonical == NULL) {
                cleanup_field_pattern(&pattern);
                return RS_RET_OUT_OF_MEMORY;
            }
        } else {
            char *normalized = normalize_label(patternText);
            if (normalized == NULL) {
                cleanup_field_pattern(&pattern);
                r = report_validation_issue(pData, context, "could not derive canonical name");
                if (r != RS_RET_OK) return r;
                continue;
            }
            pattern.canonical = normalized;
        }
        if (json_object_object_get_ex(entry, "section", &value) && json_object_is_type(value, json_type_string)) {
            section = json_object_get_string(value);
            if (section != NULL) {
                pattern.section = strdup(section);
                if (pattern.section == NULL) {
                    cleanup_field_pattern(&pattern);
                    return RS_RET_OUT_OF_MEMORY;
                }
            }
        }
        if (json_object_object_get_ex(entry, "priority", &value) && json_object_is_type(value, json_type_int))
            pattern.priority = json_object_get_int(value);
        if (json_object_object_get_ex(entry, "value_type", &value) && json_object_is_type(value, json_type_string)) {
            const char *typeText = json_object_get_string(value);
            if (!parse_field_value_type_string(typeText, &pattern.value_type)) {
                cleanup_field_pattern(&pattern);
                r = report_validation_issue(pData, context, "unknown value_type");
                if (r != RS_RET_OK) return r;
                continue;
            }
        }
        if (json_object_object_get_ex(entry, "sensitivity", &value) && json_object_is_type(value, json_type_string)) {
            const char *sens = json_object_get_string(value);
            if (!parse_field_sensitivity_string(sens, &pattern.sensitivity)) {
                cleanup_field_pattern(&pattern);
                r = report_validation_issue(pData, context, "unknown sensitivity");
                if (r != RS_RET_OK) return r;
                continue;
            }
        }
        r = append_field_pattern_owned(&pData->corePatterns, &pData->corePatternCount, &pattern);
        if (r != RS_RET_OK) {
            cleanup_field_pattern(&pattern);
            return r;
        }
    }
    return RS_RET_OK;
}

static rsRetVal load_event_field_definitions(instanceData *pData, struct json_object *array, const char *source) {
    size_t len;
    if (array == NULL) return RS_RET_OK;
    if (!json_object_is_type(array, json_type_array))
        return report_validation_issue(pData, source, "eventFields must be an array");
    len = json_object_array_length(array);
    for (size_t i = 0; i < len; ++i) {
        struct json_object *entry = json_object_array_get_idx(array, (int)i);
        struct json_object *value;
        event_field_mapping_t mapping;
        char context[128];
        rsRetVal r;
        snprintf(context, sizeof(context), "%s eventFields[%zu]", source, i);
        if (!json_object_is_type(entry, json_type_object)) {
            r = report_validation_issue(pData, context, "entry must be an object");
            if (r != RS_RET_OK) return r;
            continue;
        }
        memset(&mapping, 0, sizeof(mapping));
        if (!json_object_object_get_ex(entry, "event_id", &value) || !json_object_is_type(value, json_type_int)) {
            r = report_validation_issue(pData, context, "missing event_id");
            if (r != RS_RET_OK) return r;
            continue;
        }
        mapping.event_id = json_object_get_int(value);
        if (json_object_object_get_ex(entry, "required_flags", &value)) {
            sbool ok = 0;
            mapping.required_flags = parse_section_flags_array(value, &ok);
            if (!ok) {
                r = report_validation_issue(pData, context, "unknown required flag");
                if (r != RS_RET_OK) return r;
                continue;
            }
        }
        if (!json_object_object_get_ex(entry, "patterns", &value) || !json_object_is_type(value, json_type_array)) {
            r = report_validation_issue(pData, context, "patterns must be an array");
            if (r != RS_RET_OK) return r;
            continue;
        }
        size_t plen = json_object_array_length(value);
        for (size_t j = 0; j < plen; ++j) {
            struct json_object *patternObj = json_object_array_get_idx(value, (int)j);
            field_pattern_t patternEntry;
            const char *patternText;
            const char *canonical = NULL;
            struct json_object *pv;
            char itemContext[160];
            snprintf(itemContext, sizeof(itemContext), "%s patterns[%zu]", context, j);
            if (!json_object_is_type(patternObj, json_type_object)) {
                r = report_validation_issue(pData, itemContext, "entry must be an object");
                if (r != RS_RET_OK) return r;
                continue;
            }
            memset(&patternEntry, 0, sizeof(patternEntry));
            patternEntry.value_type = fieldValueString;
            patternEntry.priority = FIELD_PRIORITY_EVENT_OVERRIDE;
            patternEntry.sensitivity = fieldSensitivityCaseSensitive;
            if (!json_object_object_get_ex(patternObj, "pattern", &pv) || !json_object_is_type(pv, json_type_string)) {
                r = report_validation_issue(pData, itemContext, "missing pattern");
                if (r != RS_RET_OK) return r;
                continue;
            }
            patternText = json_object_get_string(pv);
            patternEntry.pattern = strdup(patternText);
            if (patternEntry.pattern == NULL) {
                cleanup_event_field_mapping(&mapping);
                return RS_RET_OUT_OF_MEMORY;
            }
            if (json_object_object_get_ex(patternObj, "canonical", &pv) && json_object_is_type(pv, json_type_string))
                canonical = json_object_get_string(pv);
            if (canonical != NULL) {
                patternEntry.canonical = strdup(canonical);
                if (patternEntry.canonical == NULL) {
                    cleanup_field_pattern(&patternEntry);
                    cleanup_event_field_mapping(&mapping);
                    return RS_RET_OUT_OF_MEMORY;
                }
            } else {
                char *normalized = normalize_label(patternText);
                if (normalized == NULL) {
                    cleanup_field_pattern(&patternEntry);
                    r = report_validation_issue(pData, itemContext, "could not derive canonical name");
                    if (r != RS_RET_OK) {
                        cleanup_event_field_mapping(&mapping);
                        return r;
                    }
                    continue;
                }
                patternEntry.canonical = normalized;
            }
            if (json_object_object_get_ex(patternObj, "section", &pv) && json_object_is_type(pv, json_type_string)) {
                const char *section = json_object_get_string(pv);
                if (section != NULL) {
                    patternEntry.section = strdup(section);
                    if (patternEntry.section == NULL) {
                        cleanup_field_pattern(&patternEntry);
                        cleanup_event_field_mapping(&mapping);
                        return RS_RET_OUT_OF_MEMORY;
                    }
                }
            }
            if (json_object_object_get_ex(patternObj, "priority", &pv) && json_object_is_type(pv, json_type_int))
                patternEntry.priority = json_object_get_int(pv);
            if (json_object_object_get_ex(patternObj, "value_type", &pv) && json_object_is_type(pv, json_type_string)) {
                const char *typeText = json_object_get_string(pv);
                if (!parse_field_value_type_string(typeText, &patternEntry.value_type)) {
                    cleanup_field_pattern(&patternEntry);
                    r = report_validation_issue(pData, itemContext, "unknown value_type");
                    if (r != RS_RET_OK) {
                        cleanup_event_field_mapping(&mapping);
                        return r;
                    }
                    continue;
                }
            }
            if (json_object_object_get_ex(patternObj, "sensitivity", &pv) &&
                json_object_is_type(pv, json_type_string)) {
                const char *sens = json_object_get_string(pv);
                if (!parse_field_sensitivity_string(sens, &patternEntry.sensitivity)) {
                    cleanup_field_pattern(&patternEntry);
                    r = report_validation_issue(pData, itemContext, "unknown sensitivity");
                    if (r != RS_RET_OK) {
                        cleanup_event_field_mapping(&mapping);
                        return r;
                    }
                    continue;
                }
            }
            r = append_field_pattern_owned(&mapping.patterns, &mapping.pattern_count, &patternEntry);
            if (r != RS_RET_OK) {
                cleanup_field_pattern(&patternEntry);
                cleanup_event_field_mapping(&mapping);
                return r;
            }
        }
        r = merge_event_field_mapping(pData, &mapping);
        if (r != RS_RET_OK) {
            cleanup_event_field_mapping(&mapping);
            return r;
        }
    }
    return RS_RET_OK;
}

static rsRetVal load_event_metadata_definitions(instanceData *pData, struct json_object *array, const char *source) {
    size_t len;
    if (array == NULL) return RS_RET_OK;
    if (!json_object_is_type(array, json_type_array))
        return report_validation_issue(pData, source, "events must be an array");
    len = json_object_array_length(array);
    for (size_t i = 0; i < len; ++i) {
        struct json_object *entry = json_object_array_get_idx(array, (int)i);
        struct json_object *value;
        event_mapping_t mapping;
        char context[128];
        rsRetVal r;
        snprintf(context, sizeof(context), "%s events[%zu]", source, i);
        if (!json_object_is_type(entry, json_type_object)) {
            r = report_validation_issue(pData, context, "entry must be an object");
            if (r != RS_RET_OK) return r;
            continue;
        }
        memset(&mapping, 0, sizeof(mapping));
        if (!json_object_object_get_ex(entry, "event_id", &value) || !json_object_is_type(value, json_type_int)) {
            r = report_validation_issue(pData, context, "missing event_id");
            if (r != RS_RET_OK) return r;
            continue;
        }
        mapping.event_id = json_object_get_int(value);
        if (json_object_object_get_ex(entry, "category", &value) && json_object_is_type(value, json_type_string)) {
            mapping.category = strdup(json_object_get_string(value));
            if (mapping.category == NULL) {
                cleanup_event_mapping(&mapping);
                return RS_RET_OUT_OF_MEMORY;
            }
        }
        if (json_object_object_get_ex(entry, "subtype", &value) && json_object_is_type(value, json_type_string)) {
            mapping.subtype = strdup(json_object_get_string(value));
            if (mapping.subtype == NULL) {
                cleanup_event_mapping(&mapping);
                return RS_RET_OUT_OF_MEMORY;
            }
        }
        if (json_object_object_get_ex(entry, "outcome", &value) && json_object_is_type(value, json_type_string)) {
            mapping.outcome = strdup(json_object_get_string(value));
            if (mapping.outcome == NULL) {
                cleanup_event_mapping(&mapping);
                return RS_RET_OUT_OF_MEMORY;
            }
        }
        r = merge_event_mapping(pData, &mapping);
        if (r != RS_RET_OK) {
            cleanup_event_mapping(&mapping);
            return r;
        }
    }
    return RS_RET_OK;
}

static rsRetVal load_custom_definition_text(instanceData *pData, const char *jsonText, const char *sourceLabel) {
    struct json_tokener *tokener;
    struct json_object *root;
    enum json_tokener_error err;
    rsRetVal r = RS_RET_OK;
    if (jsonText == NULL || pData == NULL) return RS_RET_INVALID_PARAMS;
    tokener = json_tokener_new();
    if (tokener == NULL) return RS_RET_OUT_OF_MEMORY;
    root = json_tokener_parse_ex(tokener, jsonText, (int)strlen(jsonText));
    err = fjson_tokener_get_error(tokener);
    json_tokener_free(tokener);
    if (err != fjson_tokener_success || root == NULL) {
        return report_validation_issue(pData, sourceLabel, "invalid JSON definitions");
    }
    if (!json_object_is_type(root, json_type_object)) {
        r = report_validation_issue(pData, sourceLabel, "definition root must be an object");
        json_object_put(root);
        return r;
    }
    if (json_object_object_length(root) == 0) {
        json_object_put(root);
        return RS_RET_OK;
    }
    struct json_object *sections;
    struct json_object *fields;
    struct json_object *eventFields;
    struct json_object *events;
    if (json_object_object_get_ex(root, "sections", &sections)) {
        r = load_section_definitions(pData, sections, sourceLabel);
        if (r != RS_RET_OK) {
            json_object_put(root);
            return r;
        }
    }
    if (json_object_object_get_ex(root, "fields", &fields)) {
        r = load_field_definitions(pData, fields, sourceLabel);
        if (r != RS_RET_OK) {
            json_object_put(root);
            return r;
        }
    }
    if (json_object_object_get_ex(root, "eventFields", &eventFields)) {
        r = load_event_field_definitions(pData, eventFields, sourceLabel);
        if (r != RS_RET_OK) {
            json_object_put(root);
            return r;
        }
    }
    if (json_object_object_get_ex(root, "events", &events)) {
        r = load_event_metadata_definitions(pData, events, sourceLabel);
        if (r != RS_RET_OK) {
            json_object_put(root);
            return r;
        }
    }
    json_object_put(root);
    return RS_RET_OK;
}

static rsRetVal load_custom_definition_file(instanceData *pData, const char *path) {
    char *content = NULL;
    rsRetVal r;
    if (path == NULL) return RS_RET_INVALID_PARAMS;
    r = read_text_file(path, &content);
    if (r != RS_RET_OK) return r;
    r = load_custom_definition_text(pData, content, path);
    free(content);
    return r;
}

static sbool wildcard_match(const char *pattern, const char *text, sbool caseInsensitive) {
    const char *p = pattern;
    const char *t = text;
    const char *star = NULL;
    const char *backup = NULL;
    if (pattern == NULL || text == NULL) return 0;
    while (*t != '\0') {
        char pc;
        char tc;
        if (*p == '*') {
            star = p++;
            backup = t;
            continue;
        }
        pc = *p;
        tc = *t;
        if (caseInsensitive) {
            pc = (char)tolower((unsigned char)pc);
            tc = (char)tolower((unsigned char)tc);
        }
        if (pc == tc || (pc == '?' && *t != '\0')) {
            ++p;
            ++t;
            continue;
        }
        if (star != NULL) {
            p = star + 1;
            t = ++backup;
            continue;
        }
        return 0;
    }
    while (*p == '*') ++p;
    return *p == '\0';
}

static size_t pattern_specificity(const char *pattern) {
    size_t score = 0;
    if (pattern == NULL) return 0;
    while (*pattern != '\0') {
        if (*pattern != '*' && *pattern != '?') ++score;
        ++pattern;
    }
    return score;
}

static sbool section_pattern_matches(const section_descriptor_t *desc, const char *label) {
    const char *subject = label;
    char *normalized = NULL;
    sbool matched;
    if (desc == NULL || label == NULL || desc->pattern == NULL) return 0;
    switch (desc->sensitivity) {
        case fieldSensitivityCanonical:
            normalized = normalize_label(label);
            subject = normalized;
            matched = wildcard_match(desc->pattern, subject ? subject : label, 0);
            free(normalized);
            return matched;
        case fieldSensitivityCaseInsensitive:
            return wildcard_match(desc->pattern, label, 1);
        case fieldSensitivityCaseSensitive:
        default:
            return wildcard_match(desc->pattern, label, 0);
    }
}

static inline int section_is_enabled(const instanceData *pData, uint32_t flags);

static const section_descriptor_t *select_section_descriptor(const instanceData *inst, const char *label) {
    const section_descriptor_t *best = NULL;
    int bestPriority = INT_MIN;
    size_t bestSpecificity = 0;
    if (inst == NULL || label == NULL) return NULL;
    for (size_t i = 0; i < inst->sectionDescriptorCount; ++i) {
        const section_descriptor_t *candidate = &inst->sectionDescriptors[i];
        if (!section_is_enabled(inst, candidate->flags)) continue;
        if (!section_pattern_matches(candidate, label)) continue;
        size_t specificity = pattern_specificity(candidate->pattern);
        if (candidate->priority > bestPriority ||
            (candidate->priority == bestPriority && specificity > bestSpecificity)) {
            best = candidate;
            bestPriority = candidate->priority;
            bestSpecificity = specificity;
        }
    }
    return best;
}

static const section_descriptor_t *find_embedded_section_descriptor(const instanceData *inst,
                                                                    char *label,
                                                                    char **sectionStartOut) {
    const section_descriptor_t *best = NULL;
    char *bestStart = NULL;
    int bestPriority = INT_MIN;
    size_t bestSpecificity = 0;
    size_t labelLen;

    if (sectionStartOut != NULL) *sectionStartOut = NULL;
    if (inst == NULL || label == NULL || sectionStartOut == NULL) return NULL;

    labelLen = strlen(label);
    if (labelLen == 0) return NULL;

    for (size_t i = 0; i < inst->sectionDescriptorCount; ++i) {
        const section_descriptor_t *candidate = &inst->sectionDescriptors[i];
        const char *pattern = candidate->pattern;
        size_t patternLen;
        char *candidateStart;
        int cmp;

        if (!section_is_enabled(inst, candidate->flags)) continue;
        if (pattern == NULL) continue;
        patternLen = strlen(pattern);
        if (patternLen == 0 || patternLen >= labelLen) continue;

        candidateStart = label + labelLen - patternLen;
        if (candidateStart <= label) continue;
        if (!isspace((unsigned char)candidateStart[-1]) && candidateStart[-1] != '.' && candidateStart[-1] != '-')
            continue;

        switch (candidate->sensitivity) {
            case fieldSensitivityCaseInsensitive:
                cmp = strncasecmp(candidateStart, pattern, patternLen);
                break;
            case fieldSensitivityCanonical: {
                char *normalized = normalize_label(candidateStart);
                cmp = (normalized != NULL) ? strcmp(normalized, pattern) : 1;
                free(normalized);
                break;
            }
            case fieldSensitivityCaseSensitive:
            default:
                cmp = strncmp(candidateStart, pattern, patternLen);
                break;
        }
        if (cmp != 0) continue;

        size_t specificity = pattern_specificity(candidate->pattern);
        if (candidate->priority > bestPriority ||
            (candidate->priority == bestPriority && specificity > bestSpecificity)) {
            best = candidate;
            bestPriority = candidate->priority;
            bestSpecificity = specificity;
            bestStart = candidateStart;
        }
    }

    if (best != NULL) *sectionStartOut = bestStart;
    return best;
}

static inline int section_is_enabled(const instanceData *pData, uint32_t flags) {
    if ((flags & SECTION_FLAG_NETWORK) && !pData->enableNetwork) return 0;
    if ((flags & SECTION_FLAG_LAPS) && !pData->enableLaps) return 0;
    if ((flags & SECTION_FLAG_TLS) && !pData->enableTls) return 0;
    if ((flags & SECTION_FLAG_WDAC) && !pData->enableWdac) return 0;
    return 1;
}

static inline const char *lookup_logon_description(int logonType) {
    for (size_t i = 0; i < ARRAY_SIZE(g_logonTypeMap); ++i) {
        if (g_logonTypeMap[i].type_id == logonType) return g_logonTypeMap[i].description;
    }
    return NULL;
}

static inline const event_mapping_t *lookup_event_mapping(const instanceData *inst, int eventId) {
    if (inst != NULL && inst->eventMappings != NULL) {
        for (size_t i = 0; i < inst->eventMappingCount; ++i) {
            if (inst->eventMappings[i].event_id == eventId) return &inst->eventMappings[i];
        }
    }
    return NULL;
}

static const char *find_case_insensitive(const char *haystack, const char *needle) {
    size_t nlen;
    if (haystack == NULL || needle == NULL) return NULL;
    nlen = strlen(needle);
    if (nlen == 0) return haystack;
    for (const char *p = haystack; *p; ++p) {
        if (strncasecmp(p, needle, nlen) == 0) return p;
    }
    return NULL;
}

static inline const char *skip_lws_const(const char *p) {
    if (p == NULL) return NULL;
    while (*p != '\0' && isspace((unsigned char)*p)) ++p;
    return p;
}

static inline const char *skip_nonspace_const(const char *p) {
    if (p == NULL) return NULL;
    while (*p != '\0' && !isspace((unsigned char)*p)) ++p;
    return p;
}

static inline sbool is_all_digits_range(const char *start, const char *end) {
    if (start == NULL || end == NULL || start >= end) return 0;
    for (const char *p = start; p < end; ++p) {
        if (!isdigit((unsigned char)*p)) return 0;
    }
    return 1;
}

static const char *skip_structured_data_sections(const char *p) {
    const char *cur = p;
    if (cur == NULL) return NULL;
    while (*cur == '[') {
        ++cur;
        while (*cur != '\0' && *cur != ']') {
            if (*cur == '\\' && cur[1] != '\0') {
                cur += 2;
                continue;
            }
            ++cur;
        }
        if (*cur != ']') return NULL;
        ++cur;
        while (*cur != '\0' && isspace((unsigned char)*cur)) ++cur;
    }
    return cur;
}

static const char *skip_rfc5424_header(const char *p) {
    const char *cur = skip_lws_const(p);
    const char *end;
    if (cur == NULL) return NULL;
    end = skip_nonspace_const(cur);
    if (!is_all_digits_range(cur, end)) return NULL;
    cur = skip_lws_const(end);
    for (int i = 0; i < 5; ++i) {
        end = skip_nonspace_const(cur);
        if (end == cur) return NULL;
        cur = skip_lws_const(end);
    }
    if (*cur == '-') {
        ++cur;
        cur = skip_lws_const(cur);
    } else if (*cur == '[') {
        cur = skip_structured_data_sections(cur);
        if (cur == NULL) return NULL;
        cur = skip_lws_const(cur);
    } else {
        return NULL;
    }
    return cur;
}

static const char *skip_rfc3164_header(const char *p) {
    const char *cur = skip_lws_const(p);
    const char *end;
    if (cur == NULL) return NULL;
    for (int i = 0; i < 3; ++i) {
        end = skip_nonspace_const(cur);
        if (end == cur) return NULL;
        cur = skip_lws_const(end);
    }
    end = skip_nonspace_const(cur);
    if (end == cur) return NULL;
    cur = skip_lws_const(end);
    return cur;
}

/**
 * @brief Locate the start of the Snare payload within a syslog message.
 *
 * The parser receives messages that may contain RFC3164 or RFC5424 envelopes,
 * pre-parsed payloads, or bare Snare messages. This function applies a series
 * of heuristics to find the beginning of either the textual or JSON Snare
 * event block.
 *
 * @param msg Raw message text as received by the action.
 * @return Pointer to the beginning of the Snare payload or @c NULL when no
 *         payload could be identified.
 */
static const char *locate_snare_payload(const char *msg, smsg_t *pMsg) {
    const char *cursor;
    const char *afterPri;
    const char *candidate;
    if (msg == NULL) return NULL;
    dbgprintf("[mmsnareparse DEBUG] locate_snare_payload: input msg='%s'\n", msg);
    cursor = skip_lws_const(msg);
    afterPri = cursor;
    if (cursor != NULL && *cursor == '<') {
        const char *p = cursor + 1;
        sbool haveDigits = 0;
        while (*p >= '0' && *p <= '9') {
            haveDigits = 1;
            ++p;
        }
        if (haveDigits && *p == '>') {
            afterPri = p + 1;
        }
    }
    cursor = skip_lws_const(afterPri);

    // Check if MSWinEventLog is in the syslog tag (RFC3164 parser moves it there)
    // If so, the message body should start with version, channel, etc.
    // But after syslog parsing, the message might start with timestamp instead
    // We need to find the version ("1") followed by tab and channel
    if (pMsg != NULL) {
        uchar *tagBuf = NULL;
        int tagLen = 0;
        getTAG(pMsg, &tagBuf, &tagLen, 0);
        if (tagBuf != NULL && tagLen > 0 && strstr((const char *)tagBuf, "MSWinEventLog") != NULL) {
            // MSWinEventLog is in tag - message body should be: version, channel, record, timestamp, eventid,
            // provider... But after syslog parsing, it might be: timestamp, eventid, provider... (version and channel
            // missing) Or it might be: version, channel, record, timestamp, eventid, provider... Look for pattern:
            // version number (single or multi-digit) followed by tab/escaped-tab, then channel name
            const char *p = cursor;
            while (p != NULL && *p != '\0') {
                // Skip whitespace
                while (*p == ' ' || *p == '\t') p++;
                if (*p == '\0') break;  // Reached end after skipping whitespace
                // Check for version number (single or multi-digit) followed by tab/escaped-tab
                if (*p >= '0' && *p <= '9') {
                    const char *versionEnd = p;
                    while (*versionEnd >= '0' && *versionEnd <= '9') versionEnd++;
                    if (versionEnd > p && (*versionEnd == '\t' || *versionEnd == ' ' ||
                                           (versionEnd[0] == '#' && versionEnd[1] == '0' && versionEnd[2] == '1' &&
                                            versionEnd[3] == '1'))) {
                        const char *afterVersion = versionEnd;
                        if (*afterVersion == '#') {
                            afterVersion += 4;
                        } else {
                            afterVersion++;
                        }
                        while (*afterVersion == ' ' || *afterVersion == '\t') afterVersion++;
                        if (strncmp(afterVersion, "Microsoft-Windows-", 19) == 0) {
                            dbgprintf(
                                "[mmsnareparse DEBUG] locate_snare_payload: found MSWinEventLog in syslog tag, found "
                                "version+channel at '%s'\n",
                                p);
                            return p;
                        }
                    }
                    // Advance past the version number we just checked to avoid redundant checks
                    p = versionEnd;
                } else {
                    p++;
                }
            }
            // If we can't find version+channel pattern, the version and channel were removed by syslog parser
            // In this case, we need to extract channel from the original message
            // Return cursor for tokenization, but channel extraction will use rawMsg
            dbgprintf(
                "[mmsnareparse DEBUG] locate_snare_payload: found MSWinEventLog in syslog tag, version+channel not "
                "found, using cursor '%s'\n",
                cursor);
            return cursor;
        }
    }

    // First, check if MSWinEventLog is at the cursor (direct match)
    if (cursor != NULL && strncmp(cursor, "MSWinEventLog", 13) == 0) {
        dbgprintf("[mmsnareparse DEBUG] locate_snare_payload: found MSWinEventLog at '%s'\n", cursor);
        return cursor;
    }

    // Handle case where syslog header has been parsed and we have a timestamp followed by MSWinEventLog
    if (cursor != NULL) {
        // Look for MSWinEventLog anywhere in the original message
        // This is the most reliable way to detect RFC5424 format
        const char *msWinEventLog = strstr(msg, "MSWinEventLog");
        if (msWinEventLog != NULL) {
            dbgprintf("[mmsnareparse DEBUG] locate_snare_payload: found MSWinEventLog in message at '%s'\n",
                      msWinEventLog);
            return msWinEventLog;
        }
        // Look for the EventID pattern (4-digit number) which comes before the provider.
        // Search provider once to avoid repeated full-suffix scans on malformed input.
        const char *providerPos = strstr(cursor, "Microsoft-Windows-Security-Auditing");
        if (providerPos != NULL) {
            const char *eventIdStart = cursor;
            while (*eventIdStart != '\0' && eventIdStart < providerPos) {
                if (*eventIdStart >= '0' && *eventIdStart <= '9') {
                    // Found a digit, check if it's a 4-digit EventID
                    const char *eventIdEnd = eventIdStart;
                    while (*eventIdEnd >= '0' && *eventIdEnd <= '9') {
                        eventIdEnd++;
                    }
                    if (eventIdEnd - eventIdStart == 4) {
                        // Found a 4-digit number, check if it's followed by whitespace before the provider
                        if (*eventIdEnd == '\t' || *eventIdEnd == ' ') {
                            const char *afterEventId = skip_lws_const(eventIdEnd);
                            if (afterEventId != NULL && afterEventId <= providerPos) {
                                dbgprintf("[mmsnareparse DEBUG] locate_snare_payload: found EventID pattern at '%s'\n",
                                          eventIdStart);
                                return eventIdStart;
                            }
                        }
                    }
                    eventIdStart = eventIdEnd;
                    continue;
                }
                eventIdStart++;
            }
        }
        // After syslog parsing, MSWinEventLog may be removed from the message
        // Try to find MSWinEventLog anywhere in the original message
        // This allows us to use RFC5424 format with channel in tokens[2]
        const char *msWinEventLogPos = strstr(msg, "MSWinEventLog");
        if (msWinEventLogPos != NULL) {
            // Found MSWinEventLog in original message - use RFC5424 format
            // If it's before cursor, return it directly
            // If it's after cursor, we still want to use it (cursor might be after syslog header)
            if (msWinEventLogPos < cursor) {
                dbgprintf(
                    "[mmsnareparse DEBUG] locate_snare_payload: found MSWinEventLog before cursor, using RFC5424 "
                    "format at '%s'\n",
                    msWinEventLogPos);
                return msWinEventLogPos;
            } else {
                // MSWinEventLog is after cursor (e.g., after syslog header)
                // This is still valid - return it to use RFC5424 format
                dbgprintf(
                    "[mmsnareparse DEBUG] locate_snare_payload: found MSWinEventLog after cursor, using RFC5424 format "
                    "at '%s'\n",
                    msWinEventLogPos);
                return msWinEventLogPos;
            }
        }
        // If we didn't find EventID pattern, try looking for Microsoft-Windows-Security-Auditing
        // but we need to go back to find the EventID that comes before it
        const char *msWinSec = strstr(cursor, "Microsoft-Windows-Security-Auditing");
        if (msWinSec != NULL) {
            // Go back from the provider to find the EventID
            const char *searchStart = cursor;
            while (searchStart < msWinSec) {
                if (*searchStart >= '0' && *searchStart <= '9') {
                    const char *eventIdEnd = searchStart;
                    while (*eventIdEnd >= '0' && *eventIdEnd <= '9') {
                        eventIdEnd++;
                    }
                    if (eventIdEnd - searchStart == 4) {
                        // Found a 4-digit number before the provider
                        dbgprintf("[mmsnareparse DEBUG] locate_snare_payload: found EventID before provider at '%s'\n",
                                  searchStart);
                        return searchStart;
                    }
                    searchStart = eventIdEnd;
                    continue;
                }
                searchStart++;
            }
            // If we still can't find EventID, fall back to provider
            dbgprintf("[mmsnareparse DEBUG] locate_snare_payload: found Microsoft-Windows-Security-Auditing at '%s'\n",
                      msWinSec);
            return msWinSec;
        }
    }
    if (cursor != NULL) {
        const char *versionEnd = skip_nonspace_const(cursor);
        if (is_all_digits_range(cursor, versionEnd)) {
            candidate = skip_rfc5424_header(cursor);
            if (candidate != NULL && strncmp(candidate, "MSWinEventLog", 13) == 0) return candidate;
        }
        candidate = skip_rfc3164_header(cursor);
        if (candidate != NULL && strncmp(candidate, "MSWinEventLog", 13) == 0) return candidate;
    }
    if (afterPri != NULL) {
        candidate = strstr(afterPri, "MSWinEventLog");
        if (candidate != NULL) return candidate;
    }
    return NULL;
}

/**
 * @brief Aggregates parsing state while transforming a Snare message.
 *
 * @var parse_context::inst Active module configuration shared by workers.
 * @var parse_context::msg  Message currently processed.
 * @var parse_context::root Root JSON container produced by the parser.
 * @var parse_context::event JSON object containing Event-level metadata.
 * @var parse_context::eventData Lazily created EventData object.
 * @var parse_context::unparsed Bucket for text that could not be normalized.
 * @var parse_context::logonDerived Derived fields attached to the Logon node.
 * @var parse_context::eventId Numeric Windows Event ID currently in scope.
 * @var parse_context::activeSection Descriptor for the active description section.
 * @var parse_context::activeSectionObj JSON object representing the active section.
 * @var parse_context::summarySet Tracks whether the Summary field was populated.
 */
typedef struct parse_context {
    instanceData *inst;
    smsg_t *msg;
    struct json_object *root;
    struct json_object *event;
    struct json_object *eventData;
    struct json_object *unparsed;
    struct json_object *logonDerived;
    struct json_object *validationRoot;
    struct json_object *validationErrors;
    struct json_object *statsRoot;
    struct json_object *statsParsing;
    int eventId;
    const section_descriptor_t *activeSection;
    struct json_object *activeSectionObj;
    sbool summarySet;
    const field_pattern_t *corePatterns;
    size_t corePatternCount;
    const field_pattern_t *eventPatterns;
    size_t eventPatternCount;
    int patternEventId;
    sbool patternsPrepared;
    validation_context_t validation;
    field_detection_context_t fieldDetection;
    size_t totalFields;
    size_t successfulParses;
    size_t failedParses;
} parse_context_t;

static struct json_object *ensure_object(struct json_object *parent, const char *name) {
    struct json_object *existing;
    if (parent == NULL || name == NULL) return NULL;
    if (json_object_object_get_ex(parent, name, &existing)) return existing;
    existing = json_object_new_object();
    if (existing == NULL) return NULL;
    json_object_object_add(parent, name, existing);
    return existing;
}

static inline struct json_object *ensure_event_data(parse_context_t *ctx) {
    if (ctx == NULL || ctx->root == NULL) return NULL;
    if (ctx->eventData == NULL) {
        ctx->eventData = json_object_new_object();
        if (ctx->eventData != NULL) json_object_object_add(ctx->root, "EventData", ctx->eventData);
    }
    return ctx->eventData;
}

static inline struct json_object *ensure_logon_root(parse_context_t *ctx) {
    if (ctx == NULL || ctx->root == NULL) return NULL;
    if (ctx->logonDerived == NULL) {
        ctx->logonDerived = json_object_new_object();
        if (ctx->logonDerived != NULL) json_object_object_add(ctx->root, "Logon", ctx->logonDerived);
    }
    return ctx->logonDerived;
}

static inline void append_unparsed(parse_context_t *ctx, const char *text) {
    struct json_object *arr;
    if (text == NULL || *text == '\0') return;
    if (ctx->unparsed == NULL) {
        ctx->unparsed = json_object_new_array();
        if (ctx->unparsed != NULL) json_object_object_add(ctx->root, "Unparsed", ctx->unparsed);
    }
    arr = ctx->unparsed;
    if (arr != NULL) json_object_array_add(arr, json_object_new_string(text));
}

static void initialize_observability(parse_context_t *ctx) {
    if (ctx == NULL || ctx->inst == NULL) return;
    ctx->validation = ctx->inst->validationTemplate;
    ctx->validation.current_error_count = 0;
    ctx->fieldDetection.parse_ctx = ctx;
    ctx->fieldDetection.validation = &ctx->validation;
    ctx->fieldDetection.parsing_errors = 0;
    ctx->fieldDetection.enable_debug = ctx->inst->runtimeConfig.enable_debug || ctx->validation.enable_debug;
    ctx->fieldDetection.enable_fallback = ctx->inst->runtimeConfig.enable_fallback;
    ctx->fieldDetection.errors_array = NULL;
    ctx->totalFields = 0;
    ctx->successfulParses = 0;
    ctx->failedParses = 0;

    if (ctx->root != NULL) {
        ctx->validationRoot = ensure_object(ctx->root, "Validation");
        if (ctx->validationRoot != NULL) {
            ctx->validationErrors = json_object_new_array();
            if (ctx->validationErrors != NULL) {
                json_object_object_add(ctx->validationRoot, "Errors", ctx->validationErrors);
                ctx->fieldDetection.errors_array = ctx->validationErrors;
            }
        }
        ctx->statsRoot = ensure_object(ctx->root, "Stats");
        if (ctx->statsRoot != NULL) ctx->statsParsing = ensure_object(ctx->statsRoot, "ParsingStats");
    }
}

static void finalize_parsing_stats(parse_context_t *ctx) {
    if (ctx == NULL || ctx->statsParsing == NULL) return;
    json_object_object_add(ctx->statsParsing, "total_fields", json_object_new_int64((long long)ctx->totalFields));
    json_object_object_add(ctx->statsParsing, "successful_parses",
                           json_object_new_int64((long long)ctx->successfulParses));
    json_object_object_add(ctx->statsParsing, "failed_parses", json_object_new_int64((long long)ctx->failedParses));
}

static void json_add_string(struct json_object *obj, const char *name, const char *value) {
    if (obj == NULL || name == NULL || value == NULL) return;
    json_object_object_add(obj, name, json_object_new_string(value));
}

static void json_add_int64(struct json_object *obj, const char *name, long long value) {
    if (obj == NULL || name == NULL) return;
    json_object_object_add(obj, name, json_object_new_int64(value));
}

static void json_add_bool(struct json_object *obj, const char *name, sbool value) {
    if (obj == NULL || name == NULL) return;
    json_object_object_add(obj, name, json_object_new_boolean(value ? 1 : 0));
}

static sbool try_parse_int64(const char *value, long long *outVal) {
    char *end = NULL;
    long long val;
    if (value == NULL || *value == '\0') return 0;
    val = strtoll(value, &end, 0);
    if (end == value || (end != NULL && *end != '\0' && !isspace((unsigned char)*end))) return 0;
    if (outVal != NULL) *outVal = val;
    return 1;
}

static sbool try_parse_bool(const char *value, sbool *outVal) {
    if (value == NULL) return 0;
    if (!strcasecmp(value, "true") || !strcasecmp(value, "yes") || !strcasecmp(value, "enabled") ||
        !strcasecmp(value, "on")) {
        if (outVal != NULL) *outVal = 1;
        return 1;
    }
    if (!strcasecmp(value, "false") || !strcasecmp(value, "no") || !strcasecmp(value, "disabled") ||
        !strcasecmp(value, "off")) {
        if (outVal != NULL) *outVal = 0;
        return 1;
    }
    if (!strcmp(value, "1")) {
        if (outVal != NULL) *outVal = 1;
        return 1;
    }
    if (!strcmp(value, "0")) {
        if (outVal != NULL) *outVal = 0;
        return 1;
    }
    return 0;
}

static struct json_object *try_parse_json_block(const char *value) {
    struct json_tokener *tokener;
    struct json_object *parsed;
    enum json_tokener_error err;
    if (value == NULL) return NULL;
    tokener = json_tokener_new();
    if (tokener == NULL) return NULL;
    parsed = json_tokener_parse_ex(tokener, value, (int)strlen(value));
    err = fjson_tokener_get_error(tokener);
    json_tokener_free(tokener);
    if (err != fjson_tokener_success || parsed == NULL) {
        if (parsed != NULL) json_object_put(parsed);
        return NULL;
    }
    return parsed;
}

static void parse_privilege_sequence(parse_context_t *ctx, const char *text);

static void ensure_event_patterns(parse_context_t *ctx) {
    if (ctx == NULL) return;
    if (ctx->patternsPrepared && ctx->patternEventId == ctx->eventId) return;
    ctx->eventPatterns = NULL;
    ctx->eventPatternCount = 0;
    ctx->patternEventId = ctx->eventId;
    if (ctx->inst->eventFieldMappings != NULL) {
        for (size_t i = 0; i < ctx->inst->eventFieldMappingCount; ++i) {
            const event_field_mapping_t *mapping = &ctx->inst->eventFieldMappings[i];
            if (mapping->event_id == ctx->eventId) {
                if (section_is_enabled(ctx->inst, mapping->required_flags)) {
                    ctx->eventPatterns = mapping->patterns;
                    ctx->eventPatternCount = mapping->pattern_count;
                }
                break;
            }
        }
    }
    ctx->patternsPrepared = 1;
}

static sbool pattern_matches(const field_pattern_t *pattern, const char *label, const char *canon) {
    if (pattern == NULL) return 0;
    switch (pattern->sensitivity) {
        case fieldSensitivityCanonical:
            if (canon == NULL) return 0;
            return strcmp(pattern->pattern, canon) == 0;
        case fieldSensitivityCaseSensitive:
            if (label == NULL) return 0;
            return strcmp(pattern->pattern, label) == 0;
        case fieldSensitivityCaseInsensitive:
            if (label == NULL) return 0;
            return strcasecmp(pattern->pattern, label) == 0;
        default:
            return 0;
    }
}

static const field_pattern_t *select_field_pattern(parse_context_t *ctx,
                                                   const char *label,
                                                   const char *canon,
                                                   const char *sectionName) {
    const field_pattern_t *bestSection = NULL;
    const field_pattern_t *bestGeneric = NULL;
    const field_pattern_t *bestFallback = NULL;
    int bestSectionPrio = INT_MIN;
    int bestGenericPrio = INT_MIN;
    int bestFallbackPrio = INT_MIN;

    if (ctx == NULL) return NULL;

    const size_t eventCount = ctx->eventPatterns != NULL ? ctx->eventPatternCount : 0u;
    const size_t coreCount = ctx->corePatterns != NULL ? ctx->corePatternCount : 0u;

    for (size_t pass = 0; pass < 2; ++pass) {
        const field_pattern_t *patterns = (pass == 0) ? ctx->eventPatterns : ctx->corePatterns;
        size_t patternCount = (pass == 0) ? eventCount : coreCount;
        for (size_t i = 0; i < patternCount; ++i) {
            const field_pattern_t *candidate = &patterns[i];
            const char *candidateSection = candidate->section;

            if (!pattern_matches(candidate, label, canon)) continue;

            if (sectionName != NULL && candidateSection != NULL && strcmp(candidateSection, sectionName) == 0) {
                if (candidate->priority > bestSectionPrio) {
                    bestSectionPrio = candidate->priority;
                    bestSection = candidate;
                }
                continue;
            }

            if (candidateSection == NULL) {
                if (candidate->priority > bestGenericPrio) {
                    bestGenericPrio = candidate->priority;
                    bestGeneric = candidate;
                }
                continue;
            }

            if (candidate->priority > bestFallbackPrio) {
                bestFallbackPrio = candidate->priority;
                bestFallback = candidate;
            }
        }
    }

    if (bestSection != NULL) return bestSection;
    if (bestGeneric != NULL) return bestGeneric;
    // If sectionName is NULL and we have a fallback, prefer EventData section patterns
    if (sectionName == NULL && bestFallback != NULL) {
        // Look for EventData section pattern specifically
        for (size_t pass = 0; pass < 2; ++pass) {
            const field_pattern_t *patterns = (pass == 0) ? ctx->eventPatterns : ctx->corePatterns;
            size_t patternCount = (pass == 0) ? eventCount : coreCount;
            for (size_t i = 0; i < patternCount; ++i) {
                const field_pattern_t *candidate = &patterns[i];
                if (pattern_matches(candidate, label, canon) && candidate->section != NULL &&
                    strcmp(candidate->section, "EventData") == 0) {
                    return candidate;
                }
            }
        }
    }
    return bestFallback;
}

static void add_raw_string(struct json_object *obj, const char *baseName, const char *value) {
    size_t len;
    char *rawName;
    if (obj == NULL || baseName == NULL || value == NULL) return;
    len = strlen(baseName) + 4 + 1; /* "Raw" suffix */
    rawName = malloc(len);
    if (rawName == NULL) {
        json_add_string(obj, baseName, value);
        return;
    }
    snprintf(rawName, len, "%sRaw", baseName);
    json_add_string(obj, rawName, value);
    free(rawName);
}

static rsRetVal store_validated_string(field_detection_context_t *fdCtx,
                                       struct json_object *target,
                                       const char *key,
                                       const char *value,
                                       bool (*validator)(const char *),
                                       const char *error_message,
                                       sbool *storedOut) {
    if (validator == NULL || validator(value)) {
        json_add_string(target, key, value);
        if (storedOut != NULL) *storedOut = 1;
        return RS_RET_OK;
    }

    rsRetVal r = handle_parsing_error(fdCtx, error_message, key);
    if (r == RS_RET_OK && fdCtx->enable_fallback) {
        json_add_string(target, key, value);
        if (storedOut != NULL) *storedOut = 1;
    }
    return r;
}

static struct json_object *resolve_target_object(parse_context_t *ctx,
                                                 struct json_object *hint,
                                                 const field_pattern_t *pattern) {
    if (ctx == NULL) return NULL;
    if (pattern == NULL || pattern->section == NULL) {
        if (hint != NULL) return hint;
        return ensure_event_data(ctx);
    }
    if (!strcmp(pattern->section, FIELD_SECTION_EVENT_DATA)) return ensure_event_data(ctx);
    if (!strcmp(pattern->section, FIELD_SECTION_LOGON)) return ensure_logon_root(ctx);
    if (!strcmp(pattern->section, FIELD_SECTION_ROOT)) return ctx->root;
    return ensure_object(ctx->root, pattern->section);
}

static rsRetVal parse_field_value_enhanced(const char *value,
                                           field_value_type_t type,
                                           struct json_object *target,
                                           const char *key,
                                           const char *section_context,
                                           int event_id,
                                           field_detection_context_t *fdCtx,
                                           sbool *storedOut) {
    if (storedOut != NULL) *storedOut = 0;
    if (target == NULL || key == NULL || fdCtx == NULL || fdCtx->validation == NULL) return RS_RET_INVALID_PARAMS;
    if (value == NULL || *value == '\0') {
        if (storedOut != NULL) *storedOut = -1;
        return RS_RET_OK;
    }

    char *trimmed = trim_whitespace_enhanced(value);
    if (trimmed == NULL) return RS_RET_OUT_OF_MEMORY;
    if (*trimmed == '\0') {
        free(trimmed);
        if (storedOut != NULL) *storedOut = -1;
        return RS_RET_OK;
    }
    if (is_placeholder_value(trimmed)) {
        if (storedOut != NULL) *storedOut = -1;
        free(trimmed);
        return RS_RET_OK;
    }

    rsRetVal r = RS_RET_OK;
    sbool boolVal = 0;
    long long numVal = 0;
    switch (type) {
        case fieldValueInt64:
        case fieldValueInt64WithRaw:
            if (try_parse_int64(trimmed, &numVal)) {
                json_add_int64(target, key, numVal);
                if (storedOut != NULL) *storedOut = 1;
            } else {
                r = handle_parsing_error(fdCtx, "expected integer", key);
                if (r == RS_RET_OK && fdCtx->enable_fallback) {
                    if (type == fieldValueInt64WithRaw)
                        add_raw_string(target, key, trimmed);
                    else
                        json_add_string(target, key, trimmed);
                    if (storedOut != NULL) *storedOut = 1;
                }
            }
            break;
        case fieldValueBool:
            if (try_parse_bool(trimmed, &boolVal)) {
                json_add_bool(target, key, boolVal);
                if (storedOut != NULL) *storedOut = 1;
            } else {
                r = handle_parsing_error(fdCtx, "expected boolean", key);
                if (r == RS_RET_OK && fdCtx->enable_fallback) {
                    json_add_string(target, key, trimmed);
                    if (storedOut != NULL) *storedOut = 1;
                }
            }
            break;
        case fieldValueJson: {
            struct json_object *jsonBlock = try_parse_json_block(trimmed);
            if (jsonBlock != NULL) {
                json_object_object_add(target, key, jsonBlock);
                if (storedOut != NULL) *storedOut = 1;
            } else {
                if (is_json_format(trimmed)) {
                    r = handle_parsing_error(fdCtx, "invalid JSON block", key);
                    if (r != RS_RET_OK) break;
                }
                if (fdCtx->enable_fallback) {
                    json_add_string(target, key, trimmed);
                    if (storedOut != NULL) *storedOut = 1;
                }
            }
            break;
        }
        case fieldValueLogonType:
            if (try_parse_int64(trimmed, &numVal)) {
                json_add_int64(target, key, numVal);
                const char *desc = lookup_logon_description((int)numVal);
                if (desc != NULL) json_add_string(target, "LogonTypeName", desc);
                if (storedOut != NULL) *storedOut = 1;
            } else {
                r = handle_parsing_error(fdCtx, "invalid logon type", key);
                if (r == RS_RET_OK && fdCtx->enable_fallback) {
                    json_add_string(target, key, trimmed);
                    if (storedOut != NULL) *storedOut = 1;
                }
            }
            break;
        case fieldValueRemoteCredentialGuard:
            if (try_parse_bool(trimmed, &boolVal)) {
                json_add_bool(target, key, boolVal);
                if (fdCtx->parse_ctx != NULL) json_add_bool(ensure_logon_root(fdCtx->parse_ctx), key, boolVal);
                if (storedOut != NULL) *storedOut = 1;
            } else {
                r = handle_parsing_error(fdCtx, "invalid Remote Credential Guard value", key);
                if (r == RS_RET_OK && fdCtx->enable_fallback) {
                    json_add_string(target, key, trimmed);
                    if (storedOut != NULL) *storedOut = 1;
                }
            }
            break;
        case fieldValueGuid:
            r = store_validated_string(fdCtx, target, key, trimmed, is_guid_format, "invalid GUID", storedOut);
            break;
        case fieldValueIpAddress:
            r = store_validated_string(fdCtx, target, key, trimmed, is_ip_address, "invalid IP address", storedOut);
            break;
        case fieldValueTimestamp:
            r = store_validated_string(fdCtx, target, key, trimmed, is_timestamp_format, "invalid timestamp",
                                       storedOut);
            break;
        case fieldValuePrivilegeList:
            if (fdCtx->parse_ctx != NULL) {
                parse_privilege_sequence(fdCtx->parse_ctx, trimmed);
                if (storedOut != NULL) *storedOut = 1;
            }
            break;
        case fieldValueString:
        default:
            json_add_string(target, key, trimmed);
            if (storedOut != NULL) *storedOut = 1;
            break;
    }

    if (section_context != NULL && fdCtx->validation->enable_debug) {
        dbgprintf("mmsnareparse: stored field '%s' (section=%s, event=%d)\n", key, section_context, event_id);
    }

    free(trimmed);
    return r;
}

static void dispatch_field(
    parse_context_t *ctx, struct json_object *target, const char *sectionName, const char *label, const char *value) {
    if (ctx == NULL || label == NULL) return;
    if (value == NULL) value = "";

    ensure_event_patterns(ctx);

    char *canon = normalize_label(label);
    if (canon == NULL) return;

    const field_pattern_t *pattern = select_field_pattern(ctx, label, canon, sectionName);
    struct json_object *dest = resolve_target_object(ctx, target, pattern);
    if (dest == NULL && target != NULL) dest = target;
    if (dest == NULL) dest = ensure_event_data(ctx);

    if (dest != NULL) {
        sbool stored = 0;
        rsRetVal r;
        if (pattern != NULL) {
            const char *fieldName = (pattern->canonical != NULL) ? pattern->canonical : canon;
            r = parse_field_value_enhanced(value, pattern->value_type, dest, fieldName, sectionName, ctx->eventId,
                                           &ctx->fieldDetection, &stored);
        } else {
            r = parse_field_value_enhanced(value, fieldValueString, dest, canon, sectionName, ctx->eventId,
                                           &ctx->fieldDetection, &stored);
        }
        if (r != RS_RET_OK) {
            ctx->failedParses++;
        } else if (stored > 0) {
            ctx->totalFields++;
            ctx->successfulParses++;
        } else if (stored == 0) {
            ctx->failedParses++;
        }
    }

    free(canon);
}


static const char *derive_outcome(const char *auditResult) {
    if (auditResult == NULL) return NULL;
    if (find_case_insensitive(auditResult, "success") != NULL) return "success";
    if (find_case_insensitive(auditResult, "failure") != NULL || find_case_insensitive(auditResult, "fail") != NULL)
        return "failure";
    if (find_case_insensitive(auditResult, "error") != NULL) return "error";
    if (find_case_insensitive(auditResult, "warning") != NULL) return "warning";
    if (find_case_insensitive(auditResult, "information") != NULL) return "information";
    return NULL;
}

static rsRetVal handle_parsing_error(field_detection_context_t *ctx, const char *error_message, const char *context) {
    if (ctx == NULL || ctx->validation == NULL) return RS_RET_INVALID_PARAMS;
    ctx->parsing_errors++;
    ctx->validation->current_error_count++;

    if ((ctx->enable_debug || ctx->validation->enable_debug) && error_message != NULL) {
        dbgprintf("mmsnareparse: parsing error in %s: %s\n", context != NULL ? context : "<unknown>", error_message);
    }

    if (ctx->validation->log_parsing_errors && ctx->errors_array != NULL && error_message != NULL) {
        char buffer[256];
        if (context != NULL)
            snprintf(buffer, sizeof(buffer), "%s: %s", context, error_message);
        else
            snprintf(buffer, sizeof(buffer), "%s", error_message);
        json_object_array_add(ctx->errors_array, json_object_new_string(buffer));
    }

    if (ctx->validation->mode == VALIDATION_STRICT) {
        if (!ctx->validation->continue_on_error) return RS_RET_INVALID_VALUE;
        if (ctx->validation->current_error_count >= ctx->validation->max_errors_before_fail)
            return RS_RET_INVALID_VALUE;
    }

    return RS_RET_OK;
}

static rsRetVal validate_field_count(const char *message,
                                     size_t actual_count,
                                     size_t expected_count,
                                     validation_context_t *ctx) {
    if (ctx == NULL) return RS_RET_INVALID_PARAMS;
    /* Treat expected_count as total attempts and actual_count as successful parses.
     * If fewer fields were successfully parsed than attempted, mark as a validation issue.
     */
    if (expected_count > actual_count) {
        if (ctx->enable_debug)
            dbgprintf("mmsnareparse: %zu/%zu fields parsed successfully%s. message='%s'\n", actual_count,
                      expected_count, (ctx->mode == VALIDATION_STRICT ? " (strict)" : ""),
                      message != NULL ? message : "<n/a>");
        if (ctx->mode == VALIDATION_MODERATE && ctx->log_parsing_errors) {
            LogError(0, RS_RET_INVALID_VALUE, "mmsnareparse: %zu/%zu fields parsed successfully", actual_count,
                     expected_count);
        }
        if (ctx->mode == VALIDATION_STRICT) {
            return RS_RET_INVALID_VALUE;
        }
    }
    return RS_RET_OK;
}

static rsRetVal validate_required_fields(const char *message,
                                         const char *required_fields[],
                                         size_t required_count,
                                         validation_context_t *ctx) {
    if (ctx == NULL || required_fields == NULL) return RS_RET_INVALID_PARAMS;
    if (message == NULL) message = "";
    for (size_t i = 0; i < required_count; ++i) {
        if (required_fields[i] == NULL) continue;
        if (strstr(message, required_fields[i]) == NULL) {
            if (ctx->mode == VALIDATION_STRICT) {
                if (ctx->enable_debug) dbgprintf("mmsnareparse: missing required field '%s'\n", required_fields[i]);
                return RS_RET_INVALID_VALUE;
            }
            if (ctx->mode == VALIDATION_MODERATE && ctx->log_parsing_errors) {
                LogError(0, RS_RET_INVALID_VALUE, "mmsnareparse: missing required field '%s'", required_fields[i]);
            }
        }
    }
    return RS_RET_OK;
}

static void apply_event_mapping(parse_context_t *ctx, const char *auditResult) {
    const event_mapping_t *mapping;
    const char *outcome = NULL;
    if (ctx->event == NULL) return;
    mapping = lookup_event_mapping(ctx->inst, ctx->eventId);
    if (mapping != NULL) {
        if (mapping->category != NULL) json_add_string(ctx->event, "Category", mapping->category);
        if (mapping->subtype != NULL) json_add_string(ctx->event, "Subtype", mapping->subtype);
        if (mapping->outcome != NULL) outcome = mapping->outcome;
    }
    if (outcome == NULL) outcome = derive_outcome(auditResult);
    if (outcome != NULL) json_add_string(ctx->event, "Outcome", outcome);
}

static const char *find_next_token(const char *start, const char *end) {
    const char *p = start;
    while (p < end) {
        if (*p == ' ') {
            const char *q = p;
            int count = 0;
            while (q < end && *q == ' ') {
                ++q;
                ++count;
            }
            if (count >= 3) {
                const char *labelStart = q;
                while (labelStart < end && *labelStart == ' ') ++labelStart;
                if (labelStart >= end) return NULL;
                const char *colon = memchr(labelStart, ':', (size_t)(end - labelStart));
                if (colon != NULL) return p;
            }
            p = q;
        } else if (*p == '\t') {
            // Handle Windows Security Event Log format with tabs
            const char *labelStart = p + 1;
            while (labelStart < end && (*labelStart == ' ' || *labelStart == '\t')) ++labelStart;
            if (labelStart >= end) return NULL;
            const char *colon = memchr(labelStart, ':', (size_t)(end - labelStart));
            if (colon != NULL) return p;
            ++p;
        } else {
            ++p;
        }
    }
    return NULL;
}

static void handle_key_value(
    parse_context_t *ctx, struct json_object *target, const char *sectionName, const char *label, const char *value);

/**
 * @brief Parse a whitespace-delimited sequence of key-value pairs.
 *
 * Snare description blocks often condense multiple @c key: value tokens into a
 * single line separated by runs of spaces. The function walks through the
 * sequence, extracts each pair, and dispatches the normalized data to
 * ::handle_key_value.
 *
 * @param ctx          Active parsing context.
 * @param target       JSON object that receives the parsed keys.
 * @param text         Source text containing the condensed key-value sequence.
 * @param sectionName  Optional section name used to drive section-specific
 *                     normalization.
 */

typedef struct key_value_callback {
    parse_context_t *ctx;
    struct json_object *target;
    const char *sectionName;
} key_value_callback_t;

static void parse_key_value_token(const char *token, size_t len, void *user_data) {
    key_value_callback_t *cb = (key_value_callback_t *)user_data;
    if (cb == NULL || cb->ctx == NULL || cb->target == NULL || token == NULL || len == 0) return;

    char *mutable = trim_copy(token, len);
    if (mutable == NULL) return;

    char *colon = strchr(mutable, ':');
    if (colon == NULL) {
        if (*mutable != '\0') append_unparsed(cb->ctx, mutable);
        free(mutable);
        return;
    }
    *colon = '\0';
    char *key = mutable;
    char *value = colon + 1;
    trim_inplace(key);
    trim_inplace(value);
    if (*key == '\0') {
        free(mutable);
        return;
    }
    handle_key_value(cb->ctx, cb->target, cb->sectionName, key, value);
    free(mutable);
}

static void parse_key_value_sequence(parse_context_t *ctx,
                                     struct json_object *target,
                                     const char *text,
                                     const char *sectionName) {
    if (text == NULL) return;

    dbgprintf("[mmsnareparse DEBUG] parse_key_value_sequence: text='%s', sectionName='%s'\n",
              text != NULL ? text : "<null>", sectionName != NULL ? sectionName : "<none>");

    key_value_callback_t cb = {.ctx = ctx, .target = target, .sectionName = sectionName};

    // First try tokenize_on_multispace (for multi-space separated tokens)
    tokenize_on_multispace(text, strlen(text), parse_key_value_token, &cb);

    // If that didn't work (single-space separated key-value pairs), parse directly
    // Look for patterns like "Key: Value" where Key starts with uppercase
    const char *p = text;
    const char *end = text + strlen(text);
    while (p < end) {
        // Skip whitespace
        while (p < end && isspace((unsigned char)*p)) p++;
        if (p >= end) break;

        // Look for a label start (uppercase letter)
        if (!isupper((unsigned char)*p)) {
            p++;
            continue;
        }

        // Find the colon - keys should be single words (alphanumeric, no spaces)
        // This prevents matching "Event ID 3 User:" as a key
        const char *keyStart = p;
        const char *keyEnd = keyStart;
        // Find the end of the key (alphanumeric characters only, no spaces)
        while (keyEnd < end && isalnum((unsigned char)*keyEnd)) {
            keyEnd++;
        }
        // If we didn't find a colon right after the key, this isn't a valid key-value pair
        if (keyEnd >= end || *keyEnd != ':') {
            p++;
            continue;
        }
        const char *colon = keyEnd;
        p = colon + 1;  // Skip colon, update p

        // Extract key
        size_t keyLen = colon - keyStart;
        char *key = trim_copy(keyStart, keyLen);
        if (key == NULL || *key == '\0') {
            if (key) free(key);
            continue;
        }

        // Skip whitespace after colon
        while (p < end && isspace((unsigned char)*p)) p++;

        // Find the value end (next key-value pair pattern: space + uppercase + alphanumeric + colon)
        // Values can contain: letters, numbers, spaces, backslashes, hyphens, and other characters
        // Only stop when we find a complete key pattern: whitespace + uppercase + alphanumeric_word + colon
        const char *valueStart = p;
        const char *valueEnd = valueStart;
        while (valueEnd < end) {
            // Look for potential start of next key: whitespace + uppercase letter
            // We need to check this carefully to avoid false positives when values contain
            // uppercase letters, spaces, or backslashes (e.g., "CORP\NETWORK SERVICE")
            if (valueEnd > valueStart && isspace((unsigned char)*(valueEnd - 1)) && isupper((unsigned char)*valueEnd)) {
                // Potential key start found. Verify it's a complete key pattern:
                // 1. Must be a single alphanumeric word (no spaces, backslashes, etc.)
                // 2. Must be followed immediately by a colon
                const char *keyCandidate = valueEnd;
                const char *candidateKeyEnd = keyCandidate;

                // Scan for alphanumeric characters only (keys are single words)
                while (candidateKeyEnd < end && isalnum((unsigned char)*candidateKeyEnd)) {
                    candidateKeyEnd++;
                }

                // Check if we have a valid key pattern:
                // - Must have at least one alphanumeric character
                // - Must be followed immediately by a colon (no spaces or other chars)
                // - The character before keyCandidate must be whitespace (already checked above)
                if ((candidateKeyEnd - keyCandidate) > 0 && candidateKeyEnd < end && *candidateKeyEnd == ':') {
                    // Found a complete key pattern - this is the start of the next key-value pair
                    // Back up to the whitespace before the key (start of next pair)
                    valueEnd = keyCandidate - 1;  // Point to the space before the key
                    break;
                }
                // Not a valid key pattern - continue scanning (this might be part of the value)
            }
            valueEnd++;
        }

        // Extract value
        size_t valueLen = valueEnd - valueStart;
        char *value = trim_copy(valueStart, valueLen);
        if (value != NULL) {
            handle_key_value(ctx, target, sectionName, key, value);
            free(value);
        }
        free(key);

        p = valueEnd;
    }
}

static void parse_privilege_sequence(parse_context_t *ctx, const char *text) {
    struct json_object *privObj;
    struct json_object *existing;
    char *trimmed;
    char *cursor;
    char *combined = NULL;

    if (ctx == NULL || ctx->root == NULL || text == NULL) return;
    privObj = ensure_object(ctx->root, "Privileges");
    if (privObj == NULL) return;

    trimmed = trim_copy(text, strlen(text));
    if (trimmed == NULL) return;
    if (*trimmed == '\0' || is_placeholder_value(trimmed)) {
        free(trimmed);
        return;
    }

    if (json_object_object_get_ex(privObj, "PrivilegeList", &existing) &&
        json_object_is_type(existing, json_type_string)) {
        const char *existing_str = json_object_get_string(existing);
        if (existing_str != NULL && *existing_str != '\0') {
            combined = strdup(existing_str);
            if (combined == NULL) {
                free(trimmed);
                return;
            }
        }
    }

    cursor = trimmed;
    while (*cursor != '\0') {
        cursor += strspn(cursor, " \t\r\n");
        if (*cursor == '\0') break;
        size_t len = strcspn(cursor, " \t\r\n");
        if (len == 0) {
            ++cursor;
            continue;
        }
        char *token = strdup_range(cursor, len);
        cursor += len;
        if (token == NULL) {
            free(combined);
            free(trimmed);
            return;
        }
        if (*token == '\0') {
            free(token);
            continue;
        }
        if (combined == NULL) {
            combined = token;
        } else {
            char *expanded;
            if (asprintf(&expanded, "%s %s", combined, token) < 0) {
                free(token);
                free(combined);
                free(trimmed);
                return;
            }
            free(combined);
            combined = expanded;
            free(token);
        }
    }

    if (combined != NULL) {
        json_object_object_add(privObj, "PrivilegeList", json_object_new_string(combined));
        free(combined);
    }

    free(trimmed);
}

static void handle_inline_remote_credential_guard(parse_context_t *ctx,
                                                  struct json_object *sectionObj,
                                                  const char *value) {
    sbool boolVal = 0;
    if (value == NULL) return;
    if (try_parse_bool(value, &boolVal)) {
        json_add_bool(sectionObj, "Enabled", boolVal);
        json_add_bool(ensure_logon_root(ctx), "RemoteCredentialGuard", boolVal);
    }
    json_add_string(sectionObj, "Status", value);
}

static void parse_semicolon_sequence(parse_context_t *ctx, struct json_object *sectionObj, const char *text) {
    char *mutable;
    char *saveptr = NULL;
    if (text == NULL || sectionObj == NULL) return;
    mutable = strdup(text);
    if (mutable == NULL) return;
    for (char *token = strtok_r(mutable, ";", &saveptr); token != NULL; token = strtok_r(NULL, ";", &saveptr)) {
        char *kv = token;
        char *eq;
        trim_inplace(kv);
        if (*kv == '\0') continue;
        eq = strchr(kv, '=');
        if (eq == NULL) {
            append_unparsed(ctx, kv);
            continue;
        }
        *eq = '\0';
        char *key = kv;
        char *val = eq + 1;
        trim_inplace(key);
        trim_inplace(val);
        if (*key == '\0') continue;
        char *canon = normalize_label(key);
        if (canon == NULL) continue;
        if (!strcmp(canon, "CredentialRotation")) {
            sbool boolVal = 0;
            if (try_parse_bool(val, &boolVal))
                json_add_bool(sectionObj, canon, boolVal);
            else
                json_add_string(sectionObj, canon, val);
        } else {
            json_add_string(sectionObj, canon, val);
        }
        free(canon);
    }
    free(mutable);
}

/**
 * @brief Store a general key-value pair in the EventData container.
 *
 * The helper performs event-specific adjustments (for example WDAC and Windows
 * Update for Business events) and then writes the normalized value into the
 * \c EventData object.
 *
 * @param ctx   Active parsing context.
 * @param label Raw field label from the message.
 * @param value Raw value associated with @p label.
 */
static void handle_general_key(parse_context_t *ctx, const char *label, const char *value) {
    dispatch_field(ctx, NULL, NULL, label, value);
}

/**
 * @brief Normalize and store a key-value pair for a specific section.
 *
 * This function canonicalizes labels, performs numeric conversions, derives
 * helper metadata (such as @c LogonTypeName), and routes fields to derived
 * JSON locations like the Logon container.
 *
 * @param ctx         Active parsing context.
 * @param target      JSON object representing the current section.
 * @param sectionName Canonical name of the active section or @c NULL for
 *                    top-level handling.
 * @param label       Raw key extracted from the message.
 * @param value       Raw value associated with @p label.
 */
static void handle_key_value(
    parse_context_t *ctx, struct json_object *target, const char *sectionName, const char *label, const char *value) {
    dispatch_field(ctx, target, sectionName, label, value);
}

static char *normalize_description(const char *desc) {
    size_t len;
    char *out;
    size_t j = 0;
    if (desc == NULL) return NULL;
    len = strlen(desc);
    out = malloc(len + 1);
    if (out == NULL) return NULL;
    for (size_t i = 0; i < len;) {
        if (desc[i] == '\r') {
            ++i;
            continue;
        }
        if (desc[i] == '\n') {
            out[j++] = '\n';
            ++i;
            continue;
        }
        // Handle Windows Security Event Log format with multiple spaces
        // Look for patterns like "   " (3+ spaces) that separate sections
        if (desc[i] == ' ') {
            size_t spaceCount = 0;
            size_t k = i;
            while (k < len && desc[k] == ' ') {
                spaceCount++;
                k++;
            }
            // If we have 3 or more spaces, treat as section separator
            if (spaceCount >= 3) {
                out[j++] = '\n';
                i = k;
                continue;
            }
        }
        out[j++] = desc[i++];
    }
    out[j] = '\0';
    return out;
}

/**
 * @brief Parse a single normalized description line.
 *
 * Each line may introduce a new description section, extend the active section
 * with condensed key-value data, or provide fallback text that is stored in the
 * Summary or Unparsed buckets.
 *
 * @param ctx  Active parsing context.
 * @param line Mutable line buffer produced by ::normalize_description.
 */
static void parse_line(parse_context_t *ctx, char *line) {
    char *colon;
    char *label;
    char *rest;
    const section_descriptor_t *desc;
    struct json_object *sectionObj;
    if (line == NULL) return;
    trim_inplace(line);
    if (*line == '\0') return;

    dbgprintf("[mmsnareparse DEBUG] parse_line: processing line='%s'\n", line);

    colon = strchr(line, ':');
    if (colon == NULL) {
        dbgprintf("[mmsnareparse DEBUG] parse_line: no colon found, treating as content\n");

        // Special handling for Privileges section - collect privilege names
        if (ctx->activeSection != NULL && strcmp(ctx->activeSection->canonical, "Privileges") == 0 &&
            ctx->activeSection->behavior == sectionBehaviorList) {
            dbgprintf("[mmsnareparse DEBUG] parse_line: collecting privilege name '%s' in Privileges section\n", line);

            // Get or create the Privileges object
            struct json_object *privileges_obj = NULL;
            if (!json_object_object_get_ex(ctx->root, "Privileges", &privileges_obj)) {
                privileges_obj = json_object_new_object();
                json_object_object_add(ctx->root, "Privileges", privileges_obj);
            }

            // Get the current PrivilegeList string
            struct json_object *privilege_list = NULL;
            const char *current_list = "";
            if (json_object_object_get_ex(privileges_obj, "PrivilegeList", &privilege_list)) {
                current_list = json_object_get_string(privilege_list);
            }

            // Create the new privilege list string
            char *new_list = NULL;
            if (strlen(current_list) > 0) {
                int ret = asprintf(&new_list, "%s %s", current_list, line);
                if (ret < 0) new_list = NULL;
            } else {
                new_list = strdup(line);
            }

            if (new_list != NULL) {
                json_object_object_add(privileges_obj, "PrivilegeList", json_object_new_string(new_list));
                free(new_list);
            }

            return;
        }

        if (!ctx->summarySet) {
            json_add_string(ctx->root, "Summary", line);
            ctx->summarySet = 1;
        } else if (ctx->activeSection != NULL && ctx->activeSectionObj != NULL) {
            dbgprintf("[mmsnareparse DEBUG] parse_line: parsing as key-value in active section '%s'\n",
                      ctx->activeSection->canonical);
            parse_key_value_sequence(ctx, ctx->activeSectionObj, line, ctx->activeSection->canonical);
        } else {
            dbgprintf("[mmsnareparse DEBUG] parse_line: no active section, appending to unparsed\n");
            append_unparsed(ctx, line);
        }
        return;
    }
    *colon = '\0';
    label = line;
    rest = colon + 1;
    trim_inplace(label);
    while (*rest == ' ') ++rest;

    dbgprintf("[mmsnareparse DEBUG] parse_line: label='%s', rest='%s'\n", label, rest);

    desc = select_section_descriptor(ctx->inst, label);
    if (desc == NULL) {
        char *sectionStart = NULL;
        const section_descriptor_t *embedded = find_embedded_section_descriptor(ctx->inst, label, &sectionStart);
        if (embedded != NULL && sectionStart != NULL) {
            size_t summaryLen = (size_t)(sectionStart - label);
            if (summaryLen > 0) {
                char *summary = trim_copy(label, summaryLen);
                if (summary != NULL && *summary != '\0') {
                    if (!ctx->summarySet) {
                        json_add_string(ctx->root, "Summary", summary);
                        ctx->summarySet = 1;
                    } else {
                        append_unparsed(ctx, summary);
                    }
                }
                free(summary);
            }
            label = sectionStart;
            trim_inplace(label);
            desc = embedded;
        }
    }
    if (desc != NULL) {
        dbgprintf("[mmsnareparse DEBUG] parse_line: matched section pattern '%s' -> '%s'\n", desc->pattern,
                  desc->canonical);
        switch (desc->behavior) {
            case sectionBehaviorStandard:
                sectionObj = ensure_object(ctx->root, desc->canonical);
                if (sectionObj != NULL && *rest != '\0') {
                    parse_key_value_sequence(ctx, sectionObj, rest, desc->canonical);
                    ctx->activeSection = desc;
                    ctx->activeSectionObj = sectionObj;
                } else {
                    ctx->activeSection = desc;
                    ctx->activeSectionObj = sectionObj;
                }
                break;
            case sectionBehaviorInlineValue:
                sectionObj = ensure_object(ctx->root, desc->canonical);
                if (sectionObj != NULL) handle_inline_remote_credential_guard(ctx, sectionObj, rest);
                ctx->activeSection = NULL;
                ctx->activeSectionObj = NULL;
                break;
            case sectionBehaviorSemicolon:
                sectionObj = ensure_object(ctx->root, desc->canonical);
                if (sectionObj != NULL) parse_semicolon_sequence(ctx, sectionObj, rest);
                ctx->activeSection = NULL;
                ctx->activeSectionObj = NULL;
                break;
            case sectionBehaviorList: {
                // Start building the privileges list - keep section active to collect more privilege names
                struct json_object *privileges_obj = NULL;
                if (!json_object_object_get_ex(ctx->root, "Privileges", &privileges_obj)) {
                    privileges_obj = json_object_new_object();
                    json_object_object_add(ctx->root, "Privileges", privileges_obj);
                }

                // Initialize with the first privilege name
                json_object_object_add(privileges_obj, "PrivilegeList", json_object_new_string(rest));

                // Keep the section active to collect more privilege names
                ctx->activeSection = desc;
                ctx->activeSectionObj = privileges_obj;
                break;
            }
            default:
                /* Unknown section behavior - this should not happen with current enum values */
                break;
        }
        return;
    }
    // Check if we have an active section - if so, store the key-value pair in that section
    if (ctx->activeSection != NULL && ctx->activeSectionObj != NULL) {
        dbgprintf("[mmsnareparse DEBUG] parse_line: storing in active section '%s': label='%s', rest='%s'\n",
                  ctx->activeSection->canonical, label, rest);
        handle_key_value(ctx, ctx->activeSectionObj, ctx->activeSection->canonical, label, rest);
        return;
    }

    // No active section, clear the context and process as general key-value pair
    ctx->activeSection = NULL;
    ctx->activeSectionObj = NULL;
    if (!strcmp(label, "Privileges")) {
        parse_privilege_sequence(ctx, rest);
        return;
    }
    if (*rest == '\0') {
        dbgprintf("[mmsnareparse DEBUG] parse_line: rest is empty, appending to unparsed\n");
        append_unparsed(ctx, label);
        return;
    }
    dbgprintf("[mmsnareparse DEBUG] parse_line: processing general key-value pair: label='%s', rest='%s'\n", label,
              rest);
    {
        const char *restEnd = rest + strlen(rest);
        const char *valueStart = rest;
        const char *nextToken;
        const char *valueEnd;
        char *valueCopy = NULL;
        while (valueStart < restEnd && *valueStart == ' ') ++valueStart;
        nextToken = find_next_token(valueStart, restEnd);
        valueEnd = (nextToken != NULL) ? nextToken : restEnd;
        if (valueStart < valueEnd) {
            valueCopy = trim_copy(valueStart, (size_t)(valueEnd - valueStart));
        }
        dbgprintf("[mmsnareparse DEBUG] parse_line: valueCopy='%s', calling handle_general key\n",
                  valueCopy ? valueCopy : "NULL");
        if (valueCopy != NULL) {
            handle_general_key(ctx, label, valueCopy);
            free(valueCopy);
        } else if (valueStart < valueEnd) {
            char saved = *((char *)valueEnd);
            *((char *)valueEnd) = '\0';
            handle_general_key(ctx, label, valueStart);
            *((char *)valueEnd) = saved;
        } else {
            handle_general_key(ctx, label, "");
        }
        // Parse remaining key-value pairs in the rest of the line
        // Even if find_next_token didn't find a token (e.g., key-value pairs separated by single spaces),
        // we should still try to parse the rest as key-value pairs
        if (nextToken != NULL) {
            parse_key_value_sequence(ctx, NULL, nextToken, NULL);
        } else {
            // No next token found (key-value pairs separated by single spaces)
            // Check if the rest contains key-value pairs (has colons) and parse them
            const char *remaining = valueEnd;
            while (remaining < restEnd && *remaining == ' ') ++remaining;
            if (remaining < restEnd && strchr(remaining, ':') != NULL) {
                dbgprintf("[mmsnareparse DEBUG] parse_line: parsing remaining content as key-value pairs: '%s'\n",
                          remaining);
                parse_key_value_sequence(ctx, NULL, remaining, NULL);
            } else if (valueStart < restEnd && strchr(valueStart, ':') != NULL) {
                // The value itself might contain key-value pairs - parse the entire rest
                dbgprintf("[mmsnareparse DEBUG] parse_line: parsing entire rest as key-value pairs: '%s'\n",
                          valueStart);
                parse_key_value_sequence(ctx, NULL, valueStart, NULL);
            }
        }
    }
}

/**
 * @brief Normalize and parse the Snare description text.
 *
 * The routine flattens carriage returns, converts triple-space boundaries into
 * line breaks, and then feeds each logical line to ::parse_line.
 *
 * @param ctx  Active parsing context.
 * @param desc Raw description string extracted from the Snare payload.
 */
static void parse_description(parse_context_t *ctx, const char *desc) {
    char *normalized;
    char *save;
    char *line;

    dbgprintf("[mmsnareparse DEBUG] parse_description: input desc='%s'\n", desc);

    normalized = normalize_description(desc);
    if (normalized == NULL) {
        dbgprintf("[mmsnareparse DEBUG] parse_description: normalize_description returned NULL\n");
        return;
    }

    dbgprintf("[mmsnareparse DEBUG] parse_description: normalized='%s'\n", normalized);

    save = normalized;
    int lineNum = 1;
    while ((line = strsep(&normalized, "\n")) != NULL) {
        dbgprintf("[mmsnareparse DEBUG] parse_description: processing line %d: '%s'\n", lineNum, line);
        parse_line(ctx, line);
        lineNum++;
    }
    free(save);
}

/**
 * @brief Check if the first two tokens represent a version and channel.
 *
 * Determines if tokens[0] is a single-digit version number and tokens[1] starts
 * with "Microsoft-Windows-" (indicating a channel name).
 *
 * @param tokens     Array of token strings.
 * @param tokenCount Number of valid entries in @p tokens.
 * @return true if tokens[0] is a version and tokens[1] is a channel, false otherwise.
 */
static bool tokens_have_version_and_channel(char **tokens, size_t tokenCount) {
    if (tokenCount > 1 && tokens[0] != NULL && tokens[1] != NULL) {
        if (tokens[0][0] >= '0' && tokens[0][0] <= '9' && tokens[0][1] == '\0' &&
            strncmp(tokens[1], "Microsoft-Windows-", 19) == 0) {
            return true;
        }
    }
    return false;
}

/**
 * @brief Extract channel name from raw message containing MSWinEventLog format.
 *
 * Parses the MSWinEventLog format to extract the channel name. The format is:
 * MSWinEventLog<tab>version<tab>channel<tab>...
 *
 * @param rawMsg Original message text containing MSWinEventLog (can be NULL).
 * @return Malloc'd string containing the channel name, or NULL if not found.
 *         Caller must free the returned string.
 */
static char *extract_channel_from_raw_msg(const char *rawMsg) {
    if (rawMsg == NULL) {
        return NULL;
    }

    // Look for MSWinEventLog followed by tab/escaped-tab, then version, then tab, then channel
    const char *msWinPos = strstr(rawMsg, "MSWinEventLog");
    if (msWinPos == NULL) {
        return NULL;
    }

    const char *afterMSWin = msWinPos + 13;  // Skip "MSWinEventLog"
    // Skip tab/escaped-tab and version
    if (*afterMSWin == '\t' || *afterMSWin == ' ' ||
        (afterMSWin[0] == '#' && afterMSWin[1] == '0' && afterMSWin[2] == '1' && afterMSWin[3] == '1')) {
        // Skip tab/escaped-tab
        if (*afterMSWin == '#') {
            afterMSWin += 4;
        } else {
            afterMSWin++;
        }
        afterMSWin = skip_lws_const(afterMSWin);
        // Skip version (single digit typically)
        while (*afterMSWin >= '0' && *afterMSWin <= '9') {
            afterMSWin++;
        }
        // Skip next tab/escaped-tab
        if (*afterMSWin == '\t' || *afterMSWin == ' ' ||
            (afterMSWin[0] == '#' && afterMSWin[1] == '0' && afterMSWin[2] == '1' && afterMSWin[3] == '1')) {
            if (*afterMSWin == '#') {
                afterMSWin += 4;
            } else {
                afterMSWin++;
            }
            afterMSWin = skip_lws_const(afterMSWin);
            // Extract channel (up to next tab/escaped-tab or end)
            const char *channelStart = afterMSWin;
            const char *channelEnd = channelStart;
            while (*channelEnd != '\0' && *channelEnd != '\t' && *channelEnd != ' ' &&
                   !(channelEnd[0] == '#' && channelEnd[1] == '0' && channelEnd[2] == '1' && channelEnd[3] == '1')) {
                channelEnd++;
            }
            if (channelEnd > channelStart) {
                size_t channelLen = channelEnd - channelStart;
                char *channelBuf = malloc(channelLen + 1);
                if (channelBuf != NULL) {
                    memcpy(channelBuf, channelStart, channelLen);
                    channelBuf[channelLen] = '\0';
                    return channelBuf;
                }
            }
        }
    }
    return NULL;
}

/**
 * @brief Extract core metadata fields from the header token array.
 *
 * Both RFC3164 and RFC5424 Snare formats are supported. The function populates
 * the @c Event object with identifiers, source system information, and derived
 * outcome data.
 *
 * @param ctx        Active parsing context.
 * @param tokens     Array of tab-delimited tokens that form the header.
 * @param tokenCount Number of valid entries in @p tokens.
 * @param rawMsg     Original message text for extracting channel when not in tokens (can be NULL).
 *                   This is the full message text before locate_snare_payload processing.
 */
static void populate_event_metadata(
    parse_context_t *ctx, char **tokens, size_t tokenCount, const char *rawMsg, smsg_t *pMsg) {
    // Detect format based on token structure
    // RFC5424 format: MSWinEventLog, version, channel, record, timestamp, eventid, provider, n/a, n/a, eventtype,
    // computer, categorytext, empty, empty, description RFC3164 format: year, eventid, provider, n/a, n/a, eventtype,
    // computer, categorytext, empty, empty, description

    size_t eventIdIdx, providerIdx, eventTypeIdx, computerIdx, categoryTextIdx;

    // Check if this is RFC5424 format (MSWinEventLog at tokens[0])
    // Also check if MSWinEventLog is in the original message or syslog tag
    // (RFC3164 syslog parser may move MSWinEventLog to SyslogTAG)
    bool isRFC5424 = false;
    if (tokenCount > 0 && !is_placeholder(tokens[0]) && strcmp(tokens[0], "MSWinEventLog") == 0) {
        isRFC5424 = true;
    } else if (rawMsg != NULL && strstr(rawMsg, "MSWinEventLog") != NULL) {
        // MSWinEventLog found in original message - treat as RFC5424 format
        isRFC5424 = true;
    } else if (pMsg != NULL) {
        // Check if MSWinEventLog is in the syslog tag (RFC3164 parser moves it there)
        uchar *tagBuf = NULL;
        int tagLen = 0;
        getTAG(pMsg, &tagBuf, &tagLen, 0);
        if (tagBuf != NULL && tagLen > 0) {
            // Check if tag contains MSWinEventLog
            if (strstr((const char *)tagBuf, "MSWinEventLog") != NULL) {
                isRFC5424 = true;
            }
        }
    }

    if (isRFC5424) {
        // RFC5424 format
        // Check if MSWinEventLog is in syslog tag (RFC3164 parser moved it)
        bool msWinInTag = false;
        if (pMsg != NULL && (tokenCount == 0 || is_placeholder(tokens[0]) || strcmp(tokens[0], "MSWinEventLog") != 0)) {
            uchar *tagBuf = NULL;
            int tagLen = 0;
            getTAG(pMsg, &tagBuf, &tagLen, 0);
            if (tagBuf != NULL && tagLen > 0 && strstr((const char *)tagBuf, "MSWinEventLog") != NULL) {
                msWinInTag = true;
            }
        }
        if (msWinInTag) {
            // MSWinEventLog in tag: message body structure depends on what syslog parser did
            // If version and channel are present: version=0, channel=1, record=2, timestamp=3, eventid=4, provider=5...
            // If version and channel were removed: timestamp=0, eventid=1, provider=2...
            // Check if tokens[0] looks like a timestamp (contains month name like "Nov", "Mar", "Jun")
            bool versionChannelPresent = tokens_have_version_and_channel(tokens, tokenCount);
            if (versionChannelPresent) {
                // Version and channel present: version=0, channel=1, record=2, timestamp=3, eventid=4, provider=5
                eventIdIdx = 4;
                providerIdx = 5;
                eventTypeIdx = 8;
                computerIdx = 9;
                categoryTextIdx = 10;
            } else {
                // Version and channel removed by syslog parser: timestamp=0, eventid=1, provider=2
                // But tokens[0] might be timestamp, tokens[1] is EventID
                eventIdIdx = 1;
                providerIdx = 2;
                eventTypeIdx = 5;
                computerIdx = 6;
                categoryTextIdx = 7;
            }
        } else {
            // Standard RFC5424: MSWinEventLog, version, channel, record, timestamp, eventid, provider...
            eventIdIdx = 5;
            providerIdx = 6;
            eventTypeIdx = 9;
            computerIdx = 10;
            categoryTextIdx = 11;
        }
    } else {
        // RFC3164 format
        eventIdIdx = 1;
        providerIdx = 2;
        eventTypeIdx = 5;
        computerIdx = 6;
        categoryTextIdx = 7;
    }

    if (tokenCount > eventIdIdx && !is_placeholder(tokens[eventIdIdx])) {
        long long eid;
        if (try_parse_int64(tokens[eventIdIdx], &eid)) {
            ctx->eventId = (int)eid;
            json_add_int64(ctx->event, "EventID", eid);
        } else {
            json_add_string(ctx->event, "EventIDRaw", tokens[eventIdIdx]);
        }
    }
    if (tokenCount > providerIdx && !is_placeholder(tokens[providerIdx]))
        json_add_string(ctx->event, "Provider", tokens[providerIdx]);
    if (tokenCount > eventTypeIdx && !is_placeholder(tokens[eventTypeIdx]))
        json_add_string(ctx->event, "EventType", tokens[eventTypeIdx]);
    // Extract Channel field for RFC5424 format (MSWinEventLog)
    // For RFC3164 format, try to extract from raw message, fall back to "Security"
    if (isRFC5424) {
        // RFC5424 format: channel location depends on where MSWinEventLog is
        if (tokenCount > 0 && !is_placeholder(tokens[0]) && strcmp(tokens[0], "MSWinEventLog") == 0) {
            // Standard RFC5424: MSWinEventLog at tokens[0], channel is at tokens[2]
            if (tokenCount > 2 && !is_placeholder(tokens[2])) {
                json_add_string(ctx->event, "Channel", tokens[2]);
            } else {
                json_add_string(ctx->event, "Channel", "Security");
            }
        } else {
            // MSWinEventLog is in syslog tag (RFC3164 parser moved it)
            // Check if version and channel are present in tokens
            bool channelInTokens = tokens_have_version_and_channel(tokens, tokenCount);
            size_t channelIdx = 1;
            if (channelInTokens && tokenCount > channelIdx && !is_placeholder(tokens[channelIdx])) {
                json_add_string(ctx->event, "Channel", tokens[channelIdx]);
            } else {
                // Fallback: extract from rawMsg (original message before syslog parsing)
                char *channelBuf = extract_channel_from_raw_msg(rawMsg);
                if (channelBuf != NULL) {
                    json_add_string(ctx->event, "Channel", channelBuf);
                    free(channelBuf);
                } else {
                    json_add_string(ctx->event, "Channel", "Security");
                }
            }
        }
    } else {
        // RFC3164 format: try to extract channel from original message
        char *channelBuf = extract_channel_from_raw_msg(rawMsg);
        if (channelBuf != NULL) {
            json_add_string(ctx->event, "Channel", channelBuf);
            free(channelBuf);
        } else {
            json_add_string(ctx->event, "Channel", "Security");
        }
    }
    if (tokenCount > computerIdx && !is_placeholder(tokens[computerIdx]))
        json_add_string(ctx->event, "Computer", tokens[computerIdx]);
    if (tokenCount > categoryTextIdx && !is_placeholder(tokens[categoryTextIdx]))
        json_add_string(ctx->event, "CategoryText", tokens[categoryTextIdx]);
    {
        const char *normalized = getTimeReported(ctx->msg, tplFmtRFC3339Date);
        if (normalized != NULL && *normalized != '\0') {
            struct json_object *timeObj = ensure_object(ctx->event, "TimeCreated");
            if (timeObj != NULL) json_add_string(timeObj, "Normalized", normalized);
        }
    }
    const char *auditResult = NULL;
    if (tokenCount > eventTypeIdx && !is_placeholder(tokens[eventTypeIdx])) auditResult = tokens[eventTypeIdx];
    apply_event_mapping(ctx, auditResult);
}

/**
 * @brief Parse a traditional tab-delimited Snare payload.
 *
 * The payload is provided as an array of tokens split on tab characters. The
 * function builds the JSON representation, processes the message description,
 * and attaches the resulting JSON document to the message.
 *
 * @param pData       Module instance configuration.
 * @param pMsg        Message currently being processed.
 * @param rawMsg      Pointer to the raw payload text for debugging and optional
 *                    emission.
 * @param tokens      Array of tab-delimited tokens.
 * @param tokenCount  Number of valid entries in @p tokens.
 * @return ::RS_RET_OK on success or an error code on allocation failures.
 */
static rsRetVal parse_snare_text(
    instanceData *pData, smsg_t *pMsg, const char *rawMsg, const char *originalMsg, char **tokens, size_t tokenCount) {
    parse_context_t ctx;
    memset(&ctx, 0, sizeof(ctx));
    ctx.inst = pData;
    ctx.msg = pMsg;
    ctx.corePatterns = pData->corePatterns;
    ctx.corePatternCount = pData->corePatternCount;
    ctx.eventPatterns = NULL;
    ctx.eventPatternCount = 0;
    ctx.patternEventId = -1;
    ctx.patternsPrepared = 0;
    ctx.root = json_object_new_object();
    if (ctx.root == NULL) {
        LogError(0, RS_RET_OUT_OF_MEMORY, "mmsnareparse: failed to create root JSON object");
        return RS_RET_OUT_OF_MEMORY;
    }
    ctx.event = json_object_new_object();
    if (ctx.event == NULL) {
        LogError(0, RS_RET_OUT_OF_MEMORY, "mmsnareparse: failed to create event JSON object");
        json_object_put(ctx.root);
        return RS_RET_OUT_OF_MEMORY;
    }
    json_object_object_add(ctx.root, "Event", ctx.event);

    initialize_observability(&ctx);

    dbgprintf("[mmsnareparse DEBUG] Processing SNARE text message with %zu tokens\n", tokenCount);
    if (rawMsg != NULL) {
        dbgprintf("[mmsnareparse DEBUG] Raw message: %s\n", rawMsg);
    }

    if (pData->emitRawPayload && rawMsg != NULL) json_add_string(ctx.root, "Raw", rawMsg);
    populate_event_metadata(&ctx, tokens, tokenCount, originalMsg, pMsg);

    // Determine the correct description index based on message format
    size_t descriptionIdx;
    if (tokenCount > 0 && !is_placeholder(tokens[0]) && strcmp(tokens[0], "MSWinEventLog") == 0) {
        // RFC5424 format
        descriptionIdx = 13;
    } else {
        // RFC3164 format: description is typically the last token or contains "Sysmon"/"Event ID"
        // Try to find the token that contains the description
        descriptionIdx = tokenCount > 0 ? tokenCount - 1 : 0;
        // Look backwards for a token that looks like a description
        // Priority: tokens containing "Sysmon", then "Event ID", then any token with ":"
        size_t sysmonIdx = SIZE_MAX;
        size_t eventIdIdx = SIZE_MAX;
        size_t colonIdx = SIZE_MAX;
        for (size_t i = tokenCount; i > 0; i--) {
            size_t idx = i - 1;
            if (idx < tokenCount && tokens[idx] != NULL) {
                if (strstr(tokens[idx], "Sysmon") != NULL) {
                    sysmonIdx = idx;
                    break;  // Highest priority, use this
                } else if (strstr(tokens[idx], "Event ID") != NULL && eventIdIdx == SIZE_MAX) {
                    eventIdIdx = idx;
                } else if (strchr(tokens[idx], ':') != NULL && colonIdx == SIZE_MAX) {
                    colonIdx = idx;
                }
            }
        }
        if (sysmonIdx != SIZE_MAX) {
            descriptionIdx = sysmonIdx;
        } else if (eventIdIdx != SIZE_MAX) {
            descriptionIdx = eventIdIdx;
        } else if (colonIdx != SIZE_MAX) {
            descriptionIdx = colonIdx;
        }
    }

    if (tokenCount > descriptionIdx && !is_placeholder(tokens[descriptionIdx])) {
        if (ctx.eventId == 0 && ctx.event != NULL) {
            struct json_object *eventIdObj;
            if (json_object_object_get_ex(ctx.event, "EventID", &eventIdObj) &&
                json_object_is_type(eventIdObj, json_type_int)) {
                ctx.eventId = (int)json_object_get_int(eventIdObj);
            }
        }
        if (ctx.eventId == 4624 || ctx.eventId == 4625) {
            const char *required[] = {"Security ID", "Account Name", "Account Domain"};
            rsRetVal vr =
                validate_required_fields(tokens[descriptionIdx], required, ARRAY_SIZE(required), &ctx.validation);
            if (vr != RS_RET_OK && ctx.validation.mode == VALIDATION_STRICT) {
                json_object_put(ctx.root);
                return vr;
            }
        }
        // For descriptions that might span multiple tokens, try to concatenate them
        // Start from descriptionIdx and concatenate remaining tokens
        // First calculate total length to avoid inefficient realloc in loop
        size_t totalLen = 0;
        for (size_t i = descriptionIdx; i < tokenCount; i++) {
            if (tokens[i] != NULL && !is_placeholder(tokens[i])) {
                if (totalLen > 0) {
                    totalLen++;  // For space separator
                }
                totalLen += strlen(tokens[i]);
            }
        }

        char *fullDescription = NULL;
        if (totalLen > 0) {
            fullDescription = malloc(totalLen + 1);  // +1 for null terminator
            if (fullDescription != NULL) {
                char *p = fullDescription;
                for (size_t i = descriptionIdx; i < tokenCount; i++) {
                    if (tokens[i] != NULL && !is_placeholder(tokens[i])) {
                        if (p != fullDescription) {
                            *p++ = ' ';  // Add space between tokens
                        }
                        size_t tokenLen = strlen(tokens[i]);
                        memcpy(p, tokens[i], tokenLen);
                        p += tokenLen;
                    }
                }
                *p = '\0';
            }
        }
        if (fullDescription != NULL) {
            dbgprintf(
                "[mmsnareparse DEBUG] parse_snare_text: calling parse_description with concatenated description (from "
                "token %zu): '%s'\n",
                descriptionIdx, fullDescription);
            parse_description(&ctx, fullDescription);
            free(fullDescription);
        } else if (tokenCount > descriptionIdx && !is_placeholder(tokens[descriptionIdx])) {
            dbgprintf("[mmsnareparse DEBUG] parse_snare_text: calling parse_description with tokens[%zu]='%s'\n",
                      descriptionIdx, tokens[descriptionIdx]);
            parse_description(&ctx, tokens[descriptionIdx]);
        }
    } else {
        dbgprintf(
            "[mmsnareparse DEBUG] parse_snare_text: not calling parse_description (tokenCount=%zu, "
            "tokens[%zu]='%s')\n",
            tokenCount, descriptionIdx, tokenCount > descriptionIdx ? tokens[descriptionIdx] : "N/A");
    }
    if (ctx.unparsed == NULL && pData->emitDebugJson) ctx.unparsed = json_object_new_array();

    rsRetVal vr = validate_field_count(rawMsg, ctx.successfulParses, ctx.totalFields, &ctx.validation);
    if (vr != RS_RET_OK && ctx.validation.mode == VALIDATION_STRICT) {
        json_object_put(ctx.root);
        return vr;
    }

    finalize_parsing_stats(&ctx);

    const char *json_str = json_object_to_json_string_ext(ctx.root, JSON_C_TO_STRING_PRETTY);
    if (json_str != NULL) {
        dbgprintf("[mmsnareparse DEBUG] Final parsed JSON:\n%s\n", json_str);
    }
    msgAddJSON(pMsg, pData->container, ctx.root, 0, 0);
    return RS_RET_OK;
}

/**
 * @brief Ingest EventData content from a JSON Snare payload.
 *
 * @param ctx       Active parsing context.
 * @param eventData Parsed JSON object containing EventData members.
 */
static void parse_json_event_data(parse_context_t *ctx, struct json_object *eventData) {
    struct json_object_iterator it = json_object_iter_begin(eventData);
    struct json_object_iterator itEnd = json_object_iter_end(eventData);
    while (!json_object_iter_equal(&it, &itEnd)) {
        const char *key = json_object_iter_peek_name(&it);
        struct json_object *valObj = json_object_iter_peek_value(&it);
        if (key != NULL && valObj != NULL) {
            if (json_object_is_type(valObj, json_type_string)) {
                handle_general_key(ctx, key, json_object_get_string(valObj));
            } else if (json_object_is_type(valObj, json_type_int)) {
                char buf[64];
                snprintf(buf, sizeof(buf), "%lld", (long long)json_object_get_int64(valObj));
                handle_general_key(ctx, key, buf);
            } else if (json_object_is_type(valObj, json_type_boolean)) {
                handle_general_key(ctx, key, json_object_get_boolean(valObj) ? "true" : "false");
            } else if (json_object_is_type(valObj, json_type_double)) {
                char buf[64];
                snprintf(buf, sizeof(buf), "%f", json_object_get_double(valObj));
                handle_general_key(ctx, key, buf);
            }
        }
        json_object_iter_next(&it);
    }
}

/**
 * @brief Parse a Snare payload delivered as a JSON document.
 *
 * @param pData       Module instance configuration.
 * @param pMsg        Message currently being processed.
 * @param jsonPayload JSON string to parse.
 * @return ::RS_RET_OK on success or a negative error value.
 */
static rsRetVal parse_snare_json(instanceData *pData, smsg_t *pMsg, const char *jsonPayload) {
    parse_context_t ctx;
    struct json_tokener *tokener;
    struct json_object *payload;
    struct json_object *value;
    memset(&ctx, 0, sizeof(ctx));
    ctx.inst = pData;
    ctx.msg = pMsg;
    ctx.corePatterns = pData->corePatterns;
    ctx.corePatternCount = pData->corePatternCount;
    ctx.eventPatterns = NULL;
    ctx.eventPatternCount = 0;
    ctx.patternEventId = -1;
    ctx.patternsPrepared = 0;
    ctx.root = json_object_new_object();
    if (ctx.root == NULL) {
        LogError(0, RS_RET_OUT_OF_MEMORY, "mmsnareparse: failed to create root JSON object");
        return RS_RET_OUT_OF_MEMORY;
    }
    ctx.event = json_object_new_object();
    if (ctx.event == NULL) {
        LogError(0, RS_RET_OUT_OF_MEMORY, "mmsnareparse: failed to create event JSON object");
        json_object_put(ctx.root);
        return RS_RET_OUT_OF_MEMORY;
    }
    json_object_object_add(ctx.root, "Event", ctx.event);

    dbgprintf("[mmsnareparse DEBUG] Processing SNARE JSON message\n");
    dbgprintf("[mmsnareparse DEBUG] JSON payload: %s\n", jsonPayload);

    tokener = json_tokener_new();
    if (tokener == NULL) {
        LogError(0, RS_RET_OUT_OF_MEMORY, "mmsnareparse: failed to create JSON tokener");
        json_object_put(ctx.root);
        return RS_RET_OUT_OF_MEMORY;
    }
    payload = json_tokener_parse_ex(tokener, jsonPayload, (int)strlen(jsonPayload));
    if (payload == NULL) {
        LogError(0, RS_RET_COULD_NOT_PARSE, "mmsnareparse: failed to parse JSON payload: %s", jsonPayload);
        dbgprintf("[mmsnareparse DEBUG] Failed to parse JSON payload\n");
        json_tokener_free(tokener);
        json_add_string(ctx.root, "RawJSON", jsonPayload);
        msgAddJSON(pMsg, pData->container, ctx.root, 0, 0);
        return RS_RET_OK;
    }

    const char *json_str = json_object_to_json_string_ext(payload, JSON_C_TO_STRING_PRETTY);
    if (json_str != NULL) {
        dbgprintf("[mmsnareparse DEBUG] Parsed JSON payload:\n%s\n", json_str);
    }

    if (pData->emitRawPayload) json_object_object_add(ctx.root, "RawJSON", json_object_get(payload));
    if (json_object_object_get_ex(payload, "EventID", &value)) {
        long long eid = json_object_get_int64(value);
        json_add_int64(ctx.event, "EventID", eid);
        ctx.eventId = (int)eid;
    }
    if (json_object_object_get_ex(payload, "Channel", &value) && json_object_is_type(value, json_type_string))
        json_add_string(ctx.event, "Channel", json_object_get_string(value));
    if (json_object_object_get_ex(payload, "Provider", &value) && json_object_is_type(value, json_type_string))
        json_add_string(ctx.event, "Provider", json_object_get_string(value));
    /* Populate normalized time from the syslog envelope, mirroring text path */
    {
        const char *normalized = getTimeReported(ctx.msg, tplFmtRFC3339Date);
        if (normalized != NULL && *normalized != '\0') {
            struct json_object *timeObj = ensure_object(ctx.event, "TimeCreated");
            if (timeObj != NULL) json_add_string(timeObj, "Normalized", normalized);
        }
    }
    if (json_object_object_get_ex(payload, "EventTime", &value) && json_object_is_type(value, json_type_string)) {
        struct json_object *timeObj = ensure_object(ctx.event, "TimeCreated");
        if (timeObj != NULL) json_add_string(timeObj, "Raw", json_object_get_string(value));
    }
    if (json_object_object_get_ex(payload, "Message", &value) && json_object_is_type(value, json_type_string)) {
        json_add_string(ctx.root, "Summary", json_object_get_string(value));
        ctx.summarySet = 1;
    }
    if (json_object_object_get_ex(payload, "EventData", &value) && json_object_is_type(value, json_type_object))
        parse_json_event_data(&ctx, value);
    if (json_object_object_get_ex(payload, "System", &value) && json_object_is_type(value, json_type_object)) {
        struct json_object *systemObj = value;
        struct json_object *inner;
        if (json_object_object_get_ex(systemObj, "EventRecordID", &inner) &&
            json_object_is_type(inner, json_type_string))
            json_add_string(ctx.event, "RecordNumberRaw", json_object_get_string(inner));
        if (json_object_object_get_ex(systemObj, "Level", &inner) && json_object_is_type(inner, json_type_int))
            json_add_int64(ctx.event, "Level", json_object_get_int64(inner));
        if (json_object_object_get_ex(systemObj, "Computer", &inner) && json_object_is_type(inner, json_type_string))
            json_add_string(ctx.event, "Computer", json_object_get_string(inner));
    }
    apply_event_mapping(&ctx, NULL);

    rsRetVal vr = validate_field_count(jsonPayload, ctx.successfulParses, ctx.totalFields, &ctx.validation);
    if (vr != RS_RET_OK && ctx.validation.mode == VALIDATION_STRICT) {
        json_tokener_free(tokener);
        json_object_put(payload);
        json_object_put(ctx.root);
        return vr;
    }

    finalize_parsing_stats(&ctx);

    const char *json_str2 = json_object_to_json_string_ext(ctx.root, JSON_C_TO_STRING_PRETTY);
    if (json_str2 != NULL) {
        dbgprintf("[mmsnareparse DEBUG] Final parsed JSON:\n%s\n", json_str2);
    }

    json_tokener_free(tokener);
    msgAddJSON(pMsg, pData->container, ctx.root, 0, 0);
    json_object_put(payload);
    return RS_RET_OK;
}

/**
 * @brief Detect and truncate trailing extra-data section if pattern is configured.
 *
 * Searches for the configured pattern in the message. If found in a trailing
 * position (after the last tab-separated token), truncates the message at that
 * point and optionally stores the truncated content as a message property.
 *
 * @param pData      Module instance configuration.
 * @param mutableMsg Mutable copy of the message payload (will be modified if truncation occurs).
 * @param msgLen     Pointer to the length of mutableMsg (will be updated if truncation occurs).
 * @return Pointer to the truncated extra-data section (if found and truncated), NULL otherwise.
 *         The caller is responsible for freeing this memory if not NULL.
 */
static char *detect_and_truncate_trailing_extradata(instanceData *pData, char *mutableMsg, size_t *msgLen) {
    char *extradataSection = NULL;

    if (pData->ignoreTrailingPattern == NULL || mutableMsg == NULL) {
        return NULL;
    }

    /* Find the last tab character to identify the end of the last token */
    char *lastTab = strrchr(mutableMsg, '\t');
    if (lastTab == NULL) {
        /* No tabs found - this is an edge case as SNARE format normally uses tab-separated values.
         * Valid Snare messages will always have tabs and be handled in the else block below,
         * where truncation is safely anchored to the last token.
         *
         * This fallback path attempts to remove trailing enrichment sections from malformed or
         * non-standard messages. Be conservative: only truncate if pattern appears in the
         * trailing portion (last 20% or searchLimit chars, whichever is smaller) to avoid
         * accidentally removing legitimate message content. */
        size_t msgLenVal = strlen(mutableMsg);
        if (pData->ignoreTrailingPattern_isRegex) {
            /* For regex mode, search within the trailing portion (last 20% or searchLimit chars) */
            size_t trailingSearchLen = msgLenVal / 5; /* Last 20% */
            if (trailingSearchLen > pData->searchLimit) {
                trailingSearchLen = pData->searchLimit;
            }
            if (trailingSearchLen > msgLenVal) {
                trailingSearchLen = msgLenVal;
            }
            char *searchStart = mutableMsg + msgLenVal - trailingSearchLen;
            if (searchStart < mutableMsg) {
                searchStart = mutableMsg;
            }
            /* Try regex match on the trailing portion */
            const int isMatch = !regexec(&pData->ignoreTrailingPattern_preg, searchStart, 0, NULL, 0);
            if (isMatch) {
                /* Found pattern in trailing position - save it and truncate before it */
                extradataSection = strdup(searchStart);
                if (extradataSection == NULL) {
                    return NULL; /* out of memory */
                }
                *searchStart = '\0';
                if (msgLen != NULL) {
                    *msgLen = searchStart - mutableMsg;
                }
                return extradataSection;
            }
        } else {
            /* Static pattern mode */
            const char *pattern = (const char *)pData->ignoreTrailingPattern;
            size_t patternLen = strlen(pattern);
            if (patternLen == 0) {
                return NULL;
            }
            if (msgLenVal < patternLen) {
                return NULL;
            }
            size_t trailingSearchLen = msgLenVal / 5; /* Last 20% */
            if (trailingSearchLen > pData->searchLimit) {
                trailingSearchLen = pData->searchLimit;
            }
            if (trailingSearchLen < patternLen) {
                trailingSearchLen = patternLen;
            }
            /* Search backwards from end within the trailing portion only */
            char *searchStart = mutableMsg + msgLenVal - trailingSearchLen;
            for (char *searchPos = mutableMsg + msgLenVal - patternLen; searchPos >= searchStart; searchPos--) {
                if (memcmp(searchPos, pattern, patternLen) == 0) {
                    /* Found pattern in trailing position - save it and truncate before it */
                    extradataSection = strdup(searchPos);
                    if (extradataSection == NULL) {
                        return NULL; /* out of memory */
                    }
                    *searchPos = '\0';
                    if (msgLen != NULL) {
                        *msgLen = searchPos - mutableMsg;
                    }
                    return extradataSection;
                }
            }
        }
        return NULL;
    }

    /* Pattern must appear after the last tab to be considered trailing */
    char *searchStart = lastTab + 1;
    const size_t trailingTokenLen = strlen(searchStart);
    size_t trailingSearchLen = pData->searchLimit;
    if (trailingSearchLen > trailingTokenLen) {
        trailingSearchLen = trailingTokenLen;
    }
    char *cappedSearchStart = searchStart + trailingTokenLen - trailingSearchLen;

    if (pData->ignoreTrailingPattern_isRegex) {
        /* Regex mode: bound regex input to searchLimit trailing bytes to
         * reduce risk from expensive non-matching expressions on untrusted
         * message content while preserving start-anchored token patterns. */
        char savedChar = '\0';
        const sbool tokenWasTruncated = trailingTokenLen > pData->searchLimit;
        if (tokenWasTruncated) {
            savedChar = searchStart[pData->searchLimit];
            searchStart[pData->searchLimit] = '\0';
        }

        const int regexecFlags = tokenWasTruncated ? REG_NOTEOL : 0;
        const int isMatch = !regexec(&pData->ignoreTrailingPattern_preg, searchStart, 0, NULL, regexecFlags);
        if (tokenWasTruncated) {
            searchStart[pData->searchLimit] = savedChar;
        }
        if (isMatch) {
            /* Pattern found in trailing position - truncate at the start of the last token
             * (after the last tab) to remove the entire enrichment section including any
             * preceding content in that token (e.g., dynamic numbers before the pattern) */
            /* Save the extra-data section before truncating */
            extradataSection = strdup(searchStart);
            if (extradataSection == NULL) {
                return NULL;
            }
            /* Now truncate at the last tab */
            *lastTab = '\0';
            if (msgLen != NULL) {
                *msgLen = lastTab - mutableMsg;
            }
            return extradataSection;
        }
    } else {
        /* Static pattern mode: use strstr */
        const char *pattern = (const char *)pData->ignoreTrailingPattern;
        /* Defensive check: ensure pattern is not empty to avoid unintended matches */
        size_t patternLen = strlen(pattern);
        if (patternLen > 0 && trailingSearchLen >= patternLen) {
            char *patternPos = strstr(cappedSearchStart, pattern);

            if (patternPos != NULL) {
                /* Pattern found in trailing position - truncate at the start of the last token
                 * (after the last tab) to remove the entire enrichment section including any
                 * preceding content in that token (e.g., dynamic numbers before the pattern) */
                /* Save the extra-data section before truncating */
                extradataSection = strdup(searchStart);
                if (extradataSection == NULL) {
                    return NULL;
                }
                /* Now truncate at the last tab */
                *lastTab = '\0';
                if (msgLen != NULL) {
                    *msgLen = lastTab - mutableMsg;
                }
                return extradataSection;
            }
        }
    }

    return NULL;
}

/**
 * @brief Detect the payload type, parse it, and attach JSON metadata.
 *
 * This is the main entry point for each message processed by the action. It
 * locates the Snare payload, determines whether the content is text or JSON,
 * and invokes the appropriate parser.
 *
 * @param pData   Module instance configuration.
 * @param pMsg    Message currently being processed.
 * @param msgText Raw text buffer owned by the message.
 * @return ::RS_RET_OK when parsing succeeds or ::RS_RET_COULD_NOT_PARSE when
 *         no Snare payload could be located.
 */
static rsRetVal process_message(instanceData *pData, smsg_t *pMsg, uchar *msgText) {
    rsRetVal iRet = RS_RET_COULD_NOT_PARSE;
    char *mutableMsg;
    char *cursor;
    char *tokens[32];
    size_t tokenCount = 0;
    const char *rawMsg;
    const char *payloadStart;
    if (msgText == NULL) return RS_RET_COULD_NOT_PARSE;
    payloadStart = locate_snare_payload((const char *)msgText, pMsg);
    if (payloadStart == NULL) return RS_RET_COULD_NOT_PARSE;
    // rawMsg should be the original full message for channel extraction
    // Try to get raw message before syslog parsing if MSWinEventLog is in tag
    if (pMsg != NULL) {
        uchar *tagBuf = NULL;
        int tagLen = 0;
        getTAG(pMsg, &tagBuf, &tagLen, 0);
        if (tagBuf != NULL && tagLen > 0 && strstr((const char *)tagBuf, "MSWinEventLog") != NULL) {
            // MSWinEventLog is in tag - try to get raw message before syslog parsing
            uchar *rawBuf = NULL;
            int rawLen = 0;
            getRawMsg(pMsg, &rawBuf, &rawLen);
            if (rawBuf != NULL && rawLen > 0) {
                // Check if raw message contains MSWinEventLog
                if (strstr((const char *)rawBuf, "MSWinEventLog") != NULL) {
                    rawMsg = (const char *)rawBuf;
                } else {
                    rawMsg = (const char *)msgText;
                }
            } else {
                rawMsg = (const char *)msgText;
            }
        } else {
            rawMsg = (const char *)msgText;
        }
    } else {
        rawMsg = (const char *)msgText;
    }
    mutableMsg = strdup(payloadStart);
    if (mutableMsg == NULL) {
        LogError(0, RS_RET_OUT_OF_MEMORY, "mmsnareparse: failed to duplicate message text");
        return RS_RET_OUT_OF_MEMORY;
    }
    unescape_hash_sequences(mutableMsg);
    normalize_literal_tabs(mutableMsg);
    dbgprintf("[mmsnareparse DEBUG] After unescaping: '%s'\n", mutableMsg);

    /* Detect and truncate trailing extra-data section if pattern is configured */
    char *extradataSection = NULL;
    size_t msgLen = strlen(mutableMsg);
    extradataSection = detect_and_truncate_trailing_extradata(pData, mutableMsg, &msgLen);
    if (extradataSection != NULL) {
        dbgprintf("[mmsnareparse DEBUG] Truncated trailing extra-data section: '%s'\n", extradataSection);
        /* Optionally expose the extra-data section as a message property */
        if (pMsg != NULL) {
            struct json_object *extradataJson = json_object_new_string(extradataSection);
            if (extradataJson != NULL) {
                msgAddJSON(pMsg, (uchar *)"!extradata_section", extradataJson, 0, 0);
            }
        }
    }

    cursor = mutableMsg;
    while (cursor != NULL && tokenCount < ARRAY_SIZE(tokens)) {
        tokens[tokenCount++] = cursor;
        char *tab = strchr(cursor, '\t');
        if (tab == NULL) break;
        *tab = '\0';
        cursor = tab + 1;
    }

    dbgprintf("[mmsnareparse DEBUG] Processing message with %zu tokens\n", tokenCount);
    dbgprintf("[mmsnareparse DEBUG] Message type: %s\n",
              (tokenCount >= 3 && !strcmp(tokens[1], "0") && tokens[2][0] == '{') ? "JSON" : "Text");

    // Debug: Print first few tokens
    for (size_t i = 0; i < tokenCount && i < 5; i++) {
        dbgprintf("[mmsnareparse DEBUG] Token %zu: '%s'\n", i, tokens[i]);
    }

    if (tokenCount >= 3 && !strcmp(tokens[1], "0") && tokens[2][0] == '{') {
        iRet = parse_snare_json(pData, pMsg, tokens[2]);
    } else if (tokenCount >= 2) {
        // Pass rawMsg as originalMsg so populate_event_metadata can extract channel from it
        iRet = parse_snare_text(pData, pMsg, rawMsg, rawMsg, tokens, tokenCount);
    }
    free(mutableMsg);
    free(extradataSection);
    return iRet;
}

DEF_OMOD_STATIC_DATA;

static struct cnfparamdescr modpdescr[] = {{"definition.file", eCmdHdlrString, 0},
                                           {"definition.json", eCmdHdlrString, 0},
                                           {"runtime.config", eCmdHdlrString, 0},
                                           {"validation.mode", eCmdHdlrString, 0},
                                           {"ignoreTrailingPattern", eCmdHdlrString, 0},
                                           {"ignoreTrailingPattern.regex", eCmdHdlrString, 0},
                                           {"ignoreTrailingPattern.searchWindow", eCmdHdlrInt, 0}};
static struct cnfparamblk modpblk = {CNFPARAMBLK_VERSION, ARRAY_SIZE(modpdescr), modpdescr};

static struct cnfparamdescr actpdescr[] = {{"container", eCmdHdlrString, 0},
                                           {"rootpath", eCmdHdlrString, 0},
                                           {"template", eCmdHdlrGetWord, 0},
                                           {"enable.network", eCmdHdlrBinary, 0},
                                           {"enable.laps", eCmdHdlrBinary, 0},
                                           {"enable.tls", eCmdHdlrBinary, 0},
                                           {"enable.wdac", eCmdHdlrBinary, 0},
                                           {"emit.rawpayload", eCmdHdlrBinary, 0},
                                           {"emit.debugjson", eCmdHdlrBinary, 0},
                                           {"debugjson", eCmdHdlrBinary, 0},
                                           {"definition.file", eCmdHdlrString, 0},
                                           {"definition.json", eCmdHdlrString, 0},
                                           {"runtime.config", eCmdHdlrString, 0},
                                           {"validation.mode", eCmdHdlrString, 0},
                                           {"validation_mode", eCmdHdlrString, 0},
                                           {"ignoreTrailingPattern", eCmdHdlrString, 0},
                                           {"ignoreTrailingPattern.regex", eCmdHdlrString, 0},
                                           {"ignoreTrailingPattern.searchWindow", eCmdHdlrInt, 0}};
static struct cnfparamblk actpblk = {CNFPARAMBLK_VERSION, ARRAY_SIZE(actpdescr), actpdescr};

BEGINbeginCnfLoad
    CODESTARTbeginCnfLoad;
    loadModConf = pModConf;
    pModConf->pConf = pConf;
    free(pModConf->definitionFile);
    pModConf->definitionFile = NULL;
    free(pModConf->definitionJson);
    pModConf->definitionJson = NULL;
    free(pModConf->runtimeConfigFile);
    pModConf->runtimeConfigFile = NULL;
    free(pModConf->ignoreTrailingPattern);
    pModConf->ignoreTrailingPattern = NULL;
    free(pModConf->ignoreTrailingPatternRegex);
    pModConf->ignoreTrailingPatternRegex = NULL;
    pModConf->searchLimit = 256; /* default search window limit */
    init_validation_context(&pModConf->validationTemplate);
ENDbeginCnfLoad

BEGINsetModCnf
    struct cnfparamvals *pvals = NULL;
    int i;
    CODESTARTsetModCnf;
    pvals = nvlstGetParams(lst, &modpblk, NULL);
    if (pvals == NULL) {
        LogError(0, RS_RET_MISSING_CNFPARAMS, "mmsnareparse: error processing module config parameters [module(...)]");
        ABORT_FINALIZE(RS_RET_MISSING_CNFPARAMS);
    }
    if (Debug) {
        dbgprintf("module (global) param blk for mmsnareparse:\n");
        cnfparamsPrint(&modpblk, pvals);
    }
    for (i = 0; i < (int)modpblk.nParams; ++i) {
        if (!pvals[i].bUsed) continue;
        if (!strcmp(modpblk.descr[i].name, "definition.file")) {
            char *value;
            CHKmalloc(value = es_str2cstr(pvals[i].val.d.estr, NULL));
            free(loadModConf->definitionFile);
            loadModConf->definitionFile = value;
        } else if (!strcmp(modpblk.descr[i].name, "definition.json")) {
            char *value;
            CHKmalloc(value = es_str2cstr(pvals[i].val.d.estr, NULL));
            free(loadModConf->definitionJson);
            loadModConf->definitionJson = value;
        } else if (!strcmp(modpblk.descr[i].name, "runtime.config")) {
            char *value;
            CHKmalloc(value = es_str2cstr(pvals[i].val.d.estr, NULL));
            free(loadModConf->runtimeConfigFile);
            loadModConf->runtimeConfigFile = value;
        } else if (!strcmp(modpblk.descr[i].name, "validation.mode")) {
            char *mode;
            CHKmalloc(mode = es_str2cstr(pvals[i].val.d.estr, NULL));
            rsRetVal r;
            validation_mode_t parsedMode;
            r = parse_validation_mode(mode, &parsedMode);
            free(mode);
            if (r != RS_RET_OK) {
                ABORT_FINALIZE(r);
            }
            loadModConf->validationTemplate.mode = parsedMode;
        } else if (!strcmp(modpblk.descr[i].name, "ignoreTrailingPattern")) {
            char *value;
            CHKmalloc(value = es_str2cstr(pvals[i].val.d.estr, NULL));
            free(loadModConf->ignoreTrailingPattern);
            loadModConf->ignoreTrailingPattern = (uchar *)value;
        } else if (!strcmp(modpblk.descr[i].name, "ignoreTrailingPattern.regex")) {
            char *value;
            CHKmalloc(value = es_str2cstr(pvals[i].val.d.estr, NULL));
            free(loadModConf->ignoreTrailingPatternRegex);
            loadModConf->ignoreTrailingPatternRegex = (uchar *)value;
        } else if (!strcmp(modpblk.descr[i].name, "ignoreTrailingPattern.searchWindow")) {
            long long value = pvals[i].val.d.n;
            if (value < 1) {
                LogError(0, RS_RET_PARAM_ERROR, "mmsnareparse: ignoreTrailingPattern.searchWindow must be >= 1");
                ABORT_FINALIZE(RS_RET_PARAM_ERROR);
            }
            loadModConf->searchLimit = (size_t)value;
        } else {
            dbgprintf("mmsnareparse: unhandled module parameter '%s'\n", modpblk.descr[i].name);
        }
    }
    /* Check mutual exclusivity */
    if (loadModConf->ignoreTrailingPattern != NULL && loadModConf->ignoreTrailingPatternRegex != NULL) {
        LogError(0, RS_RET_PARAM_NOT_PERMITTED,
                 "mmsnareparse: ignoreTrailingPattern and ignoreTrailingPattern.regex are mutually exclusive");
        ABORT_FINALIZE(RS_RET_PARAM_NOT_PERMITTED);
    }
finalize_it:
    if (pvals != NULL) cnfparamvalsDestruct(pvals, &modpblk);
ENDsetModCnf

BEGINendCnfLoad
    CODESTARTendCnfLoad;
ENDendCnfLoad

BEGINcheckCnf
    CODESTARTcheckCnf;
ENDcheckCnf

BEGINactivateCnf
    CODESTARTactivateCnf;
    runModConf = pModConf;
ENDactivateCnf

BEGINfreeCnf
    CODESTARTfreeCnf;
    if (pModConf != NULL) {
        free(pModConf->definitionFile);
        pModConf->definitionFile = NULL;
        free(pModConf->definitionJson);
        pModConf->definitionJson = NULL;
        free(pModConf->runtimeConfigFile);
        pModConf->runtimeConfigFile = NULL;
        free(pModConf->ignoreTrailingPattern);
        pModConf->ignoreTrailingPattern = NULL;
        free(pModConf->ignoreTrailingPatternRegex);
        pModConf->ignoreTrailingPatternRegex = NULL;
    }
ENDfreeCnf

BEGINcreateInstance
    CODESTARTcreateInstance;
ENDcreateInstance

BEGINcreateWrkrInstance
    CODESTARTcreateWrkrInstance;
ENDcreateWrkrInstance

BEGINisCompatibleWithFeature
    CODESTARTisCompatibleWithFeature;
ENDisCompatibleWithFeature

BEGINfreeInstance
    CODESTARTfreeInstance;
    free(pData->container);
    free(pData->ignoreTrailingPattern);
    if (pData->ignoreTrailingPattern_isRegex) {
        regfree(&pData->ignoreTrailingPattern_preg);
    }
    free_runtime_tables(pData);
    free_runtime_config(&pData->runtimeConfig);
ENDfreeInstance

BEGINfreeWrkrInstance
    CODESTARTfreeWrkrInstance;
ENDfreeWrkrInstance

/**
 * @brief Populate default configuration values for a new instance.
 */
static inline void setInstParamDefaults(instanceData *pData) {
    pData->container = NULL;
    pData->enableNetwork = 1;
    pData->enableLaps = 1;
    pData->enableTls = 1;
    pData->enableWdac = 1;
    pData->emitRawPayload = 1;
    pData->emitDebugJson = 0;
    pData->ignoreTrailingPattern = NULL;
    pData->ignoreTrailingPattern_isRegex = 0;
    pData->searchLimit = 256; /* default search window limit */
    memset(&pData->ignoreTrailingPattern_preg, 0, sizeof(regex_t));
    init_validation_context(&pData->validationTemplate);
    init_runtime_config(&pData->runtimeConfig);
    pData->sectionDescriptors = NULL;
    pData->sectionDescriptorCount = 0;
    pData->corePatterns = NULL;
    pData->corePatternCount = 0;
    pData->eventFieldMappings = NULL;
    pData->eventFieldMappingCount = 0;
    pData->eventMappings = NULL;
    pData->eventMappingCount = 0;
}

BEGINnewActInst
    struct cnfparamvals *pvals;
    int i;
    char *definitionFile = NULL;
    char *definitionJson = NULL;
    char *runtimeConfigFile = NULL;
    char *templateName = NULL;
    sbool hasStaticPattern = 0;
    sbool hasRegexPattern = 0;
    CODESTARTnewActInst;
    if ((pvals = nvlstGetParams(lst, &actpblk, NULL)) == NULL) {
        LogError(0, RS_RET_MISSING_CNFPARAMS, "mmsnareparse: missing configuration parameters");
        ABORT_FINALIZE(RS_RET_MISSING_CNFPARAMS);
    }
    CHKiRet(createInstance(&pData));
    setInstParamDefaults(pData);
    if (loadModConf != NULL) {
        pData->validationTemplate = loadModConf->validationTemplate;
        pData->searchLimit = loadModConf->searchLimit;
        if (loadModConf->runtimeConfigFile != NULL) {
            CHKiRet(load_configuration(&pData->runtimeConfig, loadModConf->runtimeConfigFile));
        }
        if (loadModConf->ignoreTrailingPattern != NULL) {
            free(pData->ignoreTrailingPattern);
            pData->ignoreTrailingPattern = (uchar *)strdup((char *)loadModConf->ignoreTrailingPattern);
            if (pData->ignoreTrailingPattern == NULL) {
                ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
            }
        }
        if (loadModConf->ignoreTrailingPatternRegex != NULL) {
            free(pData->ignoreTrailingPattern);
            pData->ignoreTrailingPattern = (uchar *)strdup((char *)loadModConf->ignoreTrailingPatternRegex);
            if (pData->ignoreTrailingPattern == NULL) {
                ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
            }
            pData->ignoreTrailingPattern_isRegex = 1;
        }
    }
    for (i = 0; i < (int)actpblk.nParams; ++i) {
        if (!pvals[i].bUsed) continue;
        if (!strcmp(actpblk.descr[i].name, "container") || !strcmp(actpblk.descr[i].name, "rootpath")) {
            free(pData->container);
            CHKmalloc(pData->container = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
            if (pData->container != NULL && pData->container[0] == '$')
                memmove(pData->container, pData->container + 1, strlen((char *)pData->container));
        } else if (!strcmp(actpblk.descr[i].name, "template")) {
            free(templateName);
            CHKmalloc(templateName = es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "enable.network")) {
            pData->enableNetwork = (sbool)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "enable.laps")) {
            pData->enableLaps = (sbool)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "enable.tls")) {
            pData->enableTls = (sbool)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "enable.wdac")) {
            pData->enableWdac = (sbool)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "emit.rawpayload")) {
            pData->emitRawPayload = (sbool)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "emit.debugjson") || !strcmp(actpblk.descr[i].name, "debugjson")) {
            pData->emitDebugJson = (sbool)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "definition.file")) {
            free(definitionFile);
            CHKmalloc(definitionFile = es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "definition.json")) {
            free(definitionJson);
            CHKmalloc(definitionJson = es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "runtime.config")) {
            free(runtimeConfigFile);
            CHKmalloc(runtimeConfigFile = es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "validation.mode") ||
                   !strcmp(actpblk.descr[i].name, "validation_mode")) {
            char *mode;
            CHKmalloc(mode = es_str2cstr(pvals[i].val.d.estr, NULL));
            if (mode == NULL) {
                ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
            }
            CHKiRet(set_validation_mode(pData, mode));
            free(mode);
        } else if (!strcmp(actpblk.descr[i].name, "ignoreTrailingPattern")) {
            hasStaticPattern = 1;
            char *value;
            CHKmalloc(value = es_str2cstr(pvals[i].val.d.estr, NULL));
            if (value == NULL) {
                ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
            }
            free(pData->ignoreTrailingPattern);
            pData->ignoreTrailingPattern = (uchar *)value;
            pData->ignoreTrailingPattern_isRegex = 0;
        } else if (!strcmp(actpblk.descr[i].name, "ignoreTrailingPattern.regex")) {
            hasRegexPattern = 1;
            char *value;
            CHKmalloc(value = es_str2cstr(pvals[i].val.d.estr, NULL));
            if (value == NULL) {
                ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
            }
            free(pData->ignoreTrailingPattern);
            pData->ignoreTrailingPattern = (uchar *)value;
            pData->ignoreTrailingPattern_isRegex = 1;
        } else if (!strcmp(actpblk.descr[i].name, "ignoreTrailingPattern.searchWindow")) {
            long long value = pvals[i].val.d.n;
            if (value < 1) {
                LogError(0, RS_RET_PARAM_ERROR, "mmsnareparse: ignoreTrailingPattern.searchWindow must be >= 1");
                ABORT_FINALIZE(RS_RET_PARAM_ERROR);
            }
            pData->searchLimit = (size_t)value;
        }
    }
    /* Check mutual exclusivity - both action parameters cannot be set */
    if (hasStaticPattern && hasRegexPattern) {
        LogError(0, RS_RET_PARAM_NOT_PERMITTED,
                 "mmsnareparse: ignoreTrailingPattern and ignoreTrailingPattern.regex are mutually exclusive");
        ABORT_FINALIZE(RS_RET_PARAM_NOT_PERMITTED);
    }
    /* Also check if module config and action config conflict */
    if (loadModConf != NULL) {
        if ((loadModConf->ignoreTrailingPattern != NULL && hasRegexPattern) ||
            (loadModConf->ignoreTrailingPatternRegex != NULL && hasStaticPattern)) {
            LogError(0, RS_RET_PARAM_NOT_PERMITTED,
                     "mmsnareparse: ignoreTrailingPattern and ignoreTrailingPattern.regex are mutually exclusive");
            ABORT_FINALIZE(RS_RET_PARAM_NOT_PERMITTED);
        }
    }
    /* Compile regex if regex mode is enabled */
    if (pData->ignoreTrailingPattern != NULL && pData->ignoreTrailingPattern_isRegex == 1) {
        const int errcode =
            regcomp(&pData->ignoreTrailingPattern_preg, (char *)pData->ignoreTrailingPattern, REG_EXTENDED);
        if (errcode != 0) {
            char errbuff[512];
            /* POSIX: Use NULL as regex argument after regcomp failure for portability */
            regerror(errcode, NULL, errbuff, sizeof(errbuff));
            LogError(0, RS_RET_ERR, "mmsnareparse: error compiling ignoreTrailingPattern.regex '%s': %s",
                     (char *)pData->ignoreTrailingPattern, errbuff);
            ABORT_FINALIZE(RS_RET_ERR);
        }
    }
    CODE_STD_STRING_REQUESTnewActInst(1);
    if (pData->container == NULL) {
        CHKmalloc(pData->container = (uchar *)strdup(MMSNAREPARSE_CONTAINER_DEFAULT));
        if (pData->container == NULL) {
            LogError(0, RS_RET_OUT_OF_MEMORY, "mmsnareparse: failed to allocate default container name");
        }
    }
    CHKiRet(OMSRsetEntry(*ppOMSR, 0, NULL, OMSR_TPL_AS_MSG));
    CHKiRet(initialize_runtime_tables(pData));
    if (loadModConf != NULL) {
        if (loadModConf->definitionFile != NULL) {
            CHKiRet(load_custom_definition_file(pData, loadModConf->definitionFile));
        }
        if (loadModConf->definitionJson != NULL) {
            CHKiRet(load_custom_definition_text(pData, loadModConf->definitionJson, "module definition.json"));
        }
    }
    if (definitionFile != NULL) {
        CHKiRet(load_custom_definition_file(pData, definitionFile));
    }
    if (definitionJson != NULL) {
        CHKiRet(load_custom_definition_text(pData, definitionJson, "inline definitions"));
    }
    if (runtimeConfigFile != NULL) {
        CHKiRet(load_configuration(&pData->runtimeConfig, runtimeConfigFile));
    }
    CHKiRet(apply_runtime_configuration(pData, &pData->runtimeConfig));
    CODE_STD_FINALIZERnewActInst;
    cnfparamvalsDestruct(pvals, &actpblk);
    free(templateName);
    free(definitionFile);
    free(definitionJson);
    free(runtimeConfigFile);
ENDnewActInst

BEGINdbgPrintInstInfo
    CODESTARTdbgPrintInstInfo;
ENDdbgPrintInstInfo

BEGINtryResume
    CODESTARTtryResume;
ENDtryResume

BEGINdoAction_NoStrings
    smsg_t **ppMsg = (smsg_t **)pMsgData;
    smsg_t *pMsg = ppMsg[0];
    uchar *msgTxt;
    CODESTARTdoAction;
    msgTxt = getMSG(pMsg);
    iRet = process_message(pWrkrData->pData, pMsg, msgTxt);
    if (iRet == RS_RET_OK) {
        MsgSetParseSuccess(pMsg, 1);
    } else if (iRet == RS_RET_COULD_NOT_PARSE) {
        iRet = RS_RET_OK;
    }
ENDdoAction

NO_LEGACY_CONF_parseSelectorAct

    BEGINmodExit CODESTARTmodExit;
ENDmodExit

BEGINqueryEtryPt
    CODESTARTqueryEtryPt;
    CODEqueryEtryPt_STD_OMOD_QUERIES;
    CODEqueryEtryPt_STD_OMOD8_QUERIES;
    CODEqueryEtryPt_STD_CONF2_OMOD_QUERIES;
    CODEqueryEtryPt_STD_CONF2_setModCnf_QUERIES;
    CODEqueryEtryPt_STD_CONF2_QUERIES;
ENDqueryEtryPt

BEGINmodInit()
    CODESTARTmodInit;
    *ipIFVersProvided = CURR_MOD_IF_VERSION;
    CODEmodInit_QueryRegCFSLineHdlr
ENDmodInit
