How to Create a New User Alert Using PowerShell

Monitoring new user account creation is a fundamental aspect of maintaining a secure IT environment. Whether you’re safeguarding a corporate Active Directory domain or managing standalone workstations, detecting unauthorized or unexpected user creation can be critical in preventing insider threats, misconfigurations, or even external breaches. This blog post explores a PowerShell-based solution designed to alert IT professionals when new user accounts are created within a given timeframe, providing a seamless integration with NinjaOne for centralized visibility.

Background

In enterprise environments, IT administrators and Managed Service Providers (MSPs) often struggle to keep track of every account change, especially across hybrid infrastructures. Malicious actors may create new accounts as backdoors, or legitimate changes might go unnoticed, leaving security gaps. Windows offers auditing capabilities through event logs and Active Directory, but extracting and making use of this data programmatically is nontrivial.

This PowerShell script solves that by automating the detection of new user accounts and optionally pushing the alert data into a NinjaOne multiline custom field. It supports both domain controllers (DCs) and standalone systems, making it adaptable across various Windows environments. This script serves as an advanced New User Alert PowerShell script and a reliable compliance tool for auditing and monitoring.

The Script

#Requires -Version 5.1

<#
.SYNOPSIS
    Finds and alerts on new user accounts created within a specified time frame, measured in minutes. If ran on a domain controller, it will use Active Directory to find new users, otherwise it will use the event log to find new users.
.DESCRIPTION
    This script finds and alerts on new user accounts created within a specified time frame, measured in minutes.
    It can be used to monitor user creation activity in an Active Directory environment or an individual system.

    On a domain controller, it retrieves new users from Active Directory. If the Active Directory Recycle Bin is enabled, it will also retrieve deleted users that were created within the specified time frame.
    On an individual system, it retrieves new users from the event log. The event log will not retain these events indefinitely.

    When running on an individual system, it checks if auditing is enabled for User Account Management.
    If auditing is not enabled, the following will need to be run in an elevated PowerShell/cmd session:
    auditpol.exe /set /subcategory:{0CCE9235-69AE-11D9-BED3-505054503030} /success:enable
    This will enable auditing for user account management events, including user creation required to track new user creation events.
By using this script, you indicate your acceptance of the following legal terms as well as our Terms of Use at https://www.ninjaone.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).

.PARAMETER TimeFrameInMinutes
    The time frame in minutes to check for new users.
    Minimum value is 5 minutes.
    Maximum value is 525600 minutes (1 year).
.PARAMETER MultilineCustomFieldName
    The name of the multiline custom field to set with the new user information.
    This field will be populated with the names and creation times of the new users.

.EXAMPLE
    -TimeFrameInMinutes 10000 -MultilineCustomFieldName "Multiline"

    This example checks for new users created within the last 10000 minutes and sets the multiline custom field "Multiline" with the user information.
    Example output:
    [Alert] New users created within the last 10000 minutes:
    [Alert] Username: test, Full Name: test test, Created On: 05/20/2025 13:13:35
    [Alert] Username: anewaccount, Full Name: Another New Account, Created On: 05/20/2025 15:01:10

    [Info] Attempting to set Custom Field 'Multiline'.
    [Info] Successfully set Custom Field 'Multiline'!

.EXAMPLE
    -TimeFrameInMinutes 2440

    This example checks for new users created within the last 1440 minutes (24 hours).
    Example output:
    [Alert] New users created within the last 1440 minutes:
    [Alert] Username: test, Full Name: test test, Created On: 05/20/2025 13:13:35
    [Alert] Username: anewaccount, Full Name: Another New Account, Created On: 05/20/2025 15:01:10

.NOTES
    Minimum OS Architecture Supported: Windows 10, Windows Server 2016
    Release Notes: Initial release
#>

[CmdletBinding()]
param (
    $TimeFrameInMinutes,
    [string]$MultilineCustomFieldName
)

