Guía completa sobre el uso de PowerShell para realizar una búsqueda en registros de eventos de forma eficaz

En el panorama en constante evolución de las TI, la supervisión y el análisis de los registros de eventos son cruciales para mantener el buen estado del sistema, diagnosticar problemas y garantizar el cumplimiento de las normas de seguridad.

Los registros de eventos son una rica fuente de información sobre el estado y el comportamiento de los sistemas, y ser capaces de buscar y filtrar eficientemente estos registros puede mejorar significativamente la capacidad de un profesional de TI para gestionar su infraestructura.

Este artículo profundiza en un script de PowerShell que simplifica el proceso de búsqueda en registros de eventos, facilitando la localización de eventos específicos en función de criterios como el nombre del registro de eventos, el origen, el ID del evento, etc.

La importancia de la gestión de registros de eventos en TI

Los registros de eventos son registros de sucesos del sistema, seguridad y aplicaciones en un ordenador. Estos registros son indispensables para solucionar problemas, auditar incidentes de seguridad y garantizar el cumplimiento de las normas reglamentarias.

Para los proveedores de servicios gestionados (MSP) y los profesionales de TI, la capacidad de realizar búsquedas rápidas y precisas en estos registros es vital. Sin herramientas eficaces, esta tarea puede resultar engorrosa, sobre todo cuando se trata de un gran número de sistemas o entornos complejos.

PowerShell, un potente lenguaje de scripting desarrollado por Microsoft, ofrece sólidas capacidades para interactuar con los registros de eventos. El script del que hablaremos hoy está diseñado para agilizar el proceso de búsqueda eb registros de eventos, permitiendo a los profesionales de TI localizar rápidamente los más relevantes dentro del Visor de eventos.

El script para realizar una búsqueda en registros de eventos

#Requires -Version 5.1

<#
.SYNOPSIS
    Search for specific events in Event Viewer based on the event log they were in, the source of the event, or the specific event IDs used. One of these three options is required for the search.
.DESCRIPTION
    Search for specific events in Event Viewer based on the event log they were in, the source of the event, or the specific event IDs used. One of these three options is required for the search.
.EXAMPLE
    -EventLogName "Application"
    
    Matching Events Found!

    LogName      : Application
    ProviderName : Microsoft-Windows-Security-SPP
    Id           : 16384
    TimeCreated  : 4/4/2024 10:00:48 AM
    Message      : Successfully scheduled Software Protection service for re-start at 2024-04-04T21:19:48Z. Reason: Rul...

    LogName      : Application
    ProviderName : Microsoft-Windows-Security-SPP
    Id           : 16394
    TimeCreated  : 4/4/2024 10:00:17 AM
    Message      : Offline downlevel migration succeeded.

    LogName      : Application
    ProviderName : Microsoft-Windows-Security-SPP
    Id           : 16384
    TimeCreated  : 4/4/2024 9:59:59 AM
    Message      : Successfully scheduled Software Protection service for re-start at 2024-04-04T21:19:59Z. Reason: Rul...

PARAMETER: -EventLogName "Application"
    Specify the name of the Event Log from which to retrieve events.

PARAMETER: -EventLogSource "Microsoft-Windows-Kernel-General"
    Determines the source of the events to retrieve.

PARAMETER: -EventLogMessage "Alert"
    Filters events by the text contained in the event's message.

PARAMETER: -EventIDs "12, 13, 6008"
    A comma-separated list of event IDs to include in the search.

PARAMETER: -excludeEventIDs "13"
    A comma-separated list of event IDs to exclude from the search.

PARAMETER: -StartDate "12/24/2021"
    Defines the start date and time for the event search. Events logged before this time will not be included in the results.

PARAMETER: -EndDate "12/29/2021"
    Sets the end date and time for the event search. Events logged after this time will not be included.

PARAMETER: -MultilineCustomField "replaceMeWithAcustomFieldName"
    Specify the name of a multiline custom field to optionally store the search results in. Leave blank to not set a multiline field.

PARAMETER: -WysiwygCustomField "replaceMeWithACustomFieldName"
    Specify the name of a WYSIWYG custom field to optionally store the search results in. Leave blank to not set a WYSIWYG field.
.NOTES
    Minimum OS Architecture Supported: Windows 10, Windows Server 2016
    Release Notes: Initial Release
