Understanding a device’s placement within your Active Directory (AD) structure is vital for managing access, applying group policies, and maintaining network security. For Managed Service Providers (MSPs) and internal IT teams, automating the discovery of a computer’s Organizational Unit (OU) can save significant time and reduce human error. This blog post explores a robust PowerShell script designed to view a device’s Organizational Unit (OU) and, optionally, store that information in NinjaOne—a widely used endpoint management platform.
Background
In large and even mid-sized enterprise environments, devices are often segmented into OUs within Active Directory to apply targeted policies. These OUs may reflect functional roles (e.g., “Servers”), departments (e.g., “Finance”), or device types (e.g., “Laptops”). Identifying where a specific machine resides in the AD hierarchy can be a common task for technicians—but doing this manually or with inconsistent scripts can lead to inefficiencies.
This PowerShell script bridges that gap by automating the process and integrating with NinjaOne’s custom fields. For MSPs that rely on NinjaOne to consolidate device information, this automation ensures that OU data is always available and up-to-date, providing context for troubleshooting, auditing, and compliance.
The Script:
<# .SYNOPSIS Gets the Organizational Units (OUs) that this device is a member of in Active Directory or Azure AD. .DESCRIPTION Gets the Organizational Units (OUs) that this device is a member of in Active Directory or Azure AD. 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). .EXAMPLE -CustomFieldName "ReplaceMeWithAnyMultilineCustomField" Attempting to set Custom Field 'ReplaceMeWithAnyMultilineCustomField'. Successfully set Custom Field 'ReplaceMeWithAnyMultilineCustomField'! Organizational Units Found: OU=Domain Controllers,OU=Computers,DC=test,DC=lan OU=Servers,OU=Computers,DC=test,DC=lan PARAMETER: -CustomFieldName "ReplaceMeWithAnyMultilineCustomField" Name of a multiline custom field to save the results to. .EXAMPLE -CustomFieldName "ReplaceMeWithAnyMultilineCustomField" Attempting to set Custom Field 'ReplaceMeWithAnyMultilineCustomField'. Successfully set Custom Field 'ReplaceMeWithAnyMultilineCustomField'! Organizational Units Found: OU=Domain Controllers,OU=Computers,DC=test,DC=lan OU=Servers,OU=Computers,DC=test,DC=lan .OUTPUTS None .NOTES Minimum OS Architecture Supported: Windows 10, Windows Server 2016 Release Notes: Initial Release #> [CmdletBinding()] param ( [Parameter()] [String]$CustomFieldName ) begin { # If using script form variables, replace command line parameters with the form variables. if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomFieldName = $env:customFieldName } # Function to check if the script is running with elevated (administrator) privileges function Test-IsElevated { $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() $p = New-Object System.Security.Principal.WindowsPrincipal($id) $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) } function Test-IsDomainJoined { # Check the PowerShell version to determine the appropriate cmdlet to use if ($PSVersionTable.PSVersion.Major -lt 5) { return $(Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain } else { return $(Get-CimInstance -Class Win32_ComputerSystem).PartOfDomain } } function Set-NinjaProperty { [CmdletBinding()] Param( [Parameter(Mandatory = $True)] [String]$Name, [Parameter()] [String]$Type, [Parameter(Mandatory = $True, ValueFromPipeline = $True)] $Value, [Parameter()] [String]$DocumentName ) # Measure the number of characters in the provided value $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters # Throw an error if the value exceeds the character limit of 200,000 characters if ($Characters -ge 200000) { throw "Character limit exceeded: the value is greater than or equal to 200,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 = "Attachment", "Checkbox", "Date", "Date or Date Time", "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" # 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 $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 } 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 { # Otherwise, set the standard property value $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 } # Throw an error if setting the property failed if ($CustomField.Exception) { throw $CustomField } } } process { # Check if the script is running with elevated (administrator) privileges if (!(Test-IsElevated)) { Write-Host "[Error] Access Denied. Please run with Administrator privileges." exit 1 } $regPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine' $DistinguishedName = Get-ItemProperty -Path $regPath -Name 'Distinguished-Name' -ErrorAction SilentlyContinue $OrganizationalUnit = if ($DistinguishedName -and $DistinguishedName.'Distinguished-Name') { $OU = $DistinguishedName.'Distinguished-Name' -replace '^CN=.*?,', '' Write-Output $OU } else { Write-Host "[Warn] Failed to retrieve Organizational Unit from Group Policy State." } $OrganizationalUnit = if (Test-ComputerSecureChannel -ErrorAction SilentlyContinue) { "$OrganizationalUnit" } else { "(Cached) $OrganizationalUnit" } if ($OrganizationalUnit) { Write-Host "[Info] The OU for $env:COMPUTERNAME is: $OrganizationalUnit" } else { Write-Host "[Error] Failed to retrieve Organizational Units." exit 1 } # If custom field name is provided, set the custom field with the list of OUs if ($CustomFieldName) { try { Write-Host "[Info] Attempting to set Custom Field '$CustomFieldName'." if ((Test-IsDomainJoined)) { Set-NinjaProperty -Name $CustomFieldName -Value $($OrganizationalUnit | Out-String) } else { Set-NinjaProperty -Name $CustomFieldName -Value "Workgroup" } Write-Host "[Info] Successfully set Custom Field '$CustomFieldName'!" } catch { Write-Host "[Error] $($_.Exception.Message)" exit 1 } } exit 0 } end { }
Detailed Breakdown
Let’s walk through the script step by step:
- Parameter Handling and Environment Variables
The script accepts a -CustomFieldName parameter, which allows the user to define where the OU data should be saved in NinjaOne. It also checks if this parameter is being passed as an environment variable—useful for running the script from automated workflows. - Elevation Check
It confirms that the script is being executed with administrator privileges. If not, it exits early to avoid permission issues when accessing system and registry paths. - Domain Membership Detection
A helper function determines whether the device is joined to a domain, using either Get-WmiObject or Get-CimInstance based on PowerShell version. - OU Extraction
The script accesses the registry key HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine to retrieve the device’s distinguished name and strips out the CN portion to isolate the OU path. - Secure Channel Validation
It uses Test-ComputerSecureChannel to verify that the machine’s connection to the domain is valid. If the connection is broken, the result is marked as cached. - Output and NinjaOne Integration
If a custom field name is provided, the script uses Set-NinjaProperty to upload the OU data into NinjaOne. This function includes error handling, validation for field types, and support for both standard and document-linked fields. - Logging and Error Handling
Every critical step includes logging to the console for easier troubleshooting, along with well-structured error messages if anything fails.
Potential Use Cases
Imagine a scenario where an MSP onboards a new client with 200 domain-joined workstations. The client has poor documentation of their AD structure. By deploying this script across all endpoints via NinjaOne, the MSP can instantly populate each machine’s OU into a custom field. This visibility allows them to:
- Audit AD structure
- Validate policy scope
- Target scripts and alerts based on OU
It streamlines onboarding and helps establish a baseline for managing devices.
Comparisons
Traditional approaches to retrieve a device’s OU might include:
- Manually running Get-ADComputer from a domain controller
- Checking ADUC (Active Directory Users and Computers)
- Using Group Policy Result Wizard
Each of these methods has its limitations—chief among them being the lack of automation and integration. This script, by contrast, runs locally on the endpoint, does not require RSAT tools, and integrates directly with NinjaOne’s console for centralized reporting.
FAQs
Q: Does this work on Azure AD-only devices?
A: No, the script relies on the Windows registry and Active Directory. Azure AD-joined or hybrid devices without domain OUs won’t return expected results.
Q: Can this script be run on non-Windows 10 devices?
A: It supports Windows 10 and Windows Server 2016 or newer. Older systems may not support all the cmdlets used.
Q: What if the custom field exceeds size limits?
A: The script enforces a 200,000 character limit when setting field values and throws a clear error if exceeded.
Q: What happens if the secure channel is broken?
A: The OU is still retrieved but marked as “(Cached)” to indicate potential staleness.
Implications
Knowing a device’s OU has broader implications than simple inventory. It influences:
- Security baselines and patching schedules
- Group Policy enforcement
- Role-based access control
- Compliance audits
Incorrect or outdated OU data can lead to misapplied policies, security vulnerabilities, and audit failures. Automating this discovery ensures consistent, actionable data across an organization.
Recommendations
- Run as administrator to ensure access to registry and policy state.
- Deploy via NinjaOne policies for broad coverage.
- Use logging and custom field updates to centralize the data.
- Combine with AD health checks to validate the secure channel and domain join status.
- Schedule periodic runs to detect changes in OU placement.
Final Thoughts
This script is more than a utility—it’s an automation asset. By pulling actionable data into NinjaOne, MSPs and IT admins gain real-time insights into their infrastructure. Whether for compliance, troubleshooting, or planning, knowing a device’s OU is foundational—and with this PowerShell script, it’s no longer a manual task.
NinjaOne’s flexible custom fields and robust scripting integration make it an ideal platform to leverage tools like this. For MSPs aiming to scale their operations while maintaining precision, automating OU discovery is a simple win with a big payoff.