O365 Report Script Enhancements - powershell

So, I've created myself a script for generating CSV files from an O365 tenant. It pulls various data sets such as users, groups etc. However when running this script on larger tenants with 10,000+ users it can take some time and I've been made aware I'm using a lot of Disk I/O. Does anyone have any suggestions on how to enhance the speed of this script?
# PS 7.2 Compatible
# Module Installations (required once)
Install-Module MicrosoftTeams
Install-Module ExchangeOnlineManagement
Install-Module Microsoft.Online.SharePoint.PowerShell
# Module Imports (required everytime a new terminal is ran)
Import-Module ExchangeOnlineManagement -UseWindowsPowerShell
Import-Module MicrosoftTeams
Import-Module Microsoft.Online.SharePoint.PowerShell -UseWindowsPowerShell
# Configurable Vars
$tenantName = "M365x51125269"
$OutputFolder = "C:\Users\admin\Documents\Scripts\O365 Reporting\Output"
# Connecting to Services
Connect-MgGraph -Scopes DeviceManagementApps.Read.All, DeviceManagementApps.ReadWrite.All, DeviceManagementManagedDevices.Read.All, DeviceManagementManagedDevices.ReadWrite.All, DeviceManagementServiceConfig.Read.All, DeviceManagementServiceConfig.ReadWrite.All, Directory.Read.All, Directory.ReadWrite.All, User.Read.All, User.ReadBasic.All, User.ReadWrite.All
Connect-ExchangeOnline
Connect-SPOService -Url https://$tenantName-admin.sharepoint.com
# Generate a list of all users with the given properties in a variable
$users = Get-MgUser -Property 'accountEnabled, userPrincipalName, displayName, givenName, surname, department,id' -All
$OneDriveUsage = Get-SPOSite -IncludePersonalSite $true -Limit all -Filter "Url -like '-my.sharepoint.com/personal/'"
$MailboxInfo = Get-EXOMailbox
# Iterate through the variable to pull individual details out
foreach ($user in $users) {
# License
$license = Get-MgUserLicenseDetail -UserId $user.Id
$CurrentOneDrive = $OneDriveUsage| Where-Object{$_.Owner -eq $user.UserPrincipalName}
$CurrentMailboxInfo = $MailboxInfo | Where-Object{$_.PrimarySmtpAddress -eq $user.UserPrincipalName}
$sortedArray = ($CurrentMailboxInfo.EmailAddresses | Select-String '(?<=smtp:)\S+(?!=\S)' -AllMatches).Matches.Value
$stringSMTP = $sortedArray -join ";"
[PSCustomObject]#{
'Enabled Credentials' = $user.AccountEnabled
'Unique ID' = $user.UserPrincipalName
'Display Name' = $user.DisplayName
'Assigned Products' = $license.SkuPartNumber -join " ; "
'First Name' = $user.GivenName
'Last Name' = $user.Surname
'Department' = $user.Department
'Proxy Addresses' = $stringSMTP
'OneDrive Storage Usage (MB)' = $CurrentOneDrive.StorageUsageCurrent
} | Export-Csv -path "$OutputFolder\UserInfo.csv" -Append -NoTypeInformation
}
# Generate list of all sites in a variable
$siteList = Get-SPOSite -Limit ALL
# Iterate through the variable to pull individual details out
foreach($site in $siteList){
# Checking the value result to determine a connection
if ($site.IsTeamsConnected){
$connectionWrite = 'TRUE'
}
else{
$connectionWrite = 'FALSE'
}
# Generate a custom object with our desired properties for export
[PSCustomObject]#{
'Unique ID' = $site.Url
'Display Name' = $site.Title
'Teams Connected' = $connectionWrite
'Sharepoint Storage (MB)' = $site.StorageUsageCurrent
'SharePoint Last Activity Date' = $site.LastContentModifiedDate
} | Export-Csv -path "$OutputFolder\Sites.csv" -Append -NoTypeInformation
}
$teamList = Get-Team
foreach($team in $teamlist){
$teamInfo = $team
$teamReport = Import-CSV "$InputFolder\TeamReport.csv"
$teamDetails = $teamReport | Where-Object{$_."Team Name" -eq $team.DisplayName}
$Sites = Get-SPOSite | Where-Object{$_.Title -eq $team.DisplayName}
[PSCustomObject]#{
'Display Name' = $teamInfo.DisplayName
'Teams Last Activity Date' = $teamDetails."Last Activity Date"
'Sharepoint Storage (MB)' = $Sites.StorageUsageCurrent
} | Export-Csv -path "$OutputFolder\TeamReport.csv" -Append -NoTypeInformation
}
$Groups = Get-MgGroup -Property "Members" -All
foreach($group in $Groups){
if ($group.SecurityEnabled -And -not $group.MailEnabled){
$members = Get-MgGroupMember -GroupId $group.Id
[PSCustomObject]#{
'Display Name' = $group.DisplayName
'Members' = $members.AdditionalProperties.displayName -join ";"
} | Export-Csv -path "$OutputFolder\SecurityGroups.csv" -Append -NoTypeInformation
}
continue
}
$Groups = Get-MgGroup -Property "Members" -All
foreach($group in $Groups){
if ($group.GroupTypes -eq 'Unified'){
$members = Get-MgGroupMember -GroupId $group.Id
[PSCustomObject]#{
'Display Name' = $group.DisplayName
'Members' = $members.AdditionalProperties.displayName -join ";"
} | Export-Csv -path "$OutputFolder\UnifiedGroups.csv" -Append -NoTypeInformation
}
continue
}
$Groups = Get-MgGroup -Property "Members" -All
foreach($group in $Groups){
if ( ( ($group.SecurityEnabled) -and ($group.MailEnabled) ) -and ('Unified' -notin $group.GroupTypes) ){
$members = Get-MgGroupMember -GroupId $group.Id
[PSCustomObject]#{
'Display Name' = $group.DisplayName
'Members' = $members.AdditionalProperties.displayName -join ";"
} | Export-Csv -path "$OutputFolder\MailEnabledSecurityGroups.csv" -Append -NoTypeInformation
}
continue
}
$Groups = Get-MgGroup -Property "Members" -All
foreach($group in $Groups){
if ( (($group.MailEnabled) -and (-not $group.SecurityEnabled)) -and ('Unified' -notin $group.GroupTypes))
{
$members = Get-MgGroupMember -GroupId $group.Id
[PSCustomObject]#{
'Display Name' = $group.DisplayName
'Members' = $members.AdditionalProperties.displayName -join ";"
} | Export-Csv -path "$OutputFolder\DistributionGroups.csv" -Append -NoTypeInformation
}
continue
}
$sharedmailList = Get-EXOMailbox
$filteredList = $sharedmailList | Where-Object {$_.RecipientTypeDetails -notcontains "UserMailbox" -and "DiscoveryMailbox"}
foreach($sharedmail in $filteredList){
if ($sharedmail.RecipientTypeDetails -eq 'DiscoveryMailBox'){
continue
}
else{
[PSCustomObject]#{
'Unique ID' = $sharedmail.PrimarySmtpAddress
'Display Name' = $sharedmail.DisplayName
} | Export-Csv -path "$OutputFolder\SharedMailbox.csv" -Append -NoTypeInformation
}
}
# Disconnect from services to avoid error before finalising script
Disconnect-Graph
Disconnect-ExchangeOnline
Disconnect-SPOService
People are welcome to take this for their own use by the way - I've found it useful for migrations when needing to merge multiple tenancies together but needing input from clients on what they wish to keep or exclude.