By using this script, you indicate your acceptance of the following legal terms as well as our Terms of Use at https://ninjastage2.wpengine.com/terms-of-use.
    Ownership Rights: NinjaOne owns and will continue to own all right, title, and interest in and to the script (including the copyright). NinjaOne is giving you a limited license to use the script in accordance with these legal terms. 
    Use Limitation: You may only use the script for your legitimate personal or internal business purposes, and you may not share the script with another party. 
    Republication Prohibition: Under no circumstances are you permitted to re-publish the script in any script library or website belonging to or under the control of any other software provider. 
    Warranty Disclaimer: The script is provided “as is” and “as available”, without warranty of any kind. NinjaOne makes no promise or guarantee that the script will be free from defects or that it will meet your specific needs or expectations. 
    Assumption of Risk: Your use of the script is at your own risk. You acknowledge that there are certain inherent risks in using the script, and you understand and assume each of those risks. 
    Waiver and Release: You will not hold NinjaOne responsible for any adverse or unintended consequences resulting from your use of the script, and you waive any legal or equitable rights or remedies you may have against NinjaOne relating to your use of the script. 
    EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA).
#>

[CmdletBinding()]
param (
    [Parameter()]
    [String]$EventLogName,
    [Parameter()]
    [String]$EventLogSource,
    [Parameter()]
    [String]$EventLogMessage,
    [Parameter()]
    [String]$EventIDs,
    [Parameter()]
    [String]$ExcludeEventIDs,
    [Parameter()]
    [datetime]$StartDate,
    [Parameter()]
    [datetime]$EndDate,
    [Parameter()]
    [String]$MultilineCustomField,
    [Parameter()]
    [String]$WysiwygCustomField
)