begin {
    # Import the script variables
    if ($env:timeFrameInMinutes) { $TimeFrameInMinutes = $env:timeFrameInMinutes }
    if ($env:multilineCustomFieldName) { $MultilineCustomFieldName = $env:multilineCustomFieldName }

    function Test-IsElevated {
        [CmdletBinding()]
        param ()

        # Get the current Windows identity of the user running the script
        $id = [System.Security.Principal.WindowsIdentity]::GetCurrent()

        # Create a WindowsPrincipal object based on the current identity
        $p = New-Object System.Security.Principal.WindowsPrincipal($id)

        # Check if the current user is in the Administrator role
        # The function returns $True if the user has administrative privileges, $False otherwise
        # 544 is the value for the Built In Administrators role
        # Reference: https://learn.microsoft.com/en-us/dotnet/api/system.security.principal.windowsbuiltinrole
        $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]'544')
    }

    function Test-IsDomainJoined {
        # Check the PowerShell version to determine the appropriate cmdlet to use
        try {
            if ($PSVersionTable.PSVersion.Major -lt 3) {
                return $(Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain
            }
            else {
                return $(Get-CimInstance -Class Win32_ComputerSystem).PartOfDomain
            }
        }
        catch {
            Write-Host -Object "[Error] Unable to validate whether or not this device is a part of a domain."
            Write-Host -Object "[Error] $($_.Exception.Message)"
            exit 1
        }
    }

    function Test-IsDomainController {
        # Determine the method to retrieve the operating system information based on PowerShell version
        try {
            $OS = if ($PSVersionTable.PSVersion.Major -lt 3) {
                Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop
            }
            else {
                Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop
            }
        }
        catch {
            Write-Host -Object "[Error] Unable to validate whether or not this device is a domain controller."
            Write-Host -Object "[Error] $($_.Exception.Message)"
            exit 1
        }

        # Check if the ProductType is "2", which indicates that the system is a domain controller
        if ($OS.ProductType -eq "2") {
            return $true
        }
    }

    function Set-NinjaPropertyValue {
        [CmdletBinding()]
        Param(
            [Parameter(Mandatory = $True)]
            [String]$Name,
            [Parameter(Mandatory = $True, ValueFromPipeline = $True)]
            $Value,
            [Parameter()]
            [String]$Type,
            [Parameter()]
            [String]$DocumentName,
            [Parameter()]
            [Switch]$Piped
        )

        if ($Type -eq "Date Time") { $Type = "DateTime" }
        if ($Type -match "[-]") { $Type = $Type -replace '-' }
        if ($Type -match "[/]") { $Type = $Type -replace '/' }

        # Remove the non-breaking space character
        if ($Type -eq "WYSIWYG") {
            $Value = $Value -replace ' ', '&nbsp;'
        }

        if ($Type -eq "DateTime" -or $Type -eq "Date") {
            $Type = "Date or Date Time"
        }

        # Measure the number of characters in the provided value
        $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters

        # Throw an error if the value exceeds the character limit of 200,000 characters
        if ($Piped -and $Characters -ge 200000) {
            throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.")
        }

        if (!$Piped -and $Characters -ge 45000) {
            throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 45,000 characters.")
        }

        # Initialize a hashtable for additional documentation parameters
        $DocumentationParams = @{}

        # If a document name is provided, add it to the documentation parameters
        if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName }

        # Define a list of valid field types
        $ValidFields = "Checkbox", "Date", "Date or Date Time", "DateTime", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine",
        "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG"

        # Warn the user if the provided type is not valid
        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" }

        # Define types that require options to be retrieved
        $NeedsOptions = "Dropdown", "MultiSelect"

        # If the property is being set in a document or field and the type needs options, retrieve them
        if ($DocumentName) {
            if ($NeedsOptions -contains $Type) {
                $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1
            }
        }
        else {
            if ($NeedsOptions -contains $Type) {
                $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1
            }
        }

        # Throw an error if there was an issue retrieving the property options
        if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions }

        # Process the property value based on its type
        switch ($Type) {
            "Checkbox" {
                # Convert the value to a boolean for Checkbox type
                $NinjaValue = [System.Convert]::ToBoolean($Value)
            }
            "Date or Date Time" {
                # Convert the value to a Unix timestamp for Date or Date Time type
                $Date = (Get-Date $Value).ToUniversalTime()
                $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date
                [long]$NinjaValue = $TimeSpan.TotalSeconds
            }
            "Dropdown" {
                # Convert the dropdown value to its corresponding GUID
                $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name"
                $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID

                # Throw an error if the value is not present in the dropdown options
                if (!($Selection)) {
                    throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.")
                }

                $NinjaValue = $Selection
            }
            "MultiSelect" {
                $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name"
                $Selections = New-Object System.Collections.Generic.List[String]
                if ($Value -match "[,]") {
                    $Value = $Value -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
                }

                $Value | ForEach-Object {
                    $GivenValue = $_
                    $Selection = $Options | Where-Object { $_.Name -eq $GivenValue } | Select-Object -ExpandProperty GUID

                    # Throw an error if the value is not present in the dropdown options
                    if (!($Selection)) {
                        throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.")
                    }

                    $Selections.Add($Selection)
                }

                $NinjaValue = $Selections -join ","
            }
            "Time" {
                # Convert the value to a Unix timestamp for Date or Date Time type
                $LocalTime = (Get-Date $Value)
                $LocalTimeZone = [TimeZoneInfo]::Local
                $UtcTime = [TimeZoneInfo]::ConvertTimeToUtc($LocalTime, $LocalTimeZone)

                [long]$NinjaValue = ($UtcTime.TimeOfDay).TotalSeconds
            }
            default {
                # For other types, use the value as is
                $NinjaValue = $Value
            }
        }

        # Set the property value in the document if a document name is provided
        if ($DocumentName) {
            $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1
        }
        else {
            try {
                # Otherwise, set the standard property value
                if ($Piped) {
                    $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1
                }
                else {
                    $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1
                }
            }
            catch {
                throw $_.Exception.Message
            }
        }

        # Throw an error if setting the property failed
        if ($CustomField.Exception) {
            throw $CustomField
        }
    }

    function Get-OSVersion {
        # Get the OS version information
        return [System.Environment]::OSVersion.Version
    }

    function Test-IsSystem {
        [CmdletBinding()]
        param ()

        # Get the current Windows identity of the user running the script
        $id = [System.Security.Principal.WindowsIdentity]::GetCurrent()

        # Check if the current identity's name matches "NT AUTHORITY*"
        # or if the identity represents the SYSTEM account
        return $id.Name -like "NT AUTHORITY*" -or $id.IsSystem
    }

    # If time frame in minutes exists, validate that it is an integer
    # If it does not exist, error out because it is required
    if (-not [string]::IsNullOrWhiteSpace($TimeFrameInMinutes)) {
        # Convert the value to an integer
        try {
            $TimeFrameInMinutes = [int]$TimeFrameInMinutes
        }
        catch {
            Write-Host -Object "[Error] Error while converting the value for 'Time Frame In Minutes' to an integer:"
            Write-Host -Object "[Error] $($_.Exception.Message)"
            Write-Host -Object "[Error] The value for 'Time Frame In Minutes' must be an integer between 5 and 525600."
            exit 1
        }

        # Validate the TimeFrameInMinutes is within the valid range
        if ($TimeFrameInMinutes -lt 5 -or $TimeFrameInMinutes -gt 525600) {
            Write-Host -Object "[Error] An invalid value was provided for 'Time Frame In Minutes': '$TimeFrameInMinutes'"
            Write-Host -Object "[Error] The value for 'Time Frame In Minutes' must be an integer between 5 and 525600."
            exit 1
        }
    }
    else {
        Write-Host -Object "[Error] The value for 'Time Frame In Minutes' is required and must be provided."
        exit 1
    }

    # Validate the custom field name if it exists
    if ($MultilineCustomFieldName) {
        # Error if the custom field name is empty
        if ([string]::IsNullOrWhiteSpace($MultilineCustomFieldName)) {
            Write-Host -Object "[Error] The value for 'Multiline Custom Field Name' cannot be empty."
            exit 1
        }

        # Trim whitespace from the custom field name
        $MultilineCustomFieldName = $MultilineCustomFieldName.Trim()

        # Validate that the field name contains only alphanumeric characters
        if ($MultilineCustomFieldName -match "[^0-9A-Z]") {
            Write-Host -Object "[Error] The 'Multiline Custom Field Name' of '$MultilineCustomFieldName' is invalid as it contains invalid characters."
            Write-Host -Object "[Error] Please provide a valid multiline custom field name to save the results, or leave it blank."
            Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/en-us/articles/360060920631-Custom-Field-Setup"
            exit 1
        }
    }

    # Get the OS version information
    try {
        $osVersion = Get-OSVersion -ErrorAction Stop
    }
    catch {
        Write-Host -Object "[Error] Failed to retrieve the OS version information."
        Write-Host -Object "[Error] $($_.Exception.Message)"
        exit 1
    }

    # Check if the OS version is supported
    if ($osVersion.Major -lt 10 -or ($osVersion.Major -eq 10 -and $osVersion.Build -lt 14393)) {
        Write-Host -Object "[Error] This script requires Windows 10 or Windows Server 2016 or later."
        exit 1
    }

}
process {
    # Check if the script is running with administrative privileges
    if (-not (Test-IsElevated)) {
        Write-Host -Object "[Error] This script requires administrative privileges. Please run as an administrator."
        exit 1
    }

    # Error if not running as system
    if (-not (Test-IsSystem)) {
        Write-Host -Object "[Error] This script must be run as the SYSTEM account. Please run as the SYSTEM account."
        exit 1
    }

    $ExitCode = 0

    # Get the current date and time
    try {
        $currentDateTime = Get-Date -ErrorAction Stop
    }
    catch {
        Write-Host -Object "[Error] Failed to retrieve the current date and time."
        Write-Host -Object "[Error] $($_.Exception.Message)"
        exit 1
    }

    try {
        # Calculate the start date and time based on the specified time frame
        $startDateTime = $currentDateTime.AddMinutes(-$TimeFrameInMinutes)
    }
    catch {
        Write-Host -Object "[Error] Failed to calculate the start date and time."
        Write-Host -Object "[Error] $($_.Exception.Message)"
        exit 1
    }

    # Get the list of new users created within the specified time frame
    # If host is a DC, get new users from Active Directory
    # Otherwise, get new users from the event log
    if (Test-IsDomainController) {
        Write-Host -Object "`n[Info] This host is a domain controller. Checking for new users in Active Directory created within the last $TimeFrameInMinutes minutes."

        $IsDomainController = $true

        # Initialize a list to hold new users
        $newUsers = [System.Collections.Generic.List[PSCustomObject]]::new()

        # Check if the AD Recycle Bin is enabled
        try {
            $ADRecycleBinEnabled = (Get-ADOptionalFeature -Filter { Name -eq "Recycle Bin Feature" } -ErrorAction Stop).FeatureScope
        }
        catch {
            Write-Host -Object "[Error] Failed to check if the AD Recycle Bin is enabled."
            Write-Host -Object "[Error] $($_.Exception.Message)"
            exit 1
        }

        if (-not $ADRecycleBinEnabled) {
            Write-Host -Object "[Warning] The Active Directory Recycle Bin is not enabled. Deleted users cannot be tracked until the Recycle Bin is enabled. This may lead to less accurate results."
        }
        else {
            try {
                # If the Recycle Bin is enabled, try to find deleted users created within the time frame
                $deletedADUsers = Get-ADObject -Filter {
                    ObjectClass -eq "User" -and
                    ObjectClass -ne "Computer" -and
                    IsDeleted -eq $TRUE -and
                    WhenCreated -ge $startDateTime
                } -Properties SamAccountName, WhenCreated -IncludeDeletedObjects -ErrorAction Stop

                # Format the deleted users to include only the necessary properties and add a status property
                $deletedADUsers = $deletedADUsers | Select-Object Name,
                SamAccountName,
                @{
                    Name       = "WhenCreated"
                    Expression = { $_.WhenCreated.ToShortDateString() + " " + $_.WhenCreated.ToLongTimeString() }
                },
                @{
                    Name       = "Status"
                    Expression = { "Deleted" }
                }
            }
            catch {
                Write-Host -Object "[Error] Failed to retrieve deleted users from Active Directory."
                Write-Host -Object "[Error] $($_.Exception.Message)"
                exit 1
            }
        }

        try {
            # Find new users in AD created within the time frame
            $newUsersInAD = Get-ADUser -Filter { WhenCreated -ge $startDateTime } -Properties WhenCreated -ErrorAction Stop

            # Format new users in AD to include only the necessary properties and add a status property
            $newUsersInAD = $newUsersInAD | Select-Object Name,
            SamAccountName,
            @{
                Name       = "WhenCreated";
                Expression = { $_.WhenCreated.ToShortDateString() + " " + $_.WhenCreated.ToLongTimeString() }
            },
            @{
                Name       = "Status";
                Expression = {
                    if ($_.Enabled) {
                        "Enabled"
                    }
                    else {
                        "Disabled"
                    }
                }
            }
        }
        catch {
            Write-Host -Object "[Error] Failed to retrieve users from Active Directory."
            Write-Host -Object "[Error] $($_.Exception.Message)"
            exit 1
        }

        # Add each new user in AD to the list of new users
        foreach ($user in $newUsersInAD) {
            $newUsers.Add($user)
        }

        # Add each deleted AD user that was created within the time frame to the list of new users
        foreach ($user in $deletedADUsers) {
            # Modify the name of the user to only include the first line before adding it to the list
            $user.Name = $user.Name -split "`n" | Select-Object -First 1
            $newUsers.Add($user)
        }
    }
    else {
        Write-Host -Object "`n[Info] Checking for new local users created within the last $TimeFrameInMinutes minutes in the event log."

        # Warn if domain-joined that only local users will be included
        if (Test-IsDomainJoined) {
            Write-Host -Object "`n[Warning] This host is domain-joined but not a domain controller. Only local users will be included in the results."
            Write-Host -Object "[Warning] Please run this script on a domain controller if you'd like to alert on domain user accounts."
        }

        # Check if user account creation auditing is enabled
        $AuditPolicy = (Get-ItemProperty HKLM:\Security\Policy\PolAdtEv\ -ErrorAction SilentlyContinue)."(default)"

        if ($AuditPolicy) {
            # If enabled, the value of this should be 1 (success only) or 3 (success and failure)
            $UserCreationAuditingValue = $AuditPolicy[102]

            if ($UserCreationAuditingValue -ne 1 -and $UserCreationAuditingValue -ne 3) {
                Write-Host -Object "[Error] Auditing is not enabled for User Account Management. Please enable it to track new user creation events."
                Write-Host -Object "[Info] After auditing is enabled, only new user creation events after the change will be tracked."
                Write-Host -Object "[Info] To enable auditing, run the following command in an elevated PowerShell/cmd session:"
                Write-Host -Object "[Info] auditpol.exe /set /subcategory:'{0CCE9235-69AE-11D9-BED3-505054503030}' /success:enable"
                exit 1
            }
        }
        else {
            Write-Host -Object "[Error] Failed to check if auditing is enabled for User Account Management. This is usually due to the script not running as the SYSTEM account."
            exit 1
        }

        # Get new users from the event log
        # Event ID 4720 corresponds to "A user account was created"
        $eventLog = @(Get-WinEvent -FilterHashtable @{LogName = "Security"; Id = 4720; StartTime = $startDateTime } -ErrorAction SilentlyContinue)

        # Process each event log entry to extract the user information
        $newUsers = foreach ($event in $eventLog) {
            # Convert the event to an XML object
            $eventXML = [xml]$event.ToXml()

            # Extract the username from the XML
            $userName = $eventXml.Event.EventData.Data | Where-Object { $_.Name -eq "TargetUserName" } | Select-Object -ExpandProperty '#text'

            # If a username is found, attempt to retrieve the local user's full name from their username
            # Otherwise, error and continue to the next event log entry
            if ($userName) {
                try {
                    $localUser = Get-LocalUser -Name $UserName -ErrorAction Stop

                    # Determine the status of the user account
                    $status = if ($localUser.Enabled) { "Enabled" } else { "Disabled" }

                    if (-not [string]::IsNullOrWhiteSpace($localUser.FullName)) {
                        $fullName = $localUser.FullName
                    }
                    elseif (-not [string]::IsNullOrWhiteSpace($localUser.Name)) {
                        $fullName = $localUser.Name
                    }
                    else {
                        $fullName = "Unknown"
                    }
                }
                catch [Microsoft.PowerShell.Commands.UserNotFoundException] {
                    # If the user is not found, it may have been deleted or renamed, so set the status to "No Longer Exists"
                    $fullName = "Unknown"
                    $Status = "No Longer Exists"
                }
                catch {
                    Write-Host -Object "[Error] Unable to retrieve the full name of the user '$userName'."
                    Write-Host -Object "[Error] $($_.Exception.Message)"
                    $fullName = "Unknown"
                    $Status = "Unknown"
                    $ExitCode = 1
                }

                # Create a custom object with the user information
                [PSCustomObject]@{
                    UserName    = $userName
                    Name        = $fullName
                    Status      = $status
                    WhenCreated = $event.TimeCreated.ToShortDateString() + " " + $event.TimeCreated.ToLongTimeString()
                }
            }
            else {
                Write-Host -Object "[Error] Failed to extract the username from the event data."
                $ExitCode = 1
                continue
            }
        }
    }

    # If new users are found not from AD, filter out any duplicate users in the list, keeping the most recently created instance only
    if ($newUsers.Count -gt 1 -and -not $IsDomainController) {
        # Group the users by username and select the most recent entry for each group
        $newUsers = $newUsers | Group-Object -Property UserName | ForEach-Object {
            $_.Group | Sort-Object -Property WhenCreated -Descending | Select-Object -First 1
        }
    }

    # Check if any new users were found
    if ($newUsers) {
        Write-Host -Object "`n[Alert] New users have been created within the last $TimeFrameInMinutes minutes:`n"

        # Format the new users object for output, showing the Enabled accounts first, then the Disabled accounts
        $newUsers = $newUsers | Sort-Object -Property { $_.Status -eq "Enabled"; $_.Status -eq "Disabled" }, WhenCreated -Descending | Select-Object @{
            Name       = "Username"
            Expression = { if ($IsDomainController) { $_.SamAccountName } else { $_.UserName } }
        }, @{
            Name       = "Full Name"
            Expression = { $_.Name }
        }, @{
            Name       = "Created On"
            Expression = { $_.WhenCreated }
        }, Status

        # Format it as a list and trim the whitespace
        $newUsers = ($newUsers | Format-List | Out-String).Trim()

        # Output to the activity feed
        $newUsers | Out-Host
    }
    elseif ($MultilineCustomFieldName) {
        Write-Host -Object "[Info] No new users have been created within the last $TimeFrameInMinutes minutes. The custom field '$MultilineCustomFieldName' will not be modified."
    }
    else {
        Write-Host -Object "[Info] No new users have been created within the last $TimeFrameInMinutes minutes."
    }

    # Write to custom field if there are new users
    if ($newUsers -and $MultilineCustomFieldName) {
        try {
            Write-Host -Object "`n[Info] Attempting to set the custom field '$MultilineCustomFieldName'."

            Set-NinjaPropertyValue -Name $MultilineCustomFieldName -Value $newUsers -Type "MultiLine"

            Write-Host -Object "[Info] Successfully set the custom field '$MultilineCustomFieldName'!"
        }
        catch {
            Write-Host -Object "[Error] Error setting the custom field '$MultilineCustomFieldName'."
            Write-Host -Object "[Error] $($_.Exception.Message)"
            $ExitCode = 1
        }
    }

    exit $ExitCode
}