Related

Using a .txt file to find ManagedBy and ManagedBy Email within an AD Group

I'm having issues trying to have my script read and apply the AD groups with my script. Right now, it's just posting what's in my script, but I would like to have the script read what's in my .txt file and use it with the rest of my script.
$filePath = "C:\Users\UserName\Downloads\ADGroupList.txt"
Get-Content -Path $filePath
Get-ADGroup -filter {Name -like "$filePath" } -Properties managedBy |
ForEach-Object {
$managedBy = $_.managedBy;
if ($managedBy -ne $null)
{
$manager = (get-aduser -Identity $managedBy -Properties emailAddress);
$managerName = $manager.Name;
$managerEmail = $manager.emailAddress;
}
else
{
$managerName = 'N/A';
$managerEmail = 'N/A';
}
Write-Output $_; } |
Select-Object #{n='Group Name';e={$_.Name}}, #{n='Managed By Name';e={$managerName}}, #{n='Managed By Email';e={$managerEmail}}
Export-Csv -Path "C:\Users\UserName\Documents\ADGroupManagerList.csv"
The easiest way is to loop over the group names you have in the ADGroupList.txt file (assuming this is a list of group names, each on a separate line)
$filePath = "C:\Users\UserName\Downloads\ADGroupList.txt"
# just loop over the group names you have in the text file and capture the output
$result = Get-Content -Path $filePath | ForEach-Object {
$group = Get-ADGroup -Filter "Name -like '$_'" -Properties managedBy
# create an object pre filled in when no manager was found
$obj = [PsCustomObject]#{
'Group Name' = $group.Name
'Managed By Name' = 'N/A'
'Managed By Email' = 'N/A'
}
# test if the ManagedBy is populated
if (-not [string]::IsNullOrWhiteSpace($group.ManagedBy)) {
# try to use the DN in property ManagedBy to find the manager
try {
$manager = Get-ADUser -Identity $group.ManagedBy -Properties EmailAddress -ErrorAction Stop
$obj.'Managed By Name' = $manager.Name
$obj.'Managed By Email' = $manager.EmailAddress
}
catch {
Write-Warning "No user found for '$($group.ManagedBy)'.. Please check AD."
}
}
# output the object so it gets collected in variable $result
$obj
}
# write the file
$result | Export-Csv -Path "C:\Users\UserName\Documents\ADGroupManagerList.csv" -NoTypeInformation
For workaround you can use this powershell script to get the mangedBy of groups.
Get-ADGroup -filter * -Properties managedBy |
ForEach-Object {
$managedBy = $_.managedBy;
if ($managedBy -ne $null)
{
$manager = (get-aduser -Identity $managedBy -Properties emailAddress);
$managerName = $manager.Name;
$managerEmail = $manager.emailAddress;
}
else
{
$managerName = 'N/A';
$managerEmail = 'N/A';
}
Write-Output $_; } |
Select-Object #{n='Group Name';e={$_.Name}}, #{n='Managed By Name';e={$managerName}}, #{n='Managed By Email';e={$managerEmail}}

