Documenting with PowerShell: O365 Groups (And Warranty updates)

This one was request by a pal of mine that I know via a MSP discord; He wanted a way to document the users in O365 groups and see what type of group it was in one go. To do this, we’re leveraging the Graph API. As in most of my blogs I have a IT-Glue version and a version in HTML for these that use different documentation systems.

For the script you’ll also need the Secure App Model ready, if you haven’t set that up yet, check out the blog and come back to this one 🙂

ITGlue version

The IT-Glue version creates a new Flexible Asset for you, and uploads each groups into their own Flexible Asset. It creates a table of the members and owners, but also tags them. That way you can easily browse to a contact and see what groups they are in.

################### Secure Application Model Information ###################
$ApplicationId = 'YourApplicationID'
$ApplicationSecret = 'YourApplicationSecret'
$RefreshToken = 'YourVeryLongRefreshToken'
################# /Secure Application Model Information ####################
  
################# IT-Glue Information ######################################
$ITGkey = "YourITGAPIKEY"
$ITGbaseURI = "https://api.eu.itglue.com"
$FlexAssetName = "Teams - Autodoc"
$Description = "Teams information automatically retrieved."
$TableStyling = "<th>", "<th style=`"background-color:#4CAF50`">"
################# /IT-Glue Information #####################################
  
  
  
write-host "Checking if IT-Glue Module is available, and if not install it." -ForegroundColor Green
#Settings IT-Glue logon information
If (Get-Module -ListAvailable -Name "ITGlueAPI") { 
    Import-module ITGlueAPI 
}
Else { 
    Install-Module ITGlueAPI -Force
    Import-Module ITGlueAPI
}

#Setting IT-Glue logon information
Add-ITGlueBaseURI -base_uri $ITGbaseURI
Add-ITGlueAPIKey $ITGkey
   
write-host "Getting IT-Glue contact list" -ForegroundColor Green
$i = 0
$AllITGlueContacts = do {
    $Contacts = (Get-ITGlueContacts -page_size 1000 -page_number $i).data.attributes
    $i++
    $Contacts
    Write-Host "Retrieved $($Contacts.count) Contacts" -ForegroundColor Yellow
}while ($Contacts.count % 1000 -eq 0 -and $Contacts.count -ne 0) 
   
write-host "Generating unique ID List" -ForegroundColor Green
 
$DomainList = foreach ($Contact in $AllITGlueContacts) {
    $ITGDomain = ($contact.'contact-emails'.value -split "@")[1]
    [PSCustomObject]@{
        Domain   = $ITGDomain
        OrgID    = $Contact.'organization-id'
        Combined = "$($ITGDomain)$($Contact.'organization-id')"
    }
} 
$DomainList = $DomainList | sort-object -Property Combined -Unique
   
write-host "Checking if Flexible Asset exists in IT-Glue." -foregroundColor green
$FilterID = (Get-ITGlueFlexibleAssetTypes -filter_name $FlexAssetName).data
if (!$FilterID) { 
    write-host "Does not exist, creating new." -foregroundColor green
    $NewFlexAssetData = 
    @{
        type          = 'flexible-asset-types'
        attributes    = @{
            name        = $FlexAssetName
            icon        = 'sitemap'
            description = $description
        }
        relationships = @{
            "flexible-asset-fields" = @{
                data = @(
                    @{
                        type       = "flexible_asset_fields"
                        attributes = @{
                            order           = 1
                            name            = "Group Name"
                            kind            = "Text"
                            required        = $true
                            "show-in-list"  = $true
                            "use-for-title" = $true
                        }
                    },
                    @{
                        type       = "flexible_asset_fields"
                        attributes = @{
                            order           = 2
                            name            = "Group Settings"
                            kind            = "Textbox"
                            required        = $true
                            "show-in-list"  = $false
                            "use-for-title" = $false
                        }
                    },
                    @{
                        type       = "flexible_asset_fields"
                        attributes = @{
                            order          = 3
                            name           = "Group Members"
                            kind           = "Textbox"
                            required       = $false
                            "show-in-list" = $false
                        }
                    },
                    @{
                        type       = "flexible_asset_fields"
                        attributes = @{
                            order          = 4
                            name           = "Group Owners"
                            kind           = "Textbox"
                            required       = $false
                            "show-in-list" = $false
                        }
                    },
                    @{
                        type       = "flexible_asset_fields"
                        attributes = @{
                            order          = 5
                            name           = "Tagged Members"
                            kind           = "Tag"
                            "tag-type"     = "Contacts"
                            required       = $false
                            "show-in-list" = $false
                        }
                    },
                    @{
                        type       = "flexible_asset_fields"
                        attributes = @{
                            order          = 6
                            name           = "Tagged Owners"
                            kind           = "Tag"
                            "tag-type"     = "Contacts"
                            required       = $false
                            "show-in-list" = $false
                        }
                    }
                    
                )
            }
        }
    }
    New-ITGlueFlexibleAssetTypes -Data $NewFlexAssetData
    $FilterID = (Get-ITGlueFlexibleAssetTypes -filter_name $FlexAssetName).data
}
   
  
  