begin {
    # Set parameters using dynamic script variables.
    if ($env:eventLogName -and $env:eventLogName -notlike "null") { $EventLogName = $env:eventLogName }
    if ($env:eventLogSource -and $env:eventLogSource -notlike "null") { $EventLogSource = $env:eventLogSource }
    if ($env:eventLogMessage -and $env:eventLogMessage -notlike "null") { $EventLogMessage = $env:eventLogMessage }
    if ($env:eventIds -and $env:eventIds -notlike "null") { $EventIDs = $env:eventIds }
    if ($env:excludeEventIds -and $env:excludeEventIds -notlike "null") { $ExcludeEventIDs = $env:excludeEventIds }
    if ($env:eventStart -and $env:eventStart -notlike "null") { $StartDate = $env:eventStart }
    if ($env:eventEnd -and $env:eventEnd -notlike "null") { $EndDate = $env:eventEnd }
    if ($env:multilineCustomFieldName -and $env:multilineCustomFieldName -notlike "null") { $MultilineCustomField = $env:multilineCustomFieldName }
    if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { $WysiwygCustomField = $env:wysiwygCustomFieldName }

    # Check if both StartDate and EndDate are provided and if StartDate is earlier than EndDate
    if (($StartDate -and $EndDate) -and $StartDate -gt $EndDate) {
        Write-Host -Object "[Error] Start date cannot be earlier than end date!"
        exit 1
    }

    # Verify that WysiwygField and MultiLineField are not the same, exiting with an error if they are.
    if ($WysiwygCustomField -and $MultilineCustomField -and ($WysiwygCustomField -eq $MultilineCustomField)) {
        Write-Host -Object "[Error] Wysiwyg Field and Multiline Field are the same! Custom fields cannot be the same type."
        Write-Host -Object "https://ninjarmm.zendesk.com/hc/en-us/articles/18601842971789-Custom-Fields-by-Type-and-Functionality"
        exit 1
    }

    # Ensure that at least one of Event ID, Event Log Name, or Event Source is provided for the query
    if (!$EventIDs -and !$EventLogName -and !$EventLogSource) {
        Write-Host -Object "[Error] You must provide either an Event ID, Event Log Name or Event Source."
        exit 1
    }

    # Trimming trailing spaces.
    if ($EventLogName) {
        $EventLogName = $EventLogName.Trim()
    }

    # Retrieve and sort all event log names available on the system
    $EventLogNamesOnSystem = Get-WinEvent -ListLog * -ErrorAction SilentlyContinue | Sort-Object LogName
    # Check if the provided EventLogName exists in the system's event logs
    if ($EventLogName -and ($EventLogNamesOnSystem).LogName -notcontains $EventLogName) {
        # If not found, print an error message and a list of valid event log names, then exit the script
        Write-Host -Object "[Error] Event Log '$EventLogName' doesn't exist! See the list below for valid event log names."
        Write-Host -Object "### Valid Event Log Names ###"
        $EventLogNamesOnSystem | Select-Object -ExpandProperty LogName | Write-Host
        exit 1 
    }

    $InvalidEventSourceCharacters = "[\\/<>&`"%\|']"
    if ($EventLogSource) {
        if ($EventLogSource -match $InvalidEventSourceCharacters) {
            Write-Host -Object "[Error] Event Log Source '$EventLogSource' contains an invalid character!"
            exit 1
        }

        if ($EventLogSource.Length -gt 255) {
            Write-Host -Object "[Error] Event Log Source '$EventLogSource' is too large to be an event source!"
            exit 1
        }

        # Trims the event log source for trailing spaces
        $EventLogSource = $EventLogSource.Trim()
    }

    # Prepare a list to hold valid event IDs to search for
    $EventIdsToSearch = New-Object System.Collections.Generic.List[int]
    # Process the input event IDs, removing any that are not purely numerical
    if ($EventIDs -and $EventIDs -match ",") {
        # If multiple event IDs are provided and separated by commas, split them
        $EventIDs -split "," | ForEach-Object {
            $EventId = $_.Trim()
            # Validate each event ID to ensure it's numerical
            if ($EventId -match '[a-zA-Z]|\W') {
                # If not, print an error and skip adding this ID to the list
                Write-Host "[Error] Event ID '$EventId' is not a valid event id. Removing it from the search."
                $ExitCode = 1
                return
            }
            # Check size of event id
            if ([long]$EventId -gt 65535 -or [long]$EventId -lt 0) {
                Write-Host "[Error] Event ID '$EventId' is not a valid event id. Event ID's must be less than or equal to 65535 and greater than or equal to 0. Removing it from the search."
                $ExitCode = 1
                return
            }
             
            # Add the validated event ID to the list
            $EventIdsToSearch.Add($EventId)
        }
    }
    elseif ($EventIDs) {
        $EventId = $EventIDs.Trim()
        # Handle a single event ID input
        if ($EventId -match '[a-zA-Z]|\W') {
            Write-Host "[Error] Event ID '$EventId' is not a valid event id. Removing it from the search."
            $ExitCode = 1
        }
        elseif ([long]$EventId -gt 65535 -or [long]$EventId -lt 0) {
            Write-Host "[Error] Event ID '$EventId' is not a valid event id. Event ID's must be less than or equal to 65535 and greater than or equal to 0. Removing it from the search."
            $ExitCode = 1
        }
        else {
            $EventIdsToSearch.Add($EventId)
        }
    }
 
    # Prepare a list to hold event IDs that should be excluded from the search
    $EventsToExclude = New-Object System.Collections.Generic.List[int]
     
    # Similar process for excluded event IDs as regular event IDs
    if ($ExcludeEventIDs -and $ExcludeEventIDs -match ",") {
        $ExcludeEventIDs -split "," | ForEach-Object {
            $ExcludeEventId = $_.Trim()
            if ($ExcludeEventId -match '[a-zA-Z]|\W') {
                Write-Host "[Error] Event ID '$ExcludeEventId' is not a valid event id. Removing it from the exclusions."
                $ExitCode = 1
                return
            }
 
            if ([long]$ExcludeEventId -gt 65535 -or [long]$ExcludeEventId -lt 0) {
                Write-Host "[Error] Event ID '$ExcludeEventId' is not a valid event id. Event ID's must be less than or equal to 65535 and greater than or equal to 0. Removing it from the exclusions."
                $ExitCode = 1
                return
            }
             
            $EventsToExclude.Add($ExcludeEventId)
        }
    }
    elseif ($ExcludeEventIDs) {
        $ExcludeEventId = $ExcludeEventIDs.Trim()
        if ($ExcludeEventId -match '[a-zA-Z]|\W') {
            Write-Host "[Error] Event ID '$ExcludeEventId' is not a valid event id. Removing it from the exclusions."
            $ExitCode = 1
        }
        elseif ([long]$ExcludeEventId -gt 65535 -or [long]$ExcludeEventId -lt 0) {
            Write-Host "[Error] Event ID '$ExcludeEventId' is not a valid event id. Event ID's must be less than or equal to 65535 and greater than or equal to 0. Removing it from the exclusions."
            $ExitCode = 1
        }
        else {
            $EventsToExclude.Add($ExcludeEventId)
        }
    }
 
    # Check if there are any event IDs to exclude and if the list of event IDs to search for is not empty.
    if ($EventsToExclude.Count -gt 0 -and $EventIdsToSearch.Count -gt 0) {
        $EventsToExclude | ForEach-Object {
            # Check if the current event ID from the exclusion list is also in the list of event IDs to search for.
            if ($EventIdsToSearch -contains $_) {
                Write-Warning "Event ID $_ has been specified for both inclusion and exclusion. It will be excluded."
            }
        }
    }
 
    # Check if there's no valid event ID, log name, or log source provided and exit if true
    if ($EventIdsToSearch.Count -eq 0 -and !$EventLogName -and !$EventLogSource) {
        Write-Host "[Error] No valid Event ID given and no Event Log Name or Event Log Source given."
        exit 1
    }

    # Handy function to set a custom field.
    function Set-NinjaProperty {
        [CmdletBinding()]
        Param(
            [Parameter(Mandatory = $True)]
            [String]$Name,
            [Parameter()]
            [String]$Type,
            [Parameter(Mandatory = $True, ValueFromPipeline = $True)]
            $Value,
            [Parameter()]
            [String]$DocumentName
        )

        $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters
        if ($Characters -ge 200000) {
            throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value with $Characters characters is greater than or equal to 200,000 characters.")
        }
    
        # If we're requested to set the field value for a Ninja document we'll specify it here.
        $DocumentationParams = @{}
        if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName }
    
        # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed.
        $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG"
        if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" }
    
        # The field below requires additional information to be set
        $NeedsOptions = "Dropdown"
        if ($DocumentName) {
            if ($NeedsOptions -contains $Type) {
                # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong.
                $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1
            }
        }
        else {
            if ($NeedsOptions -contains $Type) {
                $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1
            }
        }
    
        # If an error is received it will have an exception property, the function will exit with that error information.
        if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions }
    
        # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports.
        switch ($Type) {
            "Checkbox" {
                # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry.
                $NinjaValue = [System.Convert]::ToBoolean($Value)
            }
            "Date or Date Time" {
                # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, the given value will be matched with a GUID.
                $Date = (Get-Date $Value).ToUniversalTime()
                $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date
                $NinjaValue = $TimeSpan.TotalSeconds
            }
            "Dropdown" {
                # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid.
                $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name"
                $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID
    
                if (-not $Selection) {
                    throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown")
                }
    
                $NinjaValue = $Selection
            }
            default {
                # All the other types shouldn't require additional work on the input.
                $NinjaValue = $Value
            }
        }
    
        # We'll need to set the field differently depending on if its a field in a Ninja Document or not.
        if ($DocumentName) {
            $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1
        }
        else {
            $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1
        }
    
        if ($CustomField.Exception) {
            throw $CustomField
        }
    }

    function Test-IsElevated {
        $id = [System.Security.Principal.WindowsIdentity]::GetCurrent()
        $p = New-Object System.Security.Principal.WindowsPrincipal($id)
        $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
    }

    if (!$ExitCode) {
        $ExitCode = 0
    }
}
process {
    # Check if the script is running with elevated (Administrator) privileges
    if (!(Test-IsElevated)) {
        # If not, display an error message and exit with status code 1
        Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges."
        exit 1
    }

    # Prepare a list to hold event log names to search for
    $EventLogNamesToSearch = New-Object System.Collections.Generic.List[string]

    # If no event log name was required we'll search all the event
    if (!$EventLogName) {
        $EventLogNamesOnSystem | Where-Object { $_.RecordCount -gt 0 } | Select-Object -ExpandProperty LogName | ForEach-Object {
            $EventLogNamesToSearch.Add($_)
        }
    }
    else {
        $EventLogNamesToSearch.Add($EventLogName)
    }

    # Create XML object.
    [xml]$XML = New-Object System.Xml.XmlDocument

    # Add QueryList element to xml.
    $QueryList = $XML.CreateElement("QueryList")
    $QueryList = $XML.AppendChild($QueryList)

    # Create query element and nest it under QueryList.
    $Query = $XML.CreateElement("Query")
    $Query.SetAttribute("Id", "0")
    $Query = $QueryList.AppendChild($Query)
    

    # Foreach event log to search we're going to create a select element.
    $EventLogNamesToSearch | ForEach-Object {
        # We'll start each loop by selecting the query element to add to.
        $Query = $XML.SelectSingleNode("//Query")

        # The select element starts off with the event log to search.
        $Select = $XML.CreateElement("Select")
        $Select.SetAttribute("Path", "$_")
        
        # Reset the inner text between runnings
        $XMLInnerText = $Null

        # The inner text of each element (<Element1>InnerText</Element1>) will need to be built differently depending on the parameters.
        if ($EventLogSource) {
            $XMLInnerText = "*[System[Provider[@Name='$EventLogSource']]]"
        }
        
        # If we're given a select number of event id's to search we'll filter them here.
        if ($EventIdsToSearch.Count -gt 0) {
            $EventIDSearchText = $Null
            $EventIdsToSearch | ForEach-Object {
                # We may have been given one event id or more than one.
                if ($EventIDSearchText) {
                    $EventIDSearchText = "$EventIDSearchText or EventID=$_"
                }
                else {
                    $EventIDSearchText = "EventID=$_"
                }
            }

            # We'll replace the two ending brackets with our given search text
            if ($XMLInnerText) {
                $XMLInnerText = $XMLInnerText -replace ']]$', " and ($EventIDSearchText)]]"
            }
            else {
                $XMLInnerText = "*[System[($EventIDSearchText)]]"
            }
        }

        # If we're also asked to filter based on the date the event was created we'll create the filter text here.
        if ($StartDate -or $EndDate) {
            $DateFilter = $Null 
            if ($StartDate) {
                $XMLstartDate = Get-Date $StartDate -Format "yyyy-MM-ddTHH:mm:ss"
                # PowerShell will convert the < or > symbol for us when we go to save to the xml.
                $DateFilter = "@SystemTime>='$XMLstartDate'"
            }

            # We may or may not have been given a start date.
            if ($EndDate -and $DateFilter) {
                $XMLendDate = Get-Date $EndDate -Format "yyyy-MM-ddTHH:mm:ss"
                $DateFilter = "$DateFilter and @SystemTime<='$XMLendDate'"
            }
            elseif ($EndDate) {
                $XMLendDate = Get-Date $EndDate -Format "yyyy-MM-ddTHH:mm:ss"
                $DateFilter = "@SystemTime<='$XMLendDate'"
            }

            # Replace the last two closing brackets and add our filter text.
            if($XMLInnerText){
                $XMLInnerText = $XMLInnerText -replace ']]$', " and TimeCreated[$DateFilter]]]"
            }else{
                $XMLInnerText = "*[System[TimeCreated[$DateFilter]]]"
            }
        }

        # If no filters were given (other than the event log name) we'll need to select everything in that log
        if(!$XMLInnerText){
            $XMLInnerText = "*"
        }

        # Save our filter text to the select statement
        $Select.InnerText = $XMLInnerText

        # Append our select statement to our xml file
        $Query.AppendChild($Select) | Out-Null
    }

    # Search for matching events using the XML filter
    $MatchingEvents = Get-WinEvent -FilterXml $XML -ErrorAction SilentlyContinue

    # Exclude events based on the excluded event IDs if any are specified
    if ($EventsToExclude.Count -gt 0) {
        $MatchingEvents = $MatchingEvents | Where-Object { $EventsToExclude -notcontains $_.ID }
    }

    # Exclude events that do not match the keywords you specified.
    if ($EventLogMessage) {
        $MatchingEvents = $MatchingEvents | Where-Object { $_.Message -like "*$EventLogMessage*" }
    }

    # If the event log message is larger than 100 characters trim it and add ...
    if ($MatchingEvents) {
        $MatchingEvents = $MatchingEvents | Select-Object LevelDisplayName, LogName, ProviderName, Id, TimeCreated, @{
            Name       = 'Message'
            Expression = {
                $Characters = $_.Message | Measure-Object -Character | Select-Object -ExpandProperty Characters
                if ($Characters -gt 100) {
                    "$(($_.Message).SubString(0,100))(...)"
                }
                else {
                    $_.Message
                }
            }
        }

        # Sort the object by newest event to oldest
        $MatchingEvents = $MatchingEvents | Sort-Object TimeCreated -Descending
    }

    # Set a Wysiwyg custom field if any matching events are found and it was requested.
    if ($WysiwygCustomField -and $MatchingEvents) {
        try {
            Write-Host "Attempting to set Custom Field '$WysiwygCustomField'."

            # Prepare the custom field output.
            $CustomFieldValue = New-Object System.Collections.Generic.List[string]

            # Convert the matching events into an html report.
            $htmlTable = $MatchingEvents | Select-Object -Property LevelDisplayName, LogName, ProviderName, Id, TimeCreated, Message | ConvertTo-Html -Fragment
            
            # Set color coding
            $htmlTable = $htmlTable -replace "<tr><td>Verbose</td>", "<tr class=`"other`"><td>Verbose</td>"
            $htmlTable = $htmlTable -replace "<tr><td>Warning</td>", "<tr class=`"warning`"><td>Warning</td>"
            $htmlTable = $htmlTable -replace "<tr><td>Error</td>", "<tr class=`"danger`"><td>Error</td>"
            $htmlTable = $htmlTable -replace "<tr><td>Critical Error</td>", "<tr class=`"danger`"><td>Critical Error</td>"

            # Remove Level Display Name
            $LevelDisplayNames = $MatchingEvents | Select-Object -Property LevelDisplayName -Unique
            $LevelDisplayNames | ForEach-Object {
                $htmlTable = $htmlTable -replace "<td>$([Regex]::Escape($_.LevelDisplayName))</td>"
            }
            $htmlTable = $htmlTable -replace "<th>LevelDisplayName</th>"

            # Add the newly created html into the custom field output.
            $CustomFieldValue.Add($htmlTable)

            # Check that the output complies with the hard character limits.
            $Characters = $CustomFieldValue | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters
            if ($Characters -ge 199500) {
                Write-Warning "200,000 Character Limit has been reached! Trimming output until the character limit is satisified..."
                
                # If it doesn't comply with the limits we'll need to recreate it with some adjustments.
                $i = 0
                do {
                    # Recreate the custom field output starting with a warning that we truncated the output.
                    $CustomFieldValue = New-Object System.Collections.Generic.List[string]
                    $CustomFieldValue.Add("<h1>This info has been truncated to accommodate the 200,000 character limit.</h1>")

                    # The custom field information is sorted from newest to oldest. We'll remove the oldest first by flipping the array upside down.
                    [array]::Reverse($htmlTable)
                    # If the next entry is a row we'll delete it.
                    if ($htmlTable[$i] -match '<tr><td>' -or $htmlTable[$i] -match '<tr class=') {
                        $htmlTable[$i] = $null
                    }
                    $i++
                    # We'll flip the array back to right side up.
                    [array]::Reverse($htmlTable)

                    # Add it back to the output.
                    $CustomFieldValue.Add($htmlTable)

                    # Check that we now comply with the character limit. If not restart the do loop.
                    $Characters = $CustomFieldValue | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters
                }while ($Characters -ge 199500)
            }

            # Set the custom field.
            Set-NinjaProperty -Name $WysiwygCustomField -Value $CustomFieldValue
            Write-Host "Successfully set Custom Field '$WysiwygCustomField'!"
        }
        catch {
            Write-Host "[Error] $($_.Exception.Message)"
            $ExitCode = 1
        }
    }

    # Set a multiline custom field if any matching events are found and it was requested.
    if ($MultilineCustomField -and $MatchingEvents) {
        try {
            Write-Host "Attempting to set Custom Field '$MultilineCustomField'."
            $CustomFieldValue = New-Object System.Collections.Generic.List[string]

            # We don't want to edit the matching Events array if we have to truncate later so we'll create a duplicate here.
            $CustomFieldList = $MatchingEvents | Select-Object -Property LogName, ProviderName, Id, TimeCreated, Message

            # Format the matching items into a nice list with the relevant properties.
            $CustomFieldValue.Add(($CustomFieldList | Format-List -Property LogName, ProviderName, Id, TimeCreated, Message | Out-String))
            
            # Check that the output complies with the hard character limits.
            $Characters = $CustomFieldValue | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters
            if ($Characters -ge 9500) {
                Write-Warning "10,000 Character Limit has been reached! Trimming output until the character limit is satisified..."
                
                # If it doesn't comply with the limits we'll need to recreate it with some adjustments.
                $i = 0
                do {
                    # Recreate the custom field output starting with a warning that we truncated the output.
                    $CustomFieldValue = New-Object System.Collections.Generic.List[string]
                    $CustomFieldValue.Add("This info has been truncated to accommodate the 10,000 character limit.")
                    
                    # The custom field information is sorted from newest to oldest. We'll remove the oldest events first by flipping the array upside down.
                    [array]::Reverse($CustomFieldList)

                    # Remove the next item which in this case will be the oldest item.
                    $CustomFieldList[$i] = $null
                    $i++

                    # We'll flip the array back to right side up.
                    [array]::Reverse($CustomFieldList)

                    # Add it back to the output.
                    $CustomFieldValue.Add(($CustomFieldList | Format-List -Property LogName, ProviderName, Id, TimeCreated, Message | Out-String))

                    # Check that we now comply with the character limit. If not restart the do loop.
                    $Characters = $CustomFieldValue | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters
                }while ($Characters -ge 9500)
            }

            Set-NinjaProperty -Name $MultilineCustomField -Value $CustomFieldValue
            Write-Host "Successfully set Custom Field '$MultilineCustomField'!"
        }
        catch {
            Write-Host "[Error] $($_.Exception.Message)"
            $ExitCode = 1
        }
    }

    # If any matching events were found output them into the activity log.
    if ($MatchingEvents) {
        Write-Host "Matching Events Found!"
        $MatchingEvents | Format-List LogName, ProviderName, Id, TimeCreated, Message | Out-String | Write-Host
    }
    else {
        Write-Host "No matching events found!"
    }

    exit $ExitCode
}
end {
    
    
    
}

 