Powershell - cheking var is giving me the right results but Export-CSV gives numbers

what I want is that the $report_groups val gives me in Export-CSV the output that i get in Terminal. But i cant figure it why does he gives me Numbers.
$get_AD_Groups = Get-ADGroup -Filter '*' | Select-Object Name
$report_groups = New-Object -TypeName System.Collections.ArrayList
foreach ($item in $get_AD_Groups) {
$get_users = $item.Name | Get-ADGroupMember | Select-Object Name
$disabled_user = 0
foreach ($user in $get_users) {
$status = $user.Name | Get-ADUser -ErrorAction SilentlyContinue
if(($status.ObjectClass -eq 'user') -and ($status.Enabled -ne 'True')) {
$disabled_user++
}
}
if ($get_users.Count -eq $disabled_user) {
$report_groups.Add($item.Name)
}
}
$report_groups | Export-Csv -Path "..\report.csv" -NoTypeInformation -Force -Delimiter ";"
Now when i run $report_groups in Terminal i get the list of the AD Group BUT as soon i do the Export-CSV this is what i get:
So thanks again to Lee_Dailey for helping me on this.
Changes done.
$report_groups = #()
if ($get_users.Count -eq $disabled_user) {
$fill = [PSCustomObject]#{
AD_GROUP = $item.Name
}
$report_groups += $fill
}

invoke command on all those servers to see which admin accounts are active on each server