write-host "Creating credentials and tokens." -ForegroundColor Green
  
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, ($ApplicationSecret | Convertto-SecureString -AsPlainText -Force))
$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.windows.net/.default' -ServicePrincipal
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal
  
write-host "Creating body to request Graph access for each client." -ForegroundColor Green
$body = @{
    'resource'      = 'https://graph.microsoft.com'
    'client_id'     = $ApplicationId
    'client_secret' = $ApplicationSecret
    'grant_type'    = "client_credentials"
    'scope'         = "openid"
}
  
write-host "Connecting to Office365 to get all tenants." -ForegroundColor Green
Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
$customers = Get-MsolPartnerContract -All
foreach ($Customer in $Customers) {
    write-host "Grabbing domains for client $($Customer.name)." -ForegroundColor Green
    $CustomerDomains = Get-MsolDomain -TenantId $Customer.TenantId
    write-host "Finding possible organisation IDs" -ForegroundColor Green
    $orgid = foreach ($customerDomain in $customerdomains) {
        ($domainList | Where-Object { $_.domain -eq $customerDomain.name }).'OrgID' | Select-Object -Unique
    }
    write-host "Documenting in the following organizations." -ForegroundColor Green
    $ClientToken = Invoke-RestMethod -Method post -Uri "https://login.microsoftonline.com/$($customer.tenantid)/oauth2/token" -Body $body -ErrorAction Stop
    $headers = @{ "Authorization" = "Bearer $($ClientToken.access_token)" }
    write-host "Starting documentation process for $($customer.name)." -ForegroundColor Green
    Write-Host "Grabbing all Teams" -ForegroundColor Green
    $groups = (Invoke-RestMethod -Uri "https://graph.microsoft.com/beta/groups?&`$top=999&quot; -Headers $Headers -Method Get -ContentType "application/json").value
    Write-Host "Grabbing all Team Settings" -ForegroundColor Green
    foreach ($group in $Groups) {
        $Owners = (Invoke-RestMethod -Uri "https://graph.microsoft.com/beta/groups/$($group.id)/owners" -Headers $Headers -Method Get -ContentType "application/json").value
        $Members = (Invoke-RestMethod -Uri "https://graph.microsoft.com/beta/groups/$($group.id)/members" -Headers $Headers -Method Get -ContentType "application/json").value
        $TaggedMembers = foreach ($Member in $Members) {
            $email = $member.mail
            #Tagging devices
            if ($email) {
                #Write-Host "Finding all related contacts - Based on email: $email"
                (Get-ITGlueContacts -page_size "1000" -filter_primary_email $email).data
                start-sleep -miliseconds 110
            }
        }

        $TaggedOwners = foreach ($Owner in $Owners) {
            $email = $owner.mail
            #Tagging devices
            if ($email) {
             #   Write-Host "Finding all related contacts - Based on email: $email"
                (Get-ITGlueContacts -page_size "1000" -filter_primary_email $email).data
                start-sleep -miliseconds 110
            }
        }
        $GroupSettings = ($group | Select-Object @{Label = "Created on"; Expression = { $_.createdDateTime } },
            @{Label = "Created by Application"; Expression = { if (!$_.createdByAppId) { $false } else { $_.createdByAppId } } },
            @{Label = "Description"; Expression = { $_.description } },
            @{Label = "Mail Enabled"; Expression = { if ($_.Mailenabled) { "Yes" } else { "No " } } },
            @{Label = "Security Enabled"; Expression = { if ($_.SecurityEnabled) { "Yes" } else { "No " } } },
            @{Label = "Mail Nickname"; Expression = { $_.MailNickname } },
            @{Label = "E-Mail addresses"; Expression = { $_.Proxyaddress -join "," } } | convertto-html -Fragment | out-string) -replace $TableStyling

        $FlexAssetBody =
        @{
            type       = 'flexible-assets'
            attributes = @{
                traits = @{
                    'group-name'     = $group.displayName
                    'group-settings' = $GroupSettings
                    'group-members'  = ($members | Select-Object Displayname, Userprincipalname | convertto-html -Fragment | out-string) -replace $TableStyling
                    "group-owners"   = ($owners | Select-Object Displayname, Userprincipalname | convertto-html -Fragment | out-string) -replace $TableStyling
                    "tagged-owners"  = $TaggedOwners.id
                    'tagged-members' = $TaggedMembers.id
                }
            }
        }
  
        write-host "   Uploading $($group.displayName) into IT-Glue" -foregroundColor green
        foreach ($org in $orgID | Select-Object -Unique) {
            $ExistingFlexAsset = (Get-ITGlueFlexibleAssets -filter_flexible_asset_type_id $FilterID.id -filter_organization_id $org).data | Where-Object { $_.attributes.traits.'group-name' -eq $group.displayName }
            #If the Asset does not exist, we edit the body to be in the form of a new asset, if not, we just upload.
            if (!$ExistingFlexAsset) {
                if ($FlexAssetBody.attributes.'organization-id') {
                    $FlexAssetBody.attributes.'organization-id' = $org
                }
                else { 
                    $FlexAssetBody.attributes.add('organization-id', $org)
                    $FlexAssetBody.attributes.add('flexible-asset-type-id', $FilterID.id)
                }
                write-output "                      Creating new Team: $($Settings.displayName) into IT-Glue organisation $org"
                New-ITGlueFlexibleAssets -data $FlexAssetBody
      
            }
            else {
                write-output "                      Updating Team: $($Settings.displayName)into IT-Glue organisation $org"
                $ExistingFlexAsset = $ExistingFlexAsset | select-object -Last 1
                Set-ITGlueFlexibleAssets -id $ExistingFlexAsset.id  -data $FlexAssetBody
            }
      
        }
    }
}

Generic HTML version

The HTML version uses PsWriteHTML to make a nice overview for you. It doesn’t have the tagging options but it’s still pretty nifty.

################### Secure Application Model Information ###################
$ApplicationId = 'YourApplicationID'
$ApplicationSecret = 'YourApplicationSecret'
$RefreshToken = 'YourVeryLongRefreshToken'
################# /Secure Application Model Information ####################
write-host "Creating credentials and tokens." -ForegroundColor Green
  
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, ($ApplicationSecret | Convertto-SecureString -AsPlainText -Force))
$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.windows.net/.default' -ServicePrincipal
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal
  
