Public/Logging/Write-CustomLog.ps1

function Write-CustomLog {
    <#
        .SYNOPSIS
            Logs custom events to the Windows Event Log and optionally to JSON files.

        .DESCRIPTION
            The Write-CustomLog function provides a comprehensive logging solution that
            writes events to the Windows Event Log and optionally to JSON files with
            advanced features:

            - Supports both predefined event templates and custom event definitions
            - Automatically masks sensitive information in log messages
            - Configurable log retention and size limits
            - JSON file output with structured data for easier parsing
            - Event categorization and severity levels
            - ShouldProcess support for -WhatIf and -Confirm parameters

            The function is designed for enterprise environments requiring robust logging
            with security and compliance features built-in.

        .PARAMETER EventInfo
            A predefined EventIDInfo object containing standardized event details. This
            parameter simplifies logging of common events with consistent information.
            The object includes:
            - EventID: Numeric identifier for the event
            - Name: Short name of the event
            - Description: Detailed description
            - EventCategory: Logical category grouping
            - EventSeverity: The severity level

        .PARAMETER CustomEventId
            A custom event identifier when using non-standard events. This is typically
            an integer value from the EventID enumeration.

        .PARAMETER EventName
            The name of the custom event being logged. This should be a short, descriptive
            name that identifies the event type.

        .PARAMETER EventCategory
            Specifies the logical category for the event. Categories help organize events
            for filtering and reporting. Should be a value from the EventCategory enumeration.

        .PARAMETER Message
            The detailed log message that will be written to the event log. Sensitive
            information like passwords or personal data will be automatically masked
            when the RemoveSensitiveData feature is enabled.

        .PARAMETER CustomSeverity
            The severity level of the event (Information, Warning, Error, Critical, etc.).
            This should be a value from the EventSeverity enumeration.

        .PARAMETER LogAsJson
            When specified, the log entry will also be written to a JSON file in addition
            to the Windows Event Log. This enables structured logging for integration with
            log analysis tools.

        .PARAMETER MaximumKilobytes
            Specifies the maximum size in kilobytes for the event log. The default is 16384 KB
            (16 MB). Valid values range from 64 KB to 4,194,240 KB.

        .PARAMETER RetentionDays
            Specifies the number of days event log entries should be retained. The default
            is 30 days. Valid values range from 1 to 365 days.

        .PARAMETER LogPath
            The directory path where JSON log files will be saved when LogAsJson is specified.
            If not provided, logs will be written to the default application data location.

        .EXAMPLE
            Write-CustomLog -EventInfo ([EventIDs]::SlowPerformance) -Message 'System performance degraded due to outdated hardware.' -Verbose

            Uses a predefined event template ([EventIDs]::SlowPerformance) with a custom message.
            The event is logged with all the predefined metadata (category, severity, etc.).

        .EXAMPLE
            Write-CustomLog -CustomEventId ([EventID]::LowDiskSpace) `
                -EventName "LowDiskSpace" `
                -EventCategory SystemHealth `
                -Message "Low disk space detected on C: drive. Free space below 10%." `
                -CustomSeverity Warning `
                -Verbose

            Creates and logs a custom event with specific category and severity level.

        .EXAMPLE
            $logParams = @{
                EventInfo = ([EventIDs]::UnauthorizedAccess)
                Message = "Failed login attempt for user: Administrator from IP: 192.168.1.100"
                LogAsJson = $true
                LogPath = "D:\\SecurityLogs"
            }
            Write-CustomLog @logParams

            Logs a security event both to Windows Event Log and to a JSON file in a custom location.

        .INPUTS
            EventIDInfo

            You can pipe EventIDInfo objects to this function.

        .OUTPUTS
            None

            This function does not generate any output.

        .NOTES
            Used Functions:
                Name ║ Module/Namespace
                ═══════════════════════════════════════════╬══════════════════════════════
                Remove-SensitiveData ║ EguibarIT.DelegationPS
                Initialize-EventLogging ║ EguibarIT.DelegationPS
                Write-EventLog ║ Microsoft.PowerShell.Management
                Write-Error ║ Microsoft.PowerShell.Utility
                Write-Verbose ║ Microsoft.PowerShell.Utility
                Write-Warning ║ Microsoft.PowerShell.Utility

        .NOTES
            Version: 2.0
            DateModified: 22/May/2025
            LastModifiedBy: Vicente Rodriguez Eguibar
                            vicente@eguibar.com
                            Eguibar IT
                            http://www.eguibarit.com

        .LINK
            https://github.com/vreguibar/EguibarIT.DelegationPS

        .COMPONENT
            Logging

        .ROLE
            Operations

        .FUNCTIONALITY
            Event Logging, Auditing

    #>


    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'Predefined')]
    [OutputType([void])]

    param(
        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            ValueFromRemainingArguments = $true,
            HelpMessage = 'Default Event Information to be used.',
            Position = 0,
            ParameterSetName = 'Predefined')]
        [ValidateNotNullOrEmpty()]
        [EventIDInfo]
        $EventInfo,

        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            ValueFromRemainingArguments = $true,
            HelpMessage = 'Integer representing the Event ID.',
            Position = 1,
            ParameterSetName = 'Custom')]
        [ValidateRange(1000, 65535)] # assuming a valid custom event ID range
        [int]
        $CustomEventId,

        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            ValueFromRemainingArguments = $true,
            HelpMessage = 'Name of the event.',
            Position = 2,
            ParameterSetName = 'Custom')]
        [string]
        $EventName,

        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            ValueFromRemainingArguments = $true,
            HelpMessage = 'Category assigned to the event.',
            Position = 3,
            ParameterSetName = 'Custom')]
        [ValidateScript({
                [Enum]::IsDefined([EventCategory], $_)
            })]
        [EventCategory]
        $EventCategory,

        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            ValueFromRemainingArguments = $true,
            HelpMessage = 'Message of the event.',
            Position = 4)]
        [ValidateLength(1, 2048)]
        [string]
        $Message,

        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            ValueFromRemainingArguments = $true,
            HelpMessage = 'Severity assigned to the event.',
            Position = 5,
            ParameterSetName = 'Custom')]
        #[ValidateSet('Information', 'Warning', 'Error', 'SuccessAudit', 'FailureAudit')]
        [ValidateScript({
                [Enum]::IsDefined([EventSeverity], $_)
            })]
        [EventSeverity]
        $CustomSeverity,

        [Parameter(ParameterSetName = 'JsonLogging')]
        [switch]
        $LogAsJson,

        [Parameter(ParameterSetName = 'EventLogging')]
        [int]
        $MaximumKilobytes = 16384, # default 16 MB

        [Parameter(ParameterSetName = 'EventLogging')]
        [int]
        $RetentionDays = 30, # default 30 days

        [Parameter(ParameterSetName = 'JsonLogging')]
        [ValidateScript({ Test-Path $_ -PathType 'Container' })] # Validate directory
        [string]
        $LogPath = 'C:\Logs',

        [Parameter(ParameterSetName = 'JsonLogging')]
        [string]
        $JsonLogName = 'CustomLog',

        [Parameter(ParameterSetName = 'JsonLogging')]
        [int]
        $JsonMaxFileSizeMB = 10

    )

    Begin {

        Set-StrictMode -Version Latest

        $ErrorActionPreference = 'Stop'

        # Mask sensitive data
        #$maskedMessage = Remove-SensitiveData -Message $Message
        $maskedMessage = $Message

        # Initialize event logging
        Initialize-EventLogging -MaximumKilobytes $MaximumKilobytes -RetentionDays $RetentionDays

        if ($PSCmdlet.ParameterSetName -eq 'Custom') {
            $eventId = $CustomEventId
            $eventName = $EventName
            $eventCategory = $EventCategory
            $severity = $CustomSeverity
        } else {
            $eventId = $EventInfo.ID
            $eventName = $EventInfo.Name
            $eventCategory = $EventInfo.Category
            $severity = $EventInfo.DefaultSeverity
        } #end If-Else

        $entryType = switch ($severity) {
            'Information' {
                [System.Diagnostics.EventLogEntryType]::Information
            }
            'Warning' {
                [System.Diagnostics.EventLogEntryType]::Warning
            }
            'Error' {
                [System.Diagnostics.EventLogEntryType]::Error
            }
            'SuccessAudit' {
                [System.Diagnostics.EventLogEntryType]::SuccessAudit
            }
            'FailureAudit' {
                [System.Diagnostics.EventLogEntryType]::FailureAudit
            }
        }


        $sb = [System.Text.StringBuilder]::new()
        $sb.AppendLine("Event : $eventName") | Out-Null
        $sb.AppendLine("Event Category : $eventCategory") | Out-Null
        $sb.AppendLine("Details : $maskedMessage") | Out-Null

    } #end Begin

    Process {
        if ($PSCmdlet.ShouldProcess("Logging event: $eventName with severity $severity")) {
            try {

                # Write to Windows Event Log
                # LogName and Source are defined on $Variables which is initialized when module is imported.
                $Splat = @{
                    LogName   = $Variables.LogConfig.LogName
                    Source    = $Variables.LogConfig.Source
                    EntryType = $entryType
                    EventId   = $eventId
                    Category  = [int]([Enum]::Parse([EventCategory], $eventCategory))  # Convert EventCategory to int
                    Message   = $sb.ToString()
                }
                Write-EventLog @Splat

                # https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.eventlog.writeentry?view=net-8.0
                <# [System.Diagnostics.EventLog]::WriteEntry (string source,
                                                          string message,
                                                          System.Diagnostics.EventLogEntryType type,
                                                          int eventID,
                                                          short category,
                                                          byte[] rawData)

                $params = @(
                    $Source,
                    $Message,
                    $eventType,
                    $EventId
                )

                [System.Diagnostics.EventLog]::WriteEntry($params)
                #>




                # Log to JSON
                if ($LogAsJson) {
                    $logObject = [PSCustomObject]@{
                        EventID        = $eventId
                        Name           = $eventName
                        Category       = $eventCategory
                        Severity       = $severity
                        Message        = $maskedMessage
                        Timestamp      = (Get-Date).ToString('o')
                        AdditionalData = @{
                            # Add any additional structured data here
                            MachineName = $env:COMPUTERNAME
                            UserName    = $env:USERNAME
                        }
                    }

                    $jsonFile = Join-Path $LogPath "$JsonLogName.json"

                    # Ensure directory exists
                    if (-not (Test-Path -Path $LogPath)) {
                        New-Item -ItemType Directory -Force -Path $LogPath | Out-Null
                    }

                    # Check file size and rotate if necessary
                    if (Test-Path $jsonFile) {
                        $fileInfo = Get-Item $jsonFile
                        if ($fileInfo.Length / 1MB -ge $JsonMaxFileSizeMB) {
                            $backupFile = Join-Path $LogPath "$JsonLogName-$(Get-Date -Format 'yyyyMMddHHmmss').json"
                            Move-Item $jsonFile $backupFile
                        }
                    }

                    $logObject | ConvertTo-Json | Out-File -FilePath $jsonFile -Append

                    Write-Verbose -Message ('Event {0} was logged successfully to JSON.' -f $eventName)
                } #end If

                Write-Verbose -Message ('
                    Event {0} with ID {1}
                    was logged successfully to the event log.'
 -f
                    $eventName, $eventId
                )
            } catch {
                Write-Error -Message ('
                    An error occurred while logging the event.
                    Exception: {0}
                    Full details: {1}'
 -f
                    $_.Exception.Message, $_
                )
                throw
            } #end Try-Catch
        } #end If
    } #end Process

    End {
        Write-Verbose -Message 'Logging process completed.'
    } #end End
} #end Function