I have been fetching both direct members and groups inside local administrators group in our remote machines. I want to get an output like below.
Also , if there are GROUP members inside local admin group then I want to organize only GROUP members like below.
MACHINE01,User01,TRUE,GROUP01;GROUP02
output:
"Computername","Members"
"MACHINE01","contoso\User01 contoso\User02 contoso\GROUP01 contoso\GROUP02
desired output:
Computername,Direct Members,Account Status,Group Members
MACHINE01,User01,TRUE,GROUP01;GROUP02
MACHINE01,User02,FALSE
MACHINE02,User05,TRUE,GROUP04;GROUP05;GROUP12
MACHINE02,User08,FALSE
MACHINE02,User12,FALSE
MACHINE44,User07,TRUE
script :
$server_list = #()
Import-Csv C:\temp\server3.csv | ForEach-Object {$server_list += $_.name}
invoke-command {
$members = net localgroup administrators |
where {$_ -AND $_ -notmatch "command completed successfully"} |
select -skip 4
New-Object PSObject -Property #{
Computername = $env:COMPUTERNAME
Members=$members
}
} -computer $server_list -HideComputerName | Select * -ExcludeProperty RunspaceID, PSComputerName, PSShowComputerName | Export-CSV c:\temp\local_admins2.csv -NoTypeInformation
Get-ADComputer -Filter * -SearchBase "OU=Servers,DC=test,DC=local" | select
name | Export-Csv C:\data\servers3.csv
Output:
"ComputerName","Direct Members","Account Status","Group Members"
"machine01","user01","OK",""
"machine01","user02","Degraded",""
"machine02","user03","OK",""
"machine02","user04","Degraded",""
LASTLY UPDATE OUTPUT WMI:
ComputerName Direct Members Account Status Group Members
------------ -------------- -------------- -------------
**MACHINE01 Administrator Degraded Domain Admins;IT-Admins**
MACHINE01 Theo OK
MACHINE01 LocalAdmin OK
**MACHINE02 Administrator Degraded DBA Admins;Software-Admins**
MACHINE02 Theo OK
MACHINE02 LocalAdmin OK
net localgroup does not output anything that differentiates between users or groups, so if possible, use
Get-LocalGroupMember and Get-LocalUser:
$server_list = (Import-Csv -Path 'C:\temp\server3.csv').name
$result = Invoke-Command -ComputerName $server_list -ScriptBlock {
$members = Get-LocalGroupMember -Group "Administrators"
$groups = $members | Where-Object {$_.ObjectClass -eq 'Group'} | ForEach-Object {($_.Name -split '\\')[-1]}
$users = #($members | Where-Object {$_.ObjectClass -eq 'User'})
if ($users.Count) {
# now loop over the user objects
$users | ForEach-Object {
$name = ($_.Name -split '\\')[-1]
$user = if ($_.PrincipalSource -eq 'Local') {
Get-LocalUser -Name $name -ErrorAction SilentlyContinue
}
else {
Get-ADUser -Filter "SamAccountName -eq '$name'" -ErrorAction SilentlyContinue
}
# output an object
[PsCustomObject]#{
'ComputerName' = $env:COMPUTERNAME
'Direct Members' = $name
'Account Status' = $user.Enabled
'Group Members' = $groups -join ';'
}
# clear the $goups here because you only want to list them once per server
$groups = $null
}
}
else {
# no users, just groups
[PsCustomObject]#{
'ComputerName' = $env:COMPUTERNAME
'Direct Members' = $null
'Account Status' = $null
'Group Members' = $groups -join ';'
}
}
} | Select-Object * -ExcludeProperty PSComputerName, RunspaceId
$result | Export-Csv -Path 'c:\temp\local_admins2.csv' -NoTypeInformation
If module Microsoft.Powershell.LocalAccounts is not available to you, you can experiment with module localaccount, but I have no experience with that..
I don't have that old OSes, so you'll have to test this.
Below uses WMI to query the servers for the group memberships:
$server_list = (Import-Csv -Path 'C:\temp\server3.csv').name
$result = foreach ($server in $server_list) {
$query = "Associators of {Win32_Group.Domain='$server',Name='Administrators'} where Role=GroupComponent"
$members = Get-WmiObject -Query $query -ComputerName $server |
Where-Object { $_.__CLASS -match '(User|Group)' } |
Select-Object Name, Caption,
#{Name = 'ObjectClass'; Expression = {$matches[1]}},
#{Name = 'ComputerName'; Expression = {$_.__SERVER}},
Status, LocalAccount, SID, Domain
$groups = #($members | Where-Object {$_.ObjectClass -eq 'Group'})
$users = #($members | Where-Object {$_.ObjectClass -eq 'User'})
if ($users.Count) {
# now loop over the user objects
$users | ForEach-Object {
# output an object
[PsCustomObject]#{
'ComputerName' = $_.ComputerName
'Direct Members' = $_.Name
'Account Status' = $_.Status
'Group Members' = ($groups.Name | Sort-Object -Unique) -join ';'
}
# clear the $groups here because you only want to list them once per server
$groups = $null
}
}
elseif ($groups.Count) {
# no users, just groups
[PsCustomObject]#{
'ComputerName' = $groups[0].ComputerName
'Direct Members' = $null
'Account Status' = $null
'Group Members' = ($groups.Name | Sort-Object -Unique) -join ';'
}
}
}
$result | Export-Csv -Path 'c:\temp\local_admins2.csv' -NoTypeInformation
Another alternbative is to use [ADSI]:
$server_list = (Import-Csv -Path 'C:\temp\server3.csv').name
$group = 'Administrators'
$members = foreach ($server in $server_list) {
try {
([ADSI]"WinNT://$server/$group,group").psbase.Invoke('Members') | ForEach-Object {
# test if local or domain
$ADSPath = $_.GetType().InvokeMember("ADSPath", 'GetProperty', $null, $_, $null)
$local = ($ADSPath -like 'WinNT://*')
# get the object name
$name = $_.GetType().InvokeMember('Name', 'GetProperty', $null, $_, $null)
# get object class
$class = $_.GetType().InvokeMember('Class', 'GetProperty', $null, $_, $null)
if ($class -eq 'User') {
if ($local) {
$flag = $_.GetType().InvokeMember("userflags", 'GetProperty', $null, $_, $null)
$enabled = !($flag -band 2) # ADS_UF_ACCOUNTDISABLE
}
else {
$enabled = (Get-ADUser -Filter "SamAccountName -eq '$name'" -ErrorAction SilentlyContinue).Enabled
}
}
else { $enabled = $null }
[PSCustomObject] #{
ComputerName = $server.ToUpper()
Group = $group
Name = $name
ObjectClass = $class
Enabled = $enabled
}
}
}
catch {
Write-Warning $_
}
}
$groups = #($members | Where-Object {$_.ObjectClass -eq 'Group'})
$users = #($members | Where-Object {$_.ObjectClass -eq 'User'})
$result = if ($users.Count) {
# now loop over the user objects
$users | ForEach-Object {
# output an object
[PsCustomObject]#{
'ComputerName' = $_.ComputerName
'Direct Members' = $_.Name
'Account Status' = $_.Enabled
'Group Members' = ($groups.Name | Sort-Object -Unique) -join ';'
}
# clear the $groups here because you only want to list them once per server
$groups = $null
}
}
elseif ($groups.Count) {
# no users, just groups
[PsCustomObject]#{
'ComputerName' = $_.ComputerName
'Direct Members' = $null
'Account Status' = $null
'Group Members' = ($groups.Name | Sort-Object -Unique) -join ';'
}
}
$result | Export-Csv -Path 'c:\temp\local_admins2.csv' -NoTypeInformation
Output from the above when testing
Result using Get-LocalGroupMember:
ComputerName Direct Members Account Status Group Members
------------ -------------- -------------- -------------
MACHINE01 Administrator False Domain Admins;IT-Admins
MACHINE01 LocalAdmin True
MACHINE01 Theo True
Result using WMI:
ComputerName Direct Members Account Status Group Members
------------ -------------- -------------- -------------
MACHINE01 Administrator Degraded Domain Admins;IT-Admins
MACHINE01 Theo OK
MACHINE01 LocalAdmin OK
Result using ADSI:
ComputerName Direct Members Account Status Group Members
------------ -------------- -------------- -------------
MACHINE01 Administrator False Domain Admins;IT-Admins
MACHINE01 Theo True
MACHINE01 LocalAdmin True