write-host "Creating body to request Graph access for each client." -ForegroundColor Green
$body = @{
    'resource'      = 'https://graph.microsoft.com'
    'client_id'     = $ApplicationId
    'client_secret' = $ApplicationSecret
    'grant_type'    = "client_credentials"
    'scope'         = "openid"
}
  
write-host "Connecting to Office365 to get all tenants." -ForegroundColor Green
Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
$customers = Get-MsolPartnerContract -All
foreach ($Customer in $Customers) {
    $ClientToken = Invoke-RestMethod -Method post -Uri "https://login.microsoftonline.com/$($customer.tenantid)/oauth2/token" -Body $body -ErrorAction Stop
    $headers = @{ "Authorization" = "Bearer $($ClientToken.access_token)" }
    write-host "Starting documentation process for $($customer.name)." -ForegroundColor Green
    Write-Host "Grabbing all Teams" -ForegroundColor Green
    $groups = (Invoke-RestMethod -Uri "https://graph.microsoft.com/beta/groups?&`$top=999&quot; -Headers $Headers -Method Get -ContentType "application/json").value
    Write-Host "Grabbing all Team Settings" -ForegroundColor Green
    foreach ($group in $Groups) {
        $Owners = (Invoke-RestMethod -Uri "https://graph.microsoft.com/beta/groups/$($group.id)/owners" -Headers $Headers -Method Get -ContentType "application/json").value
        $Members = (Invoke-RestMethod -Uri "https://graph.microsoft.com/beta/groups/$($group.id)/members" -Headers $Headers -Method Get -ContentType "application/json").value

        $GroupSettings = ($group | Select-Object @{Label = "Created on"; Expression = { $_.createdDateTime } },
            @{Label = "Created by Application"; Expression = { if (!$_.createdByAppId) { $false } else { $_.createdByAppId } } },
            @{Label = "Description"; Expression = { $_.description } },
            @{Label = "Mail Enabled"; Expression = { if ($_.Mailenabled) { "Yes" } else { "No " } } },
            @{Label = "Security Enabled"; Expression = { if ($_.SecurityEnabled) { "Yes" } else { "No " } } },
            @{Label = "Mail Nickname"; Expression = { $_.MailNickname } },
            @{Label = "E-Mail addresses"; Expression = { $_.Proxyaddress -join "," } } | convertto-html -Fragment | out-string) -replace $TableStyling

     
        New-HTML {
            New-HTMLTab -Name "365 Groups: $($group.displayName)" {
                New-HTMLSection -Invisible {
                    New-HTMLSection -HeaderText 'Settings' {
                        New-HTMLTable -DataTable $GroupSettings
                    }
                }
                New-HTMLSection -Invisible {
                    New-HTMLSection -HeaderText "Members" {
                        New-HTMLTable -DataTable ($members | Select-Object Displayname, Userprincipalname)
                    }
         
                    New-HTMLSection -HeaderText "Owners" { 
                        New-HTMLTable -DataTable ($owners | Select-Object Displayname, Userprincipalname)
                    }
                }
            }
             
        } -FilePath "C:\temp\$($customer.DefaultDomainName).html" -Online
    }
}

but that’s not all! I have some other cool news that’s not really worth a blog of its own, but I’d still like to let you know.

Warranty Script updates

So a while back I created this quickly wacked together script to lookup warranties with PowerShell and create nice looking reports. The main reason for this is that there is a vendor that is showing very shady business practices around Warranty lookups and pricing them.

After a lot of requests of adding functionality or manufacturers I’ve decided to improve the script, turn it into a module and publish it to the world for easier usage. It still needs some love; especially on the help-file side and I’m hoping other community members will contribute to make this the best warranty options available. You can find the project on my Github here.

Feel free to contribute! I’d love to have other people working with me to make sure that these warranty lookup tools price themselves out of the market 😉

And that’s all! as always, Happy PowerShelling.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.