Comprender el script PowerShell

Este script PowerShell es una herramienta versátil que permite a los usuarios buscar eventos específicos en el Visor de eventos basándose en varios criterios. El script puede filtrar los eventos por nombre de registro, fuente, ID de evento, contenido del mensaje y rango de fechas. Además, ofrece opciones para excluir determinados ID de eventos de la búsqueda y almacenar los resultados en campos personalizados.

Aquí tienes un desglose detallado de cómo funciona el script para realizar una búsqueda en registros de eventos:

1. Parámetros y variables de entorno

  • El script comienza estableciendo parámetros que permiten a los usuarios definir sus criterios de búsqueda, como EventLogName, EventLogSource, EventIDs, StartDate y EndDate. También admite variables de entorno para estos parámetros, lo que permite la integración con otros flujos de trabajo de automatización.

2. Validación y tratamiento de errores

  • El script para realizar una búsqueda en registros de eventos incluye múltiples capas de validación para garantizar que los parámetros de entrada son correctos. Por ejemplo, comprueba si la fecha de inicio proporcionada es anterior a la fecha de finalización y valida que los ID de los eventos sean numéricos y estén dentro del intervalo aceptable.
  • Si los parámetros WysiwygCustomField y MultilineCustomField tienen el mismo valor, el script lanza un error, evitando configuraciones conflictivas.