end {
    
    
    
}

 

Detailed Breakdown

Here’s how the script works, broken into logical stages:

1. Input Validation

  • Accepts two parameters:
    • TimeFrameInMinutes: The window of time to check (5 minutes to 1 year).
    • MultilineCustomFieldName: The NinjaOne custom field to store the results (optional).
  • Performs strict checks for minimum OS requirements (Windows 10/Server 2016), PowerShell version, proper formatting, and permissions (must run as SYSTEM and with admin rights).

2. Environment Detection

  • Determines if the system:
    • Is domain-joined.
    • Is a domain controller.
    • Has auditing for user account management enabled (important for non-DCs).
  • Depending on the environment, it chooses between Active Directory and local event logs as the data source.

3. New User Detection

  • Domain Controller Mode:
    • Queries Active Directory for all users created after the calculated startDateTime.
    • Optionally checks the AD Recycle Bin for recently created and deleted users.
  • Standalone/Non-DC Mode:
    • Reads Windows Security event log for Event ID 4720 (user account creation).
    • Extracts usernames and enriches them with details from Get-LocalUser.

4. Data Formatting and Logging

  • Results are formatted into a clean, readable object showing:
    • Username
    • Full Name
    • Creation Time
    • Account Status (Enabled, Disabled, Deleted)