Format of the Export-Csv in Powershell - unexpected column order

I'm trying to export an array of objects to a .csv file, what do I need to do to put everything in the right column with header of a property name.
I've tried selecting properties by select to pipeline
$groups = Get-MsolGroup -All
$results = #()
foreach ($group in $groups) {
$props = #{
'DisplayName' = $group.DisplayName
'GroupType' = $group.GroupType
'Email' = $group.EmailAddress
'MemberCount' = #(Get-MsolGroupMember -GroupObjectId $group.ObjectId).Count
}
New-Object -Type PSObject -Prop $props
$results += $props
}
$results | Export-Csv -Path C:\Users\Tako\Desktop\results.csv -NoTypeInformation
You can use your code like below:
$groups = Get-MsolGroup -All
$output = $groups | ForEach-Object{
[PSCustomObject]#{
"DisplayName" = $_.DisplayName
"GroupType" = $_.GroupType
"Email" = $_.EmailAddress
"MemberCount" = #(Get-MsolGroupMember -GroupObjectId $_.ObjectId).Count
}
}
$output | Export-Csv -Path "F:\temp\1\myresults.csv" -NoTypeInformation
Here is a similar test, just test with Get-AzStorageAccount cmdlet:

Modifying PowerShell script to display RecipientDetailsType and FullAccess permission