3. Lógica de búsqueda en el registro de eventos

  • El script crea una consulta XML para filtrar los eventos en función de los criterios proporcionados. A continuación, esta consulta se utiliza con el cmdlet Get-WinEvent para recuperar los eventos coincidentes de los registros especificados.
  • El script permite la exclusión de determinados ID de eventos y puede filtrar eventos por el contenido del mensaje, lo que lo hace altamente personalizable.

4. Configuración de campo personalizada

  • Si se encuentran eventos coincidentes, el script para realizar una búsqueda en registros de eventos puede almacenar los resultados en campos personalizados, ya sea como un campo de texto multilínea o como un informe con formato HTML. Esta función es especialmente útil para los MSP que necesitan documentar hallazgos o automatizar informes.

5. Salida y clasificación

  • El script muestra los resultados en un formato legible, ordenados por la hora de creación de los eventos, de más reciente a más antiguo. También recorta los mensajes largos para no saturar la salida.

Aplicaciones prácticas del script

Consideremos un escenario en el que un profesional de TI tiene la tarea de identificar errores recurrentes en varios servidores de un entorno corporativo. Examinar manualmente los registros de eventos de cada servidor llevaría mucho tiempo y daría lugar a errores.

Aprovechando este script de PowerShell, el profesional puede automatizar el proceso de búsqueda en registros de eventos, filtrando los registros en función de ID de eventos de error específicos y limitando los resultados a un marco temporal concreto. Este enfoque no sólo ahorra tiempo, sino que también mejora la precisión, lo que permite al equipo informático diagnosticar y resolver rápidamente los problemas.