5. NinjaOne Integration

  • If a valid custom field is provided, it uses the Set-NinjaPropertyValue function to post data into the NinjaOne platform.
  • Ensures no custom field changes are made if no users are found.

Potential Use Cases

Case Study: MSP Monitoring Domain Environments

Imagine an MSP managing a domain for a financial client. They deploy this script to run hourly on all Domain Controllers via NinjaOne. One day, the script alerts them that a user named jsmith_temp was created in the last 30 minutes. This was unexpected, and after investigation, they discover an HR automation tool misfired during onboarding. Without this script, that account might have remained unnoticed, posing potential risk.

Comparisons

MethodProsCons
This PowerShell ScriptCentralized logging, NinjaOne integration, supports both DC and localRequires SYSTEM-level execution, setup for auditing
Manual Event Log MonitoringNative, no scripting neededLabor-intensive, lacks automation
SIEM Tools (e.g., Splunk)Scalable, correlation-friendlyExpensive, complex configuration
AD Email AlertsSimpleNo historical tracking, lacks formatting

Compared to these, the PowerShell script offers an affordable and extensible middle ground—ideal for MSPs and mid-sized enterprises.

Implications

Regular execution of this PowerShell script introduces proactive account monitoring into your security posture. Identifying unexpected account creations can help prevent privilege escalation, maintain compliance (HIPAA, SOX, etc.), and support internal audits. When integrated with NinjaOne, this script enables centralized and consistent visibility across devices and tenants—empowering MSPs to react faster and with confidence.

