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 ' ', ' '
}
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.
- Queries Active Directory for all users created after the calculated
- 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-NinjaPropertyValuefunction 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
| Method | Pros | Cons |
| This PowerShell Script | Centralized logging, NinjaOne integration, supports both DC and local | Requires SYSTEM-level execution, setup for auditing |
| Manual Event Log Monitoring | Native, no scripting needed | Labor-intensive, lacks automation |
| SIEM Tools (e.g., Splunk) | Scalable, correlation-friendly | Expensive, complex configuration |
| AD Email Alerts | Simple | No 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
TimeFrameInMinutesdepending 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.