# ============================================================================== # Cayosoft License Counter version 2.0.3 # Copyright Cayosoft Inc., 2025 # Use this tool to count the number of licenses required by Cayosoft products # to manage your Active Directory domains and/or Microsoft 365 tenant. # # If you have any questions, please contact Cayosoft at sales@cayosoft.com. # ============================================================================== param($ADCredential, $AzureEnvironment) [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $loginAuthURL="login.microsoftonline.com"; $msGraphURL="graph.microsoft.com"; $azureLinks=@{ "AzureCloud"=@{"login"="login.microsoftonline.com";"graph"="graph.microsoft.com";} "AzureChinaCloud"=@{"login"="login.chinacloudapi.cn";"graph"="microsoftgraph.chinacloudapi.cn";} "USGovernment"=@{"login"="login.microsoftonline.us";"graph"="graph.microsoft.us";} "AzureUSGovernmentCloud"=@{"login"="login.microsoftonline.us";"graph"="graph.microsoft.us";} "AzureUSGovernmentCloud2"=@{"login"="login.microsoftonline.eaglex.ic.gov";"graph"="graph.eaglex.ic.gov";} "AzureUSGovernmentCloud3"=@{"login"="login.microsoftonline.microsoft.scloud";"graph"="graph.microsoft.scloud";} "AzureGermanyCloud"=@{"login"="login.microsoftonline.de";"graph"="graph.microsoft.de";} "AzurePPE"=@{"login"="login.windows-ppe.net";"graph"="graph.microsoft-ppe.com";} "AzureOneBox"=@{"login"="login.microsoftonline.com";"graph"="graph.microsoft.com";} } if("$($AzureEnvironment)" -ne "" -and $azureLinks.ContainsKey($AzureEnvironment)){ $loginAuthURL = $azureLinks[$AzureEnvironment].login; $msGraphURL = $azureLinks[$AzureEnvironment].graph; } $adPrams = @{} #$offPrams = @{} if($Null -ne $ADCredential){ $adPrams["Credential"] = $ADCredential } $CollectExchangeOnly = $true # Collect licenses with EXO plans $DefaultAccount = $null # Default login hint function Get-OfficeLicenses { Param([Parameter(Mandatory=$false)][string]$LoginHint,[Parameter(Mandatory=$false)][switch]$SupportedOnly) ($accessToken = Get-OfficeAccessToken -LoginHint $LoginHint) | Out-Null Sleep -s 6 $exoPlans = @( @{ id="EXCHANGE_B_STANDARD"; skuId="90927877-dcff-4af6-b346-2332c0b15bb7"; displayName="Exchange Online POP"; }, @{ id="EXCHANGE_L_STANDARD"; skuId="d42bdbd6-c335-4231-ab3d-c8f348d5aff5"; displayName="Exchange Online (Plan 1)"; }, @{ id="EXCHANGE_S_ESSENTIALS"; skuId="1126bef5-da20-4f07-b45e-ad25d2581aa8"; displayName="Exchange Essentials"; }, @{ id="EXCHANGE_S_STANDARD_MIDMARKET"; skuId="fc52cc4b-ed7d-472d-bbe7-b081c23ecc56"; displayName="Exchange Online (Plan 1)"; }, @{ id="EXCHANGE_S_DESKLESS"; skuId="4a82b400-a79f-41a4-b4e2-e94f5787b113"; displayName="Exchange Online Kiosk"; }, @{ id="EXCHANGE_S_ENTERPRISE"; skuId="efb87545-963c-4e0d-99df-69c6916d9eb0"; displayName="Exchange Online (Plan 2)" ; }, @{ id="EXCHANGE_S_ENTERPRISE_GOV"; skuId="8c3069c0-ccdb-44be-ab77-986203a67df2"; displayName="Exchange Online (Plan 2)"; }, @{ id="EXCHANGE_S_STANDARD"; skuId="9aaf7827-d63c-4b61-89c3-182f06f82e5c"; displayName="Exchange Online (Plan 1)" ; }, @{ id="EXCHANGE_S_STANDARD_GOV"; skuId="e9b4930a-925f-45e2-ac2a-3f7788ca6fdd"; displayName="Exchange Online (Plan 1) for GCC"; }, @{ id="EXCHANGE_S_DESKLESS_GOV"; skuId="88f4d7ef-a73b-4246-8047-516022144c9f"; displayName="Exchange Online (Kiosk) for Government"; } ) $excludedSkus = @( "4468c39a-28b2-42fb-9094-840bcf28771f" ) $Error.Clear(); try{ $global:skus = Invoke-RestMethod -Uri "https://$($msGraphURL)/v1.0/subscribedSkus" -Headers @{Authorization = "Bearer $($AccessToken)" } -Method GET }catch{ } #Retry for($i=0;$i -lt 10;$i++){ if($Error.Count -gt 0 -and $Null -ne $Error.Exception -and $Null -ne $Error.Exception.Response -and $Error.Exception.Response.StatusCode -eq 429){ Sleep -s 6 $Error.Clear(); try{ $global:skus = Invoke-RestMethod -Uri "https://$($msGraphURL)/v1.0/subscribedSkus" -Headers @{Authorization = "Bearer $($AccessToken)" } -Method GET }catch{ } }else{ break; } } if($Error.Count -gt 0){ Write-Error "$($Error[0])" } $collected = @() $result = @{"Licenses"=@()} foreach ($sku in $skus.value) { $included = -not $SupportedOnly.IsPresent $exoPlan = $null if (-not $included) { if ($sku.skuId -and $excludedSkus.Contains($sku.skuId)) { $included = $false } elseif ($sku.servicePlans) { foreach ($plan in $sku.servicePlans) { $included = $exoPlans.skuId -contains $plan.servicePlanId if ($included) { $exoPlan = $plan break } } } } if ($included) { Write-Host "SKU is collected: $($sku.skuPartNumber)" -ForegroundColor Green $paid = $sku.prepaidUnits $license = New-Object PSObject $license | add-member Noteproperty SKU $sku.skuId; $license | add-member Noteproperty Name $sku.skuPartNumber; $license | add-member Noteproperty Consumed $sku.consumedUnits; $license | add-member Noteproperty Enabled $paid.Enabled; $license | add-member Noteproperty Suspended $paid.suspended; $license | add-member Noteproperty Warning $paid.warning; $license | add-member Noteproperty LockedOut $paid.lockedOut; $license | add-member Noteproperty Capability $sku.capabilityStatus; $license | add-member Noteproperty Account $sku.accountName; $license | add-member Noteproperty Plans ($sku.servicePlans.servicePlanName -join ' ') $collected += $license } else { Write-Host "SKU is excluded: $($sku.skuPartNumber)" -ForegroundColor Yellow } } return @{"Licenses"=$collected}; } function Get-OfficeAccessToken { Param([Parameter(Mandatory=$False)][bool]$ForceMFA=$false, [Parameter(Mandatory=$False)][string]$LoginHint) $tokens = Get-AccessToken -Resource "https://$($msGraphURL)" -ClientId "d3590ed6-52b3-4102-aeff-aad2292ab01c" -LoginHint $LoginHint #-Credentials $tokens.AccessToken } function Get-UserRealm { Param([Parameter(Mandatory=$True)][String]$UserName) Invoke-RestMethod -UseBasicParsing -Uri ("https://$($loginAuthURL)/common/userrealm/$UserName"+"?api-version=1.0") } function Get-OAuthInfo { Param( [Parameter(Mandatory=$True)] [System.Management.Automation.PSCredential]$Credentials, [Parameter(Mandatory=$True)] [String]$Resource, [Parameter(Mandatory=$False)][String]$ClientId="1b730954-1685-4b74-9bfd-dac224a7b894" ) Process { $headers = @{"User-Agent" = "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; Tablet PC 2.0; Microsoft Outlook 16.0.4266)" } $userRealm = Get-UserRealm($Credentials.UserName) if($userRealm.account_type -eq "Unknown") { Write-Error "User type of $($Credentials.Username) is Unknown!" return $null } elseif ($userRealm.account_type -eq "Managed") { $body = @{ "resource"=$Resource "client_id"=$ClientId "grant_type"="password" "username"=$Credentials.UserName "password"=$Credentials.GetNetworkCredential().Password "scope"="openid" } # Set the content type and call the Microsoft Online authentication API $contentType="application/x-www-form-urlencoded" try { $jsonResponse = Invoke-RestMethod -UseBasicParsing -Uri "https://$($loginAuthURL)/common/oauth2/token" -ContentType $contentType -Method POST -Body $body -Headers $headers } catch { Throw ($_.ErrorDetails.Message | convertfrom-json).error_description } } else { Write-Error "User type of $($userRealm.account_type) is not supported" return $null } $jsonResponse } } Function Convert-B64ToByteArray { param([parameter(Mandatory=$true,ValueFromPipeline)][String]$B64) $B64 = $B64.Replace("_","/").Replace("-","+").TrimEnd(0x00,"=") while ($B64.Length % 4){ $B64 += "=" } return [convert]::FromBase64String($B64) } function Read-Accesstoken { Param([Parameter(Mandatory=$True,ValueFromPipeline)][String]$AccessToken) $sections = $AccessToken.Split(".") $header = $sections[0] $payload = $sections[1] $signature = $sections[2] $base64 = [byte[]](Convert-B64ToByteArray -B64 $payload) [text.encoding]::UTF8.GetString($base64) | ConvertFrom-Json } function Prompt-Credentials { Param( [Parameter(Mandatory=$True)][String]$Resource, [Parameter(Mandatory=$True)][String]$ClientId, [Parameter(Mandatory=$False)][String]$Tenant, [Parameter(Mandatory=$False)][bool]$ForceMFA=$false, [Parameter(Mandatory=$False)][string]$LoginHint ) Process { if([string]::IsNullOrEmpty($Tenant)) { $Tenant = "common" } $auth_redirect = Get-AuthRedirectUrl -ClientId $ClientId -Resource $Resource $request_id=(New-Guid).ToString() $url="https://$($loginAuthURL)/$Tenant/oauth2/authorize?resource=$Resource&client_id=$ClientId&response_type=code&haschrome=1&redirect_uri=$auth_redirect&client-request-id=$request_id&prompt=login&scope=openid profile" if($ForceMFA){ $url+="&amr_values=mfa" } if ([string]::IsNullOrEmpty($LoginHint) -eq $false){ $url+="&login_hint=$($LoginHint)" } if($ClientId -eq "29d9ed98-a469-4536-ade2-f981bc1d605e" -and $Resource -ne "https://enrollment.manage.microsoft.com/"){ $auth_redirect="ms-aadj-redir://auth/drs" } $form = Create-LoginForm -Url $url -auth_redirect $auth_redirect if($form.ShowDialog() -ne "OK") { $form.Controls[0].Dispose() return $null } $response = [System.Web.HttpUtility]::ParseQueryString($form.Controls[0].Url.Query) $code = $response["code"] if ([string]::IsNullOrEmpty($code)) { $error = $response["error_description"]; if ([string]::IsNullOrEmpty($error)){ $error = $response["error"]; } if (-not [string]::IsNullOrEmpty($error)){ throw $error; } } $body = @{ client_id = $ClientId grant_type = "authorization_code" code = $code redirect_uri= $auth_redirect } $form.Controls[0].Dispose() $contentType="application/x-www-form-urlencoded" Invoke-RestMethod -UseBasicParsing -Uri "https://$($loginAuthURL)/$Tenant/oauth2/token" -ContentType $contentType -Method POST -Body $body } } function Get-AuthRedirectUrl { Param([Parameter(Mandatory=$True)][String]$ClientId, [Parameter(Mandatory=$True)][String]$Resource) Process { # default $redirect_uri = "urn:ietf:wg:oauth:2.0:oob" if($ClientId -eq "1fec8e78-bce4-4aaf-ab1b-5451cc387264") # Teams { $redirect_uri = "https://$($loginAuthURL)/common/oauth2/nativeclient" } elseif($ClientId -eq "9bc3ab49-b65d-410a-85ad-de819febfddc") # SPO { $redirect_uri = "https://oauth.spops.microsoft.com/" } elseif($ClientId -eq "c44b4083-3bb0-49c1-b47d-974e53cbdf3c") # Azure admin interface { $redirect_uri = "https://portal.azure.com/signin/index/?feature.prefetchtokens=true&feature.showservicehealthalerts=true&feature.usemsallogin=true" } elseif($ClientId -eq "0000000c-0000-0000-c000-000000000000") # Azure AD Account { $redirect_uri = "https://account.activedirectory.windowsazure.com/" } elseif($ClientId -eq "19db86c3-b2b9-44cc-b339-36da233a3be2") # My sign-ins { $redirect_uri = "https://mysignins.microsoft.com" } elseif($ClientId -eq "29d9ed98-a469-4536-ade2-f981bc1d605e" -and $Resource -ne "https://enrollment.manage.microsoft.com/") # Azure AD Join { $redirect_uri = "ms-aadj-redir://auth/drs" } elseif($ClientId -eq "0c1307d4-29d6-4389-a11c-5cbe7f65d7fa") # Azure Android App { $redirect_uri = "https://azureapp" } elseif($ClientId -eq "33be1cef-03fb-444b-8fd3-08ca1b4d803f") # OneDrive Web { $redirect_uri = "https://admin.onedrive.com/" } elseif($ClientId -eq "ab9b8c07-8f02-4f72-87fa-80105867a763") # OneDrive native client { $redirect_uri = "https://login.windows.net/common/oauth2/nativeclient" } elseif($ClientId -eq "3d5cffa9-04da-4657-8cab-c7f074657cad") # MS Commerce { $redirect_uri = "http://localhost/m365/commerce" } elseif($ClientId -eq "4990cffe-04e8-4e8b-808a-1175604b879f") # MS Partner - this flow doesn't work as expected :( { $redirect_uri = "https://partner.microsoft.com/aad/authPostGateway" } elseif($ClientId -eq "fb78d390-0c51-40cd-8e17-fdbfab77341b") # Microsoft Exchange REST API Based Powershell { $redirect_uri = "https://$($loginAuthURL)/common/oauth2/nativeclient" } elseif($ClientId -eq "3b511579-5e00-46e1-a89e-a6f0870e2f5a") { $redirect_uri = "https://windows365.microsoft.com/signin-oidc" } elseif($ClientId -eq "ac560269-2c17-4bac-81e0-376644cf5f99") # Guardian { $redirect_uri = "https://$($loginAuthURL)/common/oauth2/nativeclient" } elseif($ClientId -eq "a0c73c16-a7e3-4564-9a95-2bdf47383716") # Microsoft Exchange Online Remote PowerShell { $redirect_uri = "https://$($loginAuthURL)/common/oauth2/nativeclient" } return $redirect_uri } } function Get-AccessToken { Param( [Parameter(Mandatory=$False)][System.Management.Automation.PSCredential]$Credentials, [Parameter(Mandatory=$True)][String]$Resource, [Parameter(Mandatory=$True)][String]$ClientId, [Parameter(Mandatory=$False)][String]$Tenant, [Parameter(Mandatory=$False)][bool]$ForceMFA=$false, [Parameter(Mandatory=$False)][string]$LoginHint ) if(-not $Credentials) { $OAuthInfo = Prompt-Credentials -Resource $Resource -ClientId $ClientId -Tenant $Tenant -ForceMFA $ForceMFA -LoginHint $LoginHint } else { $OAuthInfo = Get-OAuthInfo -Credentials $Credentials -ClientId $ClientId -Resource "https://graph.windows.net" } if([String]::IsNullOrEmpty($OAuthInfo)){ throw "Could not get OAuthInfo" } $RefreshToken = $OAuthInfo.refresh_token $ParsedToken = Read-Accesstoken $OAuthInfo.access_token $tenant_id = $ParsedToken.tid $access_token = Get-AccessTokenWithRefreshToken -Resource $Resource -ClientId $ClientId -TenantId $tenant_id -RefreshToken $RefreshToken if([string]::IsNullOrEmpty($access_token)) { Throw "Could not get Access Token!" } @{ AccessToken = $access_token; RefreshToken = $OAuthInfo.refresh_token } } function Get-AccessTokenWithRefreshToken { Param( [String]$Resource, [Parameter(Mandatory=$True)][String]$ClientId, [Parameter(Mandatory=$True)][String]$TenantId, [Parameter(Mandatory=$True)][String]$RefreshToken ) Process { $body = @{ "resource"= $Resource "client_id"= $ClientId "grant_type"= "refresh_token" "refresh_token"= $RefreshToken "scope"= "openid" } if($ClientId -eq "ab9b8c07-8f02-4f72-87fa-80105867a763") { $url = "https://login.windows.net/common/oauth2/token" # OneDrive Sync Engine } else { $url = "https://$($loginAuthURL)/$TenantId/oauth2/token" } $contentType="application/x-www-form-urlencoded" $response = Invoke-RestMethod -UseBasicParsing -Uri $url -ContentType $contentType -Method POST -Body $body return $response.access_token } } function Enable-BrowserEmulation { $regPath="HKCU:\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BROWSER_EMULATION" if(!(Test-Path -Path $regPath )){ $key = New-Item -ItemType directory -Path $regPath -Force } # Check the registry value for WebBrowser control emulation. Should be IE 11 $reg = Get-ItemProperty -Path $regPath if([String]::IsNullOrEmpty($reg.'powershell.exe') -or [String]::IsNullOrEmpty($reg.'powershell_ise.exe')) { Set-ItemProperty -Path $regPath -Name "powershell_ise.exe" -Value 0x00002af9 Set-ItemProperty -Path $regPath -Name "powershell.exe" -Value 0x00002af9 } } function Create-LoginForm { Param( [Parameter(Mandatory=$True)][String]$Url, [Parameter(Mandatory=$True)][String]$auth_redirect, [Parameter(Mandatory=$False)][String]$Headers) Process { Enable-BrowserEmulation Add-Type -AssemblyName System.Web Add-Type -AssemblyName System.Security Add-Type -AssemblyName System.Windows.Forms # Create the form and add a WebBrowser control to it $form = New-Object Windows.Forms.Form $form.Width = 560 $form.Height = 680 $form.FormBorderStyle=[System.Windows.Forms.FormBorderStyle]::FixedDialog $form.TopMost = $true $web = New-Object Windows.Forms.WebBrowser $web.Size = $form.ClientSize $web.Anchor = "Left,Top,Right,Bottom" $form.Controls.Add($web) $field = New-Object Windows.Forms.TextBox $field.Visible = $false $form.Controls.Add($field) $field.Text = $auth_redirect # Clear WebBrowser control cache Clear-WebBrowser # Add an event listener to track down where the browser is $web.add_Navigated({ # If the url matches the redirect url, close with OK. $curl=$_.Url.ToString() $auth_redirect = $form.Controls[1].Text Write-Debug "NAVIGATED TO: $($curl)" if($curl.StartsWith($auth_redirect)) { # Check whether the body has the Bearer if(![String]::IsNullOrEmpty($form.Controls[0].Document.GetElementsByTagName("script"))) { $script=$form.Controls[0].Document.GetElementsByTagName("script").outerhtml if($script.Contains('"oAuthToken":')){ $s=$script.IndexOf('"oAuthToken":')+13 $e=$script.IndexOf('}',$s)+1 $oAuthToken=$script.Substring($s,$e-$s) | ConvertFrom-Json $at=$oAuthToken.authHeader.Split(" ")[1] $rt=$oAuthToken.refreshToken $script:AccessToken = @{"access_token"=$at; "refresh_token"=$rt} Write-Debug "ACCESSTOKEN $script:accessToken" } elseif($curl.StartsWith("https://portal.azure.com")) { Write-Debug "WAITING FOR THE TOKEN!" # Do nothing, wait for it.. return } } # Add the url to the hidden field #$form.Controls[1].Text = $curl $form.DialogResult = "OK" $form.Close() Write-Debug "PROMPT CREDENTIALS URL: $curl" } # Automatically logs in -> need to logout first elseif($curl.StartsWith($url)) { # All others Write-Verbose "Returned to the starting url, someone already logged in?" } }) # Add an event listener to track down where the browser is going $web.add_Navigating({ $curl=$_.Url.ToString() Write-Verbose "NAVIGATING TO: $curl" # SharePoint login if($curl.EndsWith("/_forms/default.aspx")) { $_.Cancel=$True $form.DialogResult = "OK" $form.Close() } }) # Set the url if([String]::IsNullOrEmpty($Headers)){ $web.Navigate($url) } else { $web.Navigate($url,"",$null,$Headers) } $form } } # Clear the Forms.WebBrowser data $source=@" [DllImport("wininet.dll", SetLastError = true)] public static extern bool InternetSetOption(IntPtr hInternet, int dwOption, IntPtr lpBuffer, int lpdwBufferLength); [DllImport("wininet.dll", SetLastError = true)] public static extern bool InternetGetCookieEx(string pchURL, string pchCookieName, System.Text.StringBuilder pchCookieData, ref uint pcchCookieData, int dwFlags, IntPtr lpReserved); "@ #Create type from source $WebBrowser = Add-Type -memberDefinition $source -passthru -name WebBrowser -ErrorAction SilentlyContinue $INTERNET_OPTION_END_BROWSER_SESSION = 42 $INTERNET_OPTION_SUPPRESS_BEHAVIOR = 81 $INTERNET_COOKIE_HTTPONLY = 0x00002000 $INTERNET_SUPPRESS_COOKIE_PERSIST = 3 function Clear-WebBrowser { # Clear the cache [IntPtr] $optionPointer = [IntPtr]::Zero $s = [System.Runtime.InteropServices.Marshal]::SizeOf($INTERNET_OPTION_END_BROWSER_SESSION) $optionPointer = [System.Runtime.InteropServices.Marshal]::AllocCoTaskMem($s) [System.Runtime.InteropServices.Marshal]::WriteInt32($optionPointer, ([ref]$INTERNET_SUPPRESS_COOKIE_PERSIST).Value) $status = $WebBrowser::InternetSetOption([IntPtr]::Zero, $INTERNET_OPTION_SUPPRESS_BEHAVIOR, $optionPointer, $s) Write-Debug "Clearing Web browser cache. Status:$status" [System.Runtime.InteropServices.Marshal]::Release($optionPointer)|out-null # Clear the current session $status = $WebBrowser::InternetSetOption([IntPtr]::Zero, $INTERNET_OPTION_END_BROWSER_SESSION, [IntPtr]::Zero, 0) Write-Debug "Clearing Web browser. Status:$status" } function Get-WebBrowserCookies { Param([Parameter(Mandatory=$True)][String]$Url) $dataSize = 1024 $cookieData = [System.Text.StringBuilder]::new($dataSize) $status = $WebBrowser::InternetGetCookieEx($Url,$null,$cookieData, [ref]$dataSize, $INTERNET_COOKIE_HTTPONLY, [IntPtr]::Zero) Write-Debug "GETCOOKIEEX Status: $status, length: $($cookieData.Length)" if(!$status) { $LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error() Write-Debug "GETCOOKIEEX ERROR: $LastError" } if($cookieData.Length -gt 0) { $cookies = $cookieData.ToString() Write-Debug "Cookies for $url`: $cookies" Return $cookies } else { Write-Warning "Cookies not found for $url" } } $global:b = @() $global:result2=@() $global:collectAD = $False $global:collectOffice = $False function Main() { $workingMode = Read-Host "Select your environment: 1 - Active Directory only 2 - Azure AD / Microsoft 365 only 3 - Active Directory and Azure AD / Microsoft 365 Enter 1, 2 or 3" Write-Host "" if($workingMode -eq '1' -or $workingMode -eq '3'){ $collectAD = $True } if($workingMode -eq '2' -or $workingMode -eq '3'){ $global:collectOffice = $True } if($workingMode -ne '1' -and $workingMode -ne '2' -and $workingMode -ne '3') { Write-Error "Enter 1, 2 or 3 to select your environment." } $a = @() if($collectAD -eq $True){ if($Null -eq $ADCredential){ Write-Host "Provide credentials for your Active Directory domain or forest ..." $adPrams["Credential"] = (Get-Credential -Message "Specify Active Directory credentials") } } if($collectOffice -eq $True){ Write-Host "Provide credentials for your Azure AD / Microsoft 365 tenant ..." #Connect-MsolService @offPrams $O365Info = Get-OfficeLicenses -SupportedOnly:$CollectExchangeOnly -LoginHint $DefaultAccount Write-Host "Collecting information about Azure AD / Microsoft 365 tenant ..." $tenatName = '' $hybridCount = 0 if ($null -ne $O365Info -and $null -ne $O365Info.Licenses -and $O365Info.Licenses.Count -gt 0){ $tenatName = ($O365Info.Licenses)[0].Account + ".onmicrosoft.com" $O365Info.Licenses | %{ if($Null -eq $_){ return } $hybridCount += ($_.Consumed + $_.Warning) $item = New-Object PSObject -Property @{ System = $tenatName SKU = $_.Name Counter = ($_.Consumed + $_.Warning) } $global:b += $item } $item = New-Object PSObject -Property @{ Name = 'Exchange Online Licenses' System = $tenatName Counter = $hybridCount } $a += $item $global:result2 = ($b | select System,SKU,Counter) } } if($collectAD -eq $True){ try{ Write-Host "Collecting information about Active Directory domains ..." $totalCount = 0 $forest = Get-ADForest -Current LoggedOnUser @adPrams foreach($dom in $forest.Domains){ try{ $dcForDomain = Get-ADDomainController -DomainName $dom -Discover -Service ADWS,PrimaryDC -NextClosestSite -ForceDiscover if($Null -eq $dcForDomain -or $Null -eq $dcForDomain.HostName){ Write-Error "Error on getting DC: $dom" continue } Write-Host "Collecting information Domain: $dom DC: $($dcForDomain.HostName[0])" $domainAD = Get-ADDomain -Identity $dom -Server ($dcForDomain.HostName[0]) -ErrorAction Ignore @adPrams if($Null -ne $domainAD){ $usersCount = ([Int32](Get-ADUser -LDAPFilter '(&(objectCategory=person)(objectClass=user)(!userAccountControl:1.2.840.113556.1.4.803:=2))' -SearchBase $domainAD.DistinguishedName -Server ($dcForDomain.HostName[0]) @adPrams).Count) $item = New-Object PSObject -Property @{ Name = 'AD Enabled Users' System = $dom Counter = $usersCount } $totalCount += $usersCount $a += $item } }catch{ Write-Error "Domain: $dom | Error: $($Error[0])" } } if($totalCount -gt 0){ $item = New-Object PSObject -Property @{ Name = 'Total AD Enabled Users' System = 'All AD domains' Counter = $totalCount } $a += $item } }catch{ Write-Error "Error: $($Error[0])" } } Write-Host "" return $a } function MainRun{ $fileName = "$($env:USERPROFILE)\Downloads\" + "CayosoftLicenseCount_" + $(Get-Date -format "yyyyMMddHHmmss") + ".txt" Write-Host '============================================================================== Cayosoft License Counter version 2.0.3 Copyright Cayosoft Inc., 2025 Use this tool to count the number of licenses required by Cayosoft products to manage your Active Directory domains and/or Microsoft 365 tenant. If you have any questions, please contact Cayosoft at sales@cayosoft.com. ----------------------------------------------------------------------------- ' # TODO: pre-create filename and tell customer. # check with required level of privileges and error. $result = Main | select Name,System,Counter Write-Host "See results below and in the TXT file: " Write-Host $fileName Write-Host '-----------------------------------------------------------------------------' $result | FT if($collectOffice -eq $True){ $result2 | select System,SKU,Counter | FT } $result | out-file $fileName $result2 | out-file $fileName -Append } MainRun ####