Otro caso de uso podría ser la auditoría de cumplimiento. Es posible que un MSP necesite verificar que determinados eventos de seguridad, como los intentos de inicio de sesión exitosos o fallidos, se producen según lo esperado en todos sus sistemas gestionados. Este script puede utilizarse para extraer estos eventos y generar informes que demuestren el cumplimiento de las políticas de seguridad.

Comparación de métodos de búsqueda en el registros de eventos

Aunque existen varios métodos para buscar en los registros de eventos, como la interfaz gráfica de usuario integrada del Visor de eventos u otras herramientas de terceros, este script de PowerShell ofrece claras ventajas. A diferencia de las búsquedas manuales a través de la GUI, este script puede automatizarse e integrarse en flujos de trabajo más amplios. Además, en comparación con otras herramientas, PowerShell ofrece un alto grado de personalización, lo que permite a los profesionales de TI adaptar los criterios de búsqueda a sus necesidades específicas.

Preguntas frecuentes

P: ¿Se puede utilizar este script para realizar una búsqueda en registros de eventos en sistemas con versiones anteriores de Windows?

R: El script requiere Windows PowerShell 5.1 y es compatible con Windows 10 y Windows Server 2016 o versiones posteriores.

P: ¿Qué ocurre si se supera el límite de caracteres al configurar un campo personalizado?