Recommendations

  • Run as SYSTEM: Ensure you execute the script with SYSTEM privileges—especially critical for accessing security logs.
  • Audit Settings: Always enable User Account Management auditing on local systems to get reliable data.
  • Field Validation: Use only alphanumeric names for NinjaOne custom fields to avoid format issues.
  • Timeframe Tuning: Adjust TimeFrameInMinutes depending on script frequency—e.g., 1440 for daily, 60 for hourly.
  • Error Handling: Monitor the script’s output logs for [Error] entries to catch misconfigurations.

Final Thoughts

New user account creation is a sensitive operation that requires visibility and control. This PowerShell script provides a smart, reliable way to create New User Alerts with minimal overhead. When coupled with NinjaOne, it becomes even more powerful—enabling actionable insights and historical logging across your infrastructure. Whether you’re a solo IT admin or managing a team at an MSP, this solution helps you stay one step ahead in user account security.

FAQs

Yes. This script requires SYSTEM-level permissions to access event logs and NinjaOne custom fields.

On non-DC systems, the script checks and informs you how to enable auditing using auditpol.exe.

Yes, as long as auditing is enabled and the system meets the OS requirements.

Absolutely. It’s designed to run via NinjaOne’s scripting module with parameters and can push alerts to custom fields for easy reporting.

Next Steps

Building an efficient and effective IT team requires a centralized solution that acts as your core service delivery tool. NinjaOne enables IT teams to monitor, manage, secure, and support all their devices, wherever they are, without the need for complex on-premises infrastructure.

Learn more about NinjaOne Remote Script Deployment, check out a live tour, or start your free trial of the NinjaOne platform.

Categories:

You might also like