I need to modify the below PowerShell script to show two more column called RecipientDetailsType and FullAccess.
The below PowerShell code is already working, but I need to get two more additional columns.
$DataPath = "C:\TEMP\Delegates-Results.csv"
$Results = #()
$MailboxUsers = Get-Mailbox -ResultSize Unlimited -Database CAB-DB02
foreach($user in $mailboxusers) {
$UPN = $user.UserPrincipalName
$MbxStats = Get-MailboxStatistics $UPN
$UserNotes = Get-User $UPN
$delegates = #(Get-MailboxPermission -Identity $UPN |
Where-Object { ($_.AccessRights -like "*FullAccess*") -and
(-not $_.IsInherited) -and
($_.User.toString() -ne "NT AUTHORITY\SELF") -and
($_.User.toString() -notlike '*Discovery Management*') } |
Select-Object #{Name='Delegate'; Expression={(Get-Recipient $_.User.toString()).DisplayName}},
#{Name='AccessRights';Expression={$_.AccessRights -join ', '}})
$Properties = #{
Name = $user.name
PrimarySmtpAddress = $user.PrimarySmtpAddress
RecipientTypeDetails = $user.RecipientTypeDetails
FullAccess = $user.FullAccess
UPN = $UPN
Alias = $user.alias
OU = $user.organizationalunit
Server = $MbxStats.servername
Database = $MbxStats.databasename
TotaItemSize = [math]::Round(($MbxStats.TotalItemSize.ToString().Split("(")[1].Split(" ")[0].Replace(",","")/1MB),2)
Delegates = $delegates.Delegate -join ', '
Notes = $UserNotes.Notes
}
$Results += New-Object psobject -Property $properties
}
$Results | Sort-Object -Property TotaItemSize | Select-Object Name,UPN,Alias,RecipientTypeDetails,FullAccess,OU,Server,Database,TotaItemSize,Notes,Delegates | Export-Csv -notypeinformation -Path $DataPath
Have you tried modifying $Properties to:
$Properties = #{
Name = $user.name
PrimarySmtpAddress = $user.PrimarySmtpAddress
RecipientDetailsType = $user.RecepientDetailsType
FullAccess = $user.FullAccess
UPN = $UPN
Alias = $user.alias
OU = $user.organizationalunit
Server = $MbxStats.servername
Database = $MbxStats.databasename
TotaItemSize = [math]::Round(($MbxStats.TotalItemSize.ToString().Split("(")[1].Split(" ")[0].Replace(",","")/1MB),2)
Notes = $UserNotes.Notes
}
$Results | Sort-Object -Property TotaItemSize | Select-Object Name,UPN,Alias,RecepientDetailsType,FullAccess,OU,Server,Database,TotaItemSize,Notes | Export-Csv -notypeinformation -Path $DataPath
If that doesn't work, let me know and I'll try it in our Exchange environment.