R: El script para realizar una búsqueda en registros de eventos incluye una lógica para truncar la salida y ajustarla al límite de caracteres, lo que garantiza que el script no falle debido a conjuntos de datos demasiado grandes.

P: ¿Es posible buscar en todos los registros de eventos de un sistema utilizando este script?

R: Sí, si no se proporciona un nombre de registro de eventos específico, el script buscará en todos los registros de eventos disponibles en el sistema.

Implicaciones para la seguridad informática

La capacidad de realizar búsquedas eficientes en los registros de eventos tiene importantes implicaciones para la seguridad informática. Al identificar rápidamente los eventos sospechosos o anómalos, los profesionales de TI pueden responder a posibles incidentes de seguridad con mayor eficacia. La automatización que proporciona este script para realizar una búsqueda en registros reduce el riesgo de error humano y garantiza que no se pasen por alto los eventos críticos.

Buenas prácticas para utilizar el script

  • Prueba siempre el script en un entorno controlado antes de desplegarlo en producción.
  • Utiliza variables de entorno para integrar el script en flujos de trabajo de automatización más amplios.
  • Actualiza periódicamente el script para incorporar nuevas funciones o solucionar posibles vulnerabilidades de seguridad.

Reflexiones finales

La gestión eficaz de los registros de eventos es esencial para mantener el buen estado del sistema, garantizar la seguridad y lograr el cumplimiento de las normativas. Este script de PowerShell para realizar una búsqueda en registros de eventos proporciona a los profesionales de TI una potente herramienta para buscar y analizar registros de eventos, lo que les permite realizar sus tareas con mayor eficacia.

Para los MSP y los equipos de TI que gestionan múltiples sistemas, NinjaOne ofrece un completo conjunto de herramientas que pueden mejorar aún más su capacidad para supervisar y gestionar la infraestructura de TI. Si integras este script en tus flujos de trabajo, puedes agilizar tus búsquedas en el registro de eventos y mejorar de forma general tus operaciones de TI.

Próximos pasos

La creación de un equipo de TI próspero y eficaz requiere contar con una solución centralizada que se convierta en tu principal herramienta de prestación de servicios. NinjaOne permite a los equipos de TI supervisar, gestionar, proteger y dar soporte a todos sus dispositivos, estén donde estén, sin necesidad de complejas infraestructuras locales.

Obtén más información sobre NinjaOne Endpoint Management, echa un vistazo a un tour en vivoo tu prueba gratuita de la plataforma NinjaOne.

Categorías:

Quizá también te interese…

×

¡Vean a NinjaOne en acción!

Al enviar este formulario, acepto la política de privacidad de NinjaOne.

Términos y condiciones de NinjaOne

Al hacer clic en el botón «Acepto» que aparece a continuación, estás aceptando los siguientes términos legales, así como nuestras Condiciones de uso:

  • Derechos de propiedad: NinjaOne posee y seguirá poseyendo todos los derechos, títulos e intereses sobre el script (incluidos los derechos de autor). NinjaOne concede al usuario una licencia limitada para utilizar el script de acuerdo con estos términos legales.
  • Limitación de uso: solo podrás utilizar el script para tus legítimos fines personales o comerciales internos, y no podrás compartirlo con terceros.
  • Prohibición de republicación: bajo ninguna circunstancia está permitido volver a publicar el script en ninguna biblioteca de scripts que pertenezca o esté bajo el control de cualquier otro proveedor de software.
  • Exclusión de garantía: el script se proporciona «tal cual» y «según disponibilidad», sin garantía de ningún tipo. NinjaOne no promete ni garantiza que el script esté libre de defectos o que satisfaga las necesidades o expectativas específicas del usuario.
  • Asunción de riesgos: el uso que el usuario haga del script corre por su cuenta y riesgo. El usuario reconoce que existen ciertos riesgos inherentes al uso del script, y entiende y asume cada uno de esos riesgos.
  • Renuncia y exención: el usuario no hará responsable a NinjaOne de cualquier consecuencia adversa o no deseada que resulte del uso del script y renuncia a cualquier derecho o recurso legal o equitativo que pueda tener contra NinjaOne en relación con su uso del script.
  • CLUF: si el usuario es cliente de NinjaOne, su uso del script está sujeto al Contrato de Licencia para el Usuario Final (CLUF).