diff --git a/HaveIBeenPwnedPasswordCheck/README.md b/HaveIBeenPwnedPasswordCheck/README.md new file mode 100644 index 0000000..6f699b8 --- /dev/null +++ b/HaveIBeenPwnedPasswordCheck/README.md @@ -0,0 +1,114 @@ +[Source](https://gcits.com/knowledge-base/check-it-glue-passwords-against-have-i-been-pwned-breaches/ "Permalink to Check IT Glue passwords against Have I Been Pwned breaches") + +# Check IT Glue passwords against Have I Been Pwned breaches + +![Use PowerShell to check IT Glue Passwords against Have I Been Pwned Breaches][1] + +Hackers will often use password spray attacks to gain access to accounts. These attacks work by trying a commonly used password against many accounts. + +If you're using the IT Glue documentation system, you can use this script to determine how secure and common the passwords in your customer environment are by checking for their presence in known data breaches. + +It works by retrieving your IT Glue Password list via the IT Glue API and run each password through the Have I Been Pwned, Pwned Password API. If a match is detected, its details will be exported to a CSV along with the how many times the password has been detected in a breach. + +## How to check your customers' passwords against Have I Been Pwned data breaches. + +#### Retrieve your IT Glue Api Key with password access + +1. Sign into IT Glue as an Administrator +2. Navigate to Account, Settings, Api Keys + ![Log Into IT Glue Settings][2] +3. Under Custom API Keys, Generate a New Key, give it a sample name and tick the Password Access box + ![ Create IT Glue Password Access API Key][3] +4. Treat this key very carefully as it can be used to access all passwords in your ITGlue environment. I recommend disabling password access and revoking the key once you have run the script. + +#### How to run the script to detect customer passwords in known HIBP data breaches + +1. Double click the below script to select it. +2. Copy and Paste it into **Visual Studio Code** +3. Save it with a **.ps1** extension +4. Install the recommended PowerShell extension in Visual Studio Code if you haven't already +5. Copy and paste the API key you created earlier into the **$key** variable in the PowerShell script. +6. If you are in the EU, you may need to update the **$baseURI** value to "https://api.eu.itglue.com" +7. Press **F5** to run the script. + +![IT Glue Passwords Detected In Breaches][4] + +8. A report of all found passwords will be exported to a CSV at **C:temppwnedpasswords.csv**. While this CSV does not contain the passwords, it does contain the usernames and other potentially sensitive information. +9. The FoundCount column in the CSV is the number of times the password has been found in a HIBP reported breach. + ![Pwned Password CSV][5] + +You can use this CSV to assist with resetting passwords and improving the security of your customers' environments. + +## Script to check IT Glue passwords against have I Been Pwned data breaches + +```powershell + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + + $key = "ENTERAPIKEYHERE" + $ITGbaseURI = "https://api.itglue.com" + + $headers = @{ + "x-api-key" = $key + } + + Function Get-StringHash([String] $String, $HashName = "MD5") { + $StringBuilder = New-Object System.Text.StringBuilder + [System.Security.Cryptography.HashAlgorithm]::Create($HashName).ComputeHash([System.Text.Encoding]::UTF8.GetBytes($String))| % { + [Void]$StringBuilder.Append($_.ToString("x2")) + } + $StringBuilder.ToString() + } + + function Get-ITGlueItem($Resource) { + $array = @() + + $body = Invoke-RestMethod -Method get -Uri "$ITGbaseUri/$Resource" -Headers $headers -ContentType application/vnd.api+json + $array += $body.data + Write-Host "Retrieved $($array.Count) items" + + if ($body.links.next) { + do { + $body = Invoke-RestMethod -Method get -Uri $body.links.next -Headers $headers -ContentType application/vnd.api+json + $array += $body.data + Write-Host "Retrieved $($array.Count) items" + } while ($body.links.next) + } + return $array + } + + $passwords = Get-ITGlueItem -Resource passwords + + foreach($password in $passwords){ + $details = Get-ITGlueItem -Resource passwords/$($password.id) + $hash = Get-StringHash -String $details.attributes.password -HashName SHA1 + $first5 = $hash.Substring(0,5) + $remaining = $hash.Substring(5) + $result = Invoke-Restmethod -Uri "https://api.pwnedpasswords.com/range/$first5" + $result = $result -split "`n" + $match = $result | Where-Object {$_ -match $remaining} + if($match){ + $FoundCount = ($match -split ":")[1] + Write-Host $FoundCount -ForegroundColor Red + Write-Host "Found $($details.attributes.'organization-name') - $($details.attributes.name)`n" -ForegroundColor Yellow + $password.attributes | Add-Member FoundCount $FoundCount -Force + $password.attributes | export-csv C:temppwnedpasswords.csv -NoTypeInformation -Append + } + } +``` + +### About The Author + +![Elliot Munro][6] + +#### [ Elliot Munro ][7] + +Elliot Munro is an Office 365 MCSA from the Gold Coast, Australia supporting hundreds of small businesses with GCITS. If you have an Office 365 or Azure issue that you'd like us to take a look at (or have a request for a useful script) send Elliot an email at [elliot@gcits.com][8] + +[1]: https://gcits.com/wp-content/uploads/PowerShellITGluePasswords-1030x436.png +[2]: https://gcits.com/wp-content/uploads/LogIntoITGlueSettings.png +[3]: https://gcits.com/wp-content/uploads/CreatePasswordAccessAPIKey.png +[4]: https://gcits.com/wp-content/uploads/ITGluePasswordsDetectedInBreaches.png +[5]: https://gcits.com/wp-content/uploads/PwnedPasswordCSV.png +[6]: https://gcits.com/wp-content/uploads/AAEAAQAAAAAAAA2QAAAAJDNlN2NmM2Y4LTU5YWYtNGRiNC1hMmI2LTBhMzdhZDVmNWUzNA-80x80.jpg +[7]: https://gcits.com/author/elliotmunro/ +[8]: mailto:elliot%40gcits.com diff --git a/HaveIBeenPwnedPasswordCheck/single.ps1 b/HaveIBeenPwnedPasswordCheck/single.ps1 new file mode 100644 index 0000000..4aa9aeb --- /dev/null +++ b/HaveIBeenPwnedPasswordCheck/single.ps1 @@ -0,0 +1,52 @@ +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +$key = "ENTERAPIKEYHERE" +$ITGbaseURI = "https://api.itglue.com" + +$headers = @{ + "x-api-key" = $key +} + +Function Get-StringHash([String] $String, $HashName = "MD5") { + $StringBuilder = New-Object System.Text.StringBuilder + [System.Security.Cryptography.HashAlgorithm]::Create($HashName).ComputeHash([System.Text.Encoding]::UTF8.GetBytes($String))| % { + [Void]$StringBuilder.Append($_.ToString("x2")) + } + $StringBuilder.ToString() +} + +function Get-ITGlueItem($Resource) { + $array = @() + + $body = Invoke-RestMethod -Method get -Uri "$ITGbaseUri/$Resource" -Headers $headers -ContentType application/vnd.api+json + $array += $body.data + Write-Host "Retrieved $($array.Count) items" + + if ($body.links.next) { + do { + $body = Invoke-RestMethod -Method get -Uri $body.links.next -Headers $headers -ContentType application/vnd.api+json + $array += $body.data + Write-Host "Retrieved $($array.Count) items" + } while ($body.links.next) + } + return $array +} + +$passwords = Get-ITGlueItem -Resource passwords + +foreach ($password in $passwords) { + $details = Get-ITGlueItem -Resource passwords/$($password.id) + $hash = Get-StringHash -String $details.attributes.password -HashName SHA1 + $first5 = $hash.Substring(0, 5) + $remaining = $hash.Substring(5) + $result = Invoke-Restmethod -Uri "https://api.pwnedpasswords.com/range/$first5" + $result = $result -split "`n" + $match = $result | Where-Object {$_ -match $remaining} + if ($match) { + $FoundCount = ($match -split ":")[1] + Write-Host $FoundCount -ForegroundColor Red + Write-Host "Found $($details.attributes.'organization-name') - $($details.attributes.name)`n" -ForegroundColor Yellow + $password.attributes | Add-Member FoundCount $FoundCount -Force + $password.attributes | export-csv C:\temp\pwnedpasswords.csv -NoTypeInformation -Append + } +} \ No newline at end of file diff --git a/Office365Sync/README.md b/Office365Sync/README.md new file mode 100644 index 0000000..9660852 --- /dev/null +++ b/Office365Sync/README.md @@ -0,0 +1,495 @@ +[Source](https://gcits.com/knowledge-base/sync-office-365-tenant-info-itglue/ "Permalink to Sync Office 365 tenant info with IT Glue") + +# Sync Office 365 tenant info with IT Glue + +![Sync Office 365 tenant info with IT Glue][1] + +Here's a script that will import information about all of your Office 365 customer tenants, then associate that info with the relevant IT Glue organisations based on the domain names of the contact's emails. In short, if a contact in IT Glue has an email address with a domain that matches a verified domain in one of your Office 365 customer tenants, then the important info about that tenant will be associated with that contact's organisation in IT Glue + +![Office 365 Customer Information In IT Glue][2] + +IT Glue has been very handy for us to securely and easily store customer documentation. We're syncing most of our clients and configurations with ConnectWise Manage and Automate already, but we still found we were heading into the Microsoft Partner portal and PowerShell a fair bit for the same pieces of info. + +Hopefully this script will make it easier for you to see your important Office 365 tenant data in one place, associated with the relevant organisation. Once you've tested it out, you can set it to run as an Azure Function on a schedule. Feel free to customise and expand upon it – In our case, we've added in a dynamic link to our Azure Function hosted offboard script, so we can offboard users from the IT Glue portal. + +## How to sync Office 365 tenant info with IT Glue + +First we need to create a flexible asset type to hold the Office 365 tenant data. In this case, we're storing information about users, domains and licenses. + +1. Go to **Account**, **Flexible Asset Types**, then click **+New** +2. Create an asset type called **Office 365![][3]** +3. Create the following fields of the following kinds: + +- Tenant Name – Text +- Tenant ID – Text +- Initial Domain – Text +- Verified Domains – Textbox +- Licenses – Textbox +- Licensed Users – Textbox + +4. Make sure the names match exactly as above and click **Save** +5. Open the new Office 365 flexible asset type again and retrieve the ID from the URL, then save it somewhere. If you're looking to automate this process, this ID and other properties can be retrieved from the API![Get IT Glue Flexible Asset Type ID][4] +6. Click **Settings**, then **API Keys** +7. Create a new custom API key and give it any name you like. Copy it and save it![Create Custom API Key in IT Glue][5] +8. Go back to **Settings**, **General** and click **Customize Sidebar![Customize Sidebar IT Glue][6]** +9. Drag your Office 365 asset type onto the sidebar and click **Save**![Edit Sidebar In IT Glue][7] +10. Copy and paste the powershell script below into Visual Studio Code, then save it as a **.ps1** file. +11. Update the **key** variable with your API Key +12. Note that European customers may have a different base URI for the API. For these customers, the $baseURI value is: + + $baseURI = "https://api.eu.itglue.com" + +13. Update the **assetTypeID** variable with the ID of the Office 365 asset type – don't include any quotes here.![Add IT Glue API and AssettypeID][8] +14. Run the script by pressing **F5** +15. Enter your Office 365 delegated admin credentials and wait for it to complete. Note that this script can work with MFA on the delegated admin account, however if you're going to be running it as an Azure Function with MFA you'll need to use and store an App Password +16. Once it's completed, you should have Office 365 tenant info associated with your IT Glue organisations![Office 365 Tenant Info In IT Glue][9] +17. The licensed user table will provide email and license info for your customer's licensed users. See the Offboard User button as an example of the customisation you can perform on this script.![Office 365 Licensed User Table In IT Glue][10] + +### PowerShell script to sync Office 365 tenant info with IT Glue + +```powershell + $key = "ENTERITGLUEAPIKEYHERE" + $assettypeID = 9999 + $baseURI = "https://api.itglue.com" + $headers = @{ + "x-api-key" = $key + } + + $credential = Get-Credential + Connect-MsolService -Credential $credential + + function GetAllITGItems($Resource) { + $array = @() + + $body = Invoke-RestMethod -Method get -Uri "$baseUri/$Resource" -Headers $headers -ContentType application/vnd.api+json + $array += $body.data + Write-Host "Retrieved $($array.Count) items" + + if ($body.links.next) { + do { + $body = Invoke-RestMethod -Method get -Uri $body.links.next -Headers $headers -ContentType application/vnd.api+json + $array += $body.data + Write-Host "Retrieved $($array.Count) items" + } while ($body.links.next) + } + return $array + } + + function CreateITGItem ($resource, $body) { + $item = Invoke-RestMethod -Method POST -ContentType application/vnd.api+json -Uri $baseURI/$resource -Body $body -Headers $headers + return $item + } + + function UpdateITGItem ($resource, $existingItem, $newBody) { + $updatedItem = Invoke-RestMethod -Method Patch -Uri "$baseUri/$Resource/$($existingItem.id)" -Headers $headers -ContentType application/vnd.api+json -Body $newBody + return $updatedItem + } + + function Build365TenantAsset ($tenantInfo) { + + $body = @{ + data = @{ + type = "flexible-assets" + attributes = @{ + "organization-id" = $tenantInfo.OrganizationID + "flexible-asset-type-id" = $assettypeID + traits = @{ + "tenant-name" = $tenantInfo.TenantName + "tenant-id" = $tenantInfo.TenantID + "initial-domain" = $tenantInfo.InitialDomain + "verified-domains" = $tenantInfo.Domains + "licenses" = $tenantInfo.Licenses + "licensed-users" = $tenantInfo.LicensedUsers + } + } + } + } + + $tenantAsset = $body | ConvertTo-Json -Depth 10 + return $tenantAsset + } + + + + $customers = Get-MsolPartnerContract -All + + $365domains = @() + + foreach ($customer in $customers) { + Write-Host "Getting domains for $($customer.name)" -ForegroundColor Green + $companyInfo = Get-MsolCompanyInformation -TenantId $customer.TenantId + + $customerDomains = Get-MsolDomain -TenantId $customer.TenantId | Where-Object {$_.status -contains "Verified"} + $initialDomain = $customerDomains | Where-Object {$_.isInitial} + $Licenses = $null + $licenseTable = $null + $Licenses = Get-MsolAccountSku -TenantId $customer.TenantId + if ($licenses) { + $licenseTableTop = " +``` + +# License Name Active Consumed Unused + +```powershell +" $licenseTableBottom = " +" +$licensesColl = @() +foreach ($license in $licenses) { +$licenseString = "$($license.SkuPartNumber)$($license.ActiveUnits) active$($license.ConsumedUnits) consumed$($license.ActiveUnits - $license.ConsumedUnits) unused" +$licensesColl += $licenseString +} +if ($licensesColl) { +$licenseString = $licensesColl -join "" +} +$licenseTable = "{0}{1}{2}" -f $licenseTableTop, $licenseString, $licenseTableBottom +} +$licensedUserTable = $null +$licensedUsers = $null +$licensedUsers = get-msoluser -TenantId $customer.TenantId -All | Where-Object {$\_.islicensed} | Sort-Object UserPrincipalName +if ($licensedUsers) { +$licensedUsersTableTop = " +``` + +# Display Name Addresses Assigned Licenses + +```powershell +" $licensedUsersTableBottom = " +" +$licensedUserColl = @() +foreach ($user in $licensedUsers) { + +$aliases = (($user.ProxyAddresses | Where-Object {$_ -cnotmatch "SMTP" -and $_ -notmatch ".onmicrosoft.com"}) -replace "SMTP:", " ") -join " +" +$licensedUserString = "$($user.DisplayName)$($user.UserPrincipalName) +$aliases$(($user.Licenses.accountsku.skupartnumber) -join " +")" +$licensedUserColl += $licensedUserString +} +if ($licensedUserColl) { +$licensedUserString = $licensedUserColl -join "" +} +$licensedUserTable = "{0}{1}{2}" -f $licensedUsersTableTop, $licensedUserString, $licensedUsersTableBottom + +} + +$hash = [ordered]@{ +TenantName = $companyInfo.displayname +PartnerTenantName = $customer.name +Domains = $customerDomains.name +TenantId = $customer.TenantId +InitialDomain = $initialDomain.name +Licenses = $licenseTable +LicensedUsers = $licensedUserTable +} +$object = New-Object psobject -Property $hash +$365domains += $object + +} + +# Get all organisations + +#$orgs = GetAllITGItems -Resource organizations + +# Get all Contacts + +$itgcontacts = GetAllITGItems -Resource contacts + +$itgEmailRecords = @() +foreach ($contact in $itgcontacts) { +foreach ($email in $contact.attributes."contact-emails") { +$hash = @{ +Domain = ($email.value -split "@")[1] +OrganizationID = $contact.attributes.'organization-id' +} +$object = New-Object psobject -Property $hash +$itgEmailRecords += $object +} +} + +$allMatches = @() +foreach ($365tenant in $365domains) { +foreach ($domain in $365tenant.Domains) { +$itgContactMatches = $itgEmailRecords | Where-Object {$\_.domain -contains $domain} +foreach ($match in $itgContactMatches) { +$hash = [ordered]@{ +Key = "$($365tenant.TenantId)-$($match.OrganizationID)" +TenantName = $365tenant.TenantName +Domains = ($365tenant.domains -join ", ") +TenantId = $365tenant.TenantId +InitialDomain = $365tenant.InitialDomain +OrganizationID = $match.OrganizationID +Licenses = $365tenant.Licenses +LicensedUsers = $365tenant.LicensedUsers +} +$object = New-Object psobject -Property $hash +$allMatches += $object +} +} +} + +$uniqueMatches = $allMatches | Sort-Object key -Unique + +foreach ($match in $uniqueMatches) { +$existingAssets = @() +$existingAssets += GetAllITGItems -Resource "flexible*assets?filter[organization_id]=$($match.OrganizationID)&filter[flexible_asset_type_id]=$assetTypeID" +$matchingAsset = $existingAssets | Where-Object {$*.attributes.traits.'tenant-id' -contains $match.TenantId} + +if ($matchingAsset) { +Write-Host "Updating Office 365 tenant for $($match.tenantName)" +$UpdatedBody = Build365TenantAsset -tenantInfo $match +$updatedItem = UpdateITGItem -resource flexible_assets -existingItem $matchingAsset -newBody $UpdatedBody +} +else { +Write-Host "Creating Office 365 tenant for $($match.tenantName)" +$newBody = Build365TenantAsset -tenantInfo $match +$newItem = CreateITGItem -resource flexible_assets -body $newBody +} +} +``` + +## How to sync Office 365 customer info and IT Glue using an Azure Function + +[Follow this guide][11] to create a Timer Triggered Azure Function that connects to Office 365. + +- Call it something descriptive like **Sync365TenantsWithITGlue** +- Set it to run on a schedule, eg once a day at 9GMT time: **0 0 9 \* \* \*** +- Remember to upload the Office 365 Azure AD v1 **MSOnline** module via FTP, and encrypt your admin credentials. + +Here is the complete script to run this code as an Azure Function: + +### PowerShell script for Timer Triggered Azure Function to sync Office 365 tenants with IT Glue + +```powershell + Write-Output "PowerShell Timer trigger function executed at:$(get-date)"; + + $FunctionName = 'Sync365TenantsWithITGlue' + $ModuleName = 'MSOnline' + $ModuleVersion = '1.1.166.0' + $username = $Env:user + $pw = $Env:password + #import PS module + $PSModulePath = "D:homesitewwwroot$FunctionNamebin$ModuleName$ModuleVersion$ModuleName.psd1" + $key = "ITGLUEAPIKEYGOESHERE" + $assetTypeID = 99999 + $baseURI = "https://api.itglue.com" + $headers = @{ + "x-api-key" = $key + } + + Import-module $PSModulePath + + # Build Credentials + $keypath = "D:homesitewwwroot$FunctionNamebinkeysPassEncryptKey.key" + $secpassword = $pw | ConvertTo-SecureString -Key (Get-Content $keypath) + $credential = New-Object System.Management.Automation.PSCredential ($username, $secpassword) + + # Connect to MSOnline + + Connect-MsolService -Credential $credential + + function GetAllITGItems($Resource) { + $array = @() + + $body = Invoke-RestMethod -Method get -Uri "$baseUri/$Resource" -Headers $headers -ContentType application/vnd.api+json + $array += $body.data + Write-Output "Retrieved $($array.Count) items" + + if ($body.links.next) { + do { + $body = Invoke-RestMethod -Method get -Uri $body.links.next -Headers $headers -ContentType application/vnd.api+json + $array += $body.data + Write-Output "Retrieved $($array.Count) items" + } while ($body.links.next) + } + return $array + } + + function CreateITGItem ($resource, $body) { + $item = Invoke-RestMethod -Method POST -ContentType application/vnd.api+json -Uri $baseURI/$resource -Body $body -Headers $headers + return $item + } + + function UpdateITGItem ($resource, $existingItem, $newBody) { + $updatedItem = Invoke-RestMethod -Method Patch -Uri "$baseUri/$Resource/$($existingItem.id)" -Headers $headers -ContentType application/vnd.api+json -Body $newBody + return $updatedItem + } + + function Build365TenantAsset ($tenantInfo) { + + $body = @{ + data = @{ + type = "flexible-assets" + attributes = @{ + "organization-id" = $tenantInfo.OrganizationID + "flexible-asset-type-id" = $assettypeID + traits = @{ + "tenant-name" = $tenantInfo.TenantName + "tenant-id" = $tenantInfo.TenantID + "initial-domain" = $tenantInfo.InitialDomain + "verified-domains" = $tenantInfo.Domains + "licenses" = $tenantInfo.Licenses + "licensed-users" = $tenantInfo.LicensedUsers + } + } + } + } + + $tenantAsset = $body | ConvertTo-Json -Depth 10 + return $tenantAsset + } + + + + $customers = Get-MsolPartnerContract -All + + $365domains = @() + + foreach ($customer in $customers) { + Write-Output "Getting domains for $($customer.name)" + $companyInfo = Get-MsolCompanyInformation -TenantId $customer.TenantId + + $customerDomains = Get-MsolDomain -TenantId $customer.TenantId | Where-Object {$_.status -contains "Verified"} + $initialDomain = $customerDomains | Where-Object {$_.isInitial} + $Licenses = $null + $licenseTable + $Licenses = Get-MsolAccountSku -TenantId $customer.TenantId + if ($Licenses) { + $licenseTableTop = " +``` + +License Name Active Consumed Unused + +```powershell +$licenseTableBottom = "" + +$licensesColl = @() +foreach ($license in $licenses) { +$licenseString = "$($license.SkuPartNumber)$($license.ActiveUnits) active$($license.ConsumedUnits) consumed$($license.ActiveUnits - $license.ConsumedUnits) unused" +$licensesColl += $licenseString +} +if ($licensesColl) { +$licenseString = $licensesColl -join "" +} +$licenseTable = "{0}{1}{2}" -f $licenseTableTop, $licenseString, $licenseTableBottom +} + +$licensedUsers = $null +$licensedUserTable = $null +$licensedUsers = get-msoluser -TenantId $customer.TenantId -All | Where-Object {$\_.islicensed} | Sort-Object UserPrincipalName +if ($licensedUsers) { +$licensedUsersTableTop = " +Display Name Addresses Assigned Licenses +" $licensedUsersTableBottom = " +" +$licensedUserColl = @() +foreach ($user in $licensedUsers) { + +$aliases = (($user.ProxyAddresses | Where-Object {$_ -cnotmatch "SMTP" -and $_ -notmatch ".onmicrosoft.com"}) -replace "SMTP:", " ") -join " +" +$licensedUserString = "$($user.DisplayName)$($user.UserPrincipalName) +$aliases$(($user.Licenses.accountsku.skupartnumber) -join " +")" +$licensedUserColl += $licensedUserString +} +if ($licensedUserColl) { +$licensedUserString = $licensedUserColl -join "" +} +$licensedUserTable = "{0}{1}{2}" -f $licensedUsersTableTop, $licensedUserString, $licensedUsersTableBottom + +} + +$hash = [ordered]@{ +TenantName = $companyInfo.displayname +PartnerTenantName = $customer.name +Domains = $customerDomains.name +TenantId = $customer.TenantId +InitialDomain = $initialDomain.name +Licenses = $licenseTable +LicensedUsers = $licensedUserTable +} +$object = New-Object psobject -Property $hash +$365domains += $object + +} +``` + +# Get all Contacts + +```powershell +$itgcontacts = GetAllITGItems -Resource contacts + +$itgEmailRecords = @() +foreach ($contact in $itgcontacts) { +foreach ($email in $contact.attributes."contact-emails") { +$hash = @{ +Domain = ($email.value -split "@")[1] +OrganizationID = $contact.attributes.'organization-id' +} +$object = New-Object psobject -Property $hash +$itgEmailRecords += $object +} +} + +$allMatches = @() +foreach ($365tenant in $365domains) { +foreach ($domain in $365tenant.Domains) { +$itgContactMatches = $itgEmailRecords | Where-Object {$\_.domain -contains $domain} +foreach ($match in $itgContactMatches) { +$hash = [ordered]@{ +Key = "$($365tenant.TenantId)-$($match.OrganizationID)" +TenantName = $365tenant.TenantName +Domains = ($365tenant.domains -join ", ") +TenantId = $365tenant.TenantId +InitialDomain = $365tenant.InitialDomain +OrganizationID = $match.OrganizationID +Licenses = $365tenant.Licenses +LicensedUsers = $365tenant.LicensedUsers +} +$object = New-Object psobject -Property $hash +$allMatches += $object +} +} +} + +$uniqueMatches = $allMatches | Sort-Object key -Unique + +foreach ($match in $uniqueMatches) { +$existingAssets = @() +$existingAssets += GetAllITGItems -Resource "flexible*assets?filter[organization_id]=$($match.OrganizationID)&filter[flexible_asset_type_id]=$assetTypeID" +$matchingAsset = $existingAssets | Where-Object {$*.attributes.traits.'tenant-id' -contains $match.TenantId} + +if ($matchingAsset) { +Write-Output "Updating Office 365 tenant for $($match.tenantName)" +$UpdatedBody = Build365TenantAsset -tenantInfo $match +$updatedItem = UpdateITGItem -resource flexible_assets -existingItem $matchingAsset -newBody $UpdatedBody +} +else { +Write-Output "Creating Office 365 tenant for $($match.tenantName)" +$newBody = Build365TenantAsset -tenantInfo $match +$newItem = CreateITGItem -resource flexible_assets -body $newBody +} +} +``` + +### About The Author + +![Elliot Munro][12] + +#### [ Elliot Munro ][13] + +Elliot Munro is an Office 365 MCSA from the Gold Coast, Australia supporting hundreds of small businesses with GCITS. If you have an Office 365 or Azure issue that you'd like us to take a look at (or have a request for a useful script) send Elliot an email at [elliot@gcits.com][14] + +[1]: https://gcits.com/wp-content/uploads/SyncOffice365ITGlue-1030x436.png +[2]: https://gcits.com/wp-content/uploads/Office365CustomerInformationInITGlue-1030x628.png +[3]: https://gcits.com/wp-content/uploads/CreateOffice365FlexibleAssetTypeITGlue.png +[4]: https://gcits.com/wp-content/uploads/GetITGlueFlexibleAssetTypeID.png +[5]: https://gcits.com/wp-content/uploads/CreateCustomAPIKeyITGlue-1030x204.png +[6]: https://gcits.com/wp-content/uploads/CustomizeSidebarITGlue.png +[7]: https://gcits.com/wp-content/uploads/EditSideBarInItGlue-1030x712.png +[8]: https://gcits.com/wp-content/uploads/AddITGlueAPIandAssettypeID.png +[9]: https://gcits.com/wp-content/uploads/Office365TenantInfoInITGlue-1030x587.png +[10]: https://gcits.com/wp-content/uploads/Office365LicensedUserTableInITGlue-1030x487.png +[11]: https://gcits.com/knowledge-base/connect-azure-function-office-365/ +[12]: https://gcits.com/wp-content/uploads/AAEAAQAAAAAAAA2QAAAAJDNlN2NmM2Y4LTU5YWYtNGRiNC1hMmI2LTBhMzdhZDVmNWUzNA-80x80.jpg +[13]: https://gcits.com/author/elliotmunro/ +[14]: mailto:elliot%40gcits.com diff --git a/Office365Sync/auzreFunction.ps1 b/Office365Sync/auzreFunction.ps1 new file mode 100644 index 0000000..5b9d5f8 --- /dev/null +++ b/Office365Sync/auzreFunction.ps1 @@ -0,0 +1,197 @@ +Write-Output "PowerShell Timer trigger function executed at:$(get-date)"; + +$FunctionName = 'Sync365TenantsWithITGlue' +$ModuleName = 'MSOnline' +$ModuleVersion = '1.1.166.0' +$username = $Env:user +$pw = $Env:password +#import PS module +$PSModulePath = "D:\home\site\wwwroot\$FunctionName\bin\$ModuleName\$ModuleVersion\$ModuleName.psd1" +$key = "ITGLUEAPIKEYGOESHERE" +$assetTypeID = 99999 +$baseURI = "https://api.itglue.com" +$headers = @{ + "x-api-key" = $key +} + +Import-module $PSModulePath + +# Build Credentials +$keypath = "D:\home\site\wwwroot\$FunctionName\bin\keys\PassEncryptKey.key" +$secpassword = $pw | ConvertTo-SecureString -Key (Get-Content $keypath) +$credential = New-Object System.Management.Automation.PSCredential ($username, $secpassword) + +# Connect to MSOnline + +Connect-MsolService -Credential $credential + +function GetAllITGItems($Resource) { + $array = @() + + $body = Invoke-RestMethod -Method get -Uri "$baseUri/$Resource" -Headers $headers -ContentType application/vnd.api+json + $array += $body.data + Write-Output "Retrieved $($array.Count) items" + + if ($body.links.next) { + do { + $body = Invoke-RestMethod -Method get -Uri $body.links.next -Headers $headers -ContentType application/vnd.api+json + $array += $body.data + Write-Output "Retrieved $($array.Count) items" + } while ($body.links.next) + } + return $array +} + +function CreateITGItem ($resource, $body) { + $item = Invoke-RestMethod -Method POST -ContentType application/vnd.api+json -Uri $baseURI/$resource -Body $body -Headers $headers + return $item +} + +function UpdateITGItem ($resource, $existingItem, $newBody) { + $updatedItem = Invoke-RestMethod -Method Patch -Uri "$baseUri/$Resource/$($existingItem.id)" -Headers $headers -ContentType application/vnd.api+json -Body $newBody + return $updatedItem +} + +function Build365TenantAsset ($tenantInfo) { + + $body = @{ + data = @{ + type = "flexible-assets" + attributes = @{ + "organization-id" = $tenantInfo.OrganizationID + "flexible-asset-type-id" = $assettypeID + traits = @{ + "tenant-name" = $tenantInfo.TenantName + "tenant-id" = $tenantInfo.TenantID + "initial-domain" = $tenantInfo.InitialDomain + "verified-domains" = $tenantInfo.Domains + "licenses" = $tenantInfo.Licenses + "licensed-users" = $tenantInfo.LicensedUsers + } + } + } + } + + $tenantAsset = $body | ConvertTo-Json -Depth 10 + return $tenantAsset +} + + + +$customers = Get-MsolPartnerContract -All + +$365domains = @() + +foreach ($customer in $customers) { + Write-Output "Getting domains for $($customer.name)" + $companyInfo = Get-MsolCompanyInformation -TenantId $customer.TenantId + + $customerDomains = Get-MsolDomain -TenantId $customer.TenantId | Where-Object {$_.status -contains "Verified"} + $initialDomain = $customerDomains | Where-Object {$_.isInitial} + $Licenses = $null + $licenseTable + $Licenses = Get-MsolAccountSku -TenantId $customer.TenantId + if ($Licenses) { + $licenseTableTop = "
License NameActiveConsumedUnused
" + $licenseTableBottom = "
" + $licensesColl = @() + foreach ($license in $licenses) { + $licenseString = "$($license.SkuPartNumber)$($license.ActiveUnits) active$($license.ConsumedUnits) consumed$($license.ActiveUnits - $license.ConsumedUnits) unused" + $licensesColl += $licenseString + } + if ($licensesColl) { + $licenseString = $licensesColl -join "" + } + $licenseTable = "{0}{1}{2}" -f $licenseTableTop, $licenseString, $licenseTableBottom + } + + $licensedUsers = $null + $licensedUserTable = $null + $licensedUsers = get-msoluser -TenantId $customer.TenantId -All | Where-Object {$_.islicensed} | Sort-Object UserPrincipalName + if ($licensedUsers) { + $licensedUsersTableTop = "
Display NameAddressesAssigned Licenses
" + $licensedUsersTableBottom = "
" + $licensedUserColl = @() + foreach ($user in $licensedUsers) { + + $aliases = (($user.ProxyAddresses | Where-Object {$_ -cnotmatch "SMTP" -and $_ -notmatch ".onmicrosoft.com"}) -replace "SMTP:", " ") -join "
" + $licensedUserString = "$($user.DisplayName)$($user.UserPrincipalName)
$aliases$(($user.Licenses.accountsku.skupartnumber) -join "
")" + $licensedUserColl += $licensedUserString + } + if ($licensedUserColl) { + $licensedUserString = $licensedUserColl -join "" + } + $licensedUserTable = "{0}{1}{2}" -f $licensedUsersTableTop, $licensedUserString, $licensedUsersTableBottom + + + } + + + $hash = [ordered]@{ + TenantName = $companyInfo.displayname + PartnerTenantName = $customer.name + Domains = $customerDomains.name + TenantId = $customer.TenantId + InitialDomain = $initialDomain.name + Licenses = $licenseTable + LicensedUsers = $licensedUserTable + } + $object = New-Object psobject -Property $hash + $365domains += $object + +} + +# Get all Contacts +$itgcontacts = GetAllITGItems -Resource contacts + +$itgEmailRecords = @() +foreach ($contact in $itgcontacts) { + foreach ($email in $contact.attributes."contact-emails") { + $hash = @{ + Domain = ($email.value -split "@")[1] + OrganizationID = $contact.attributes.'organization-id' + } + $object = New-Object psobject -Property $hash + $itgEmailRecords += $object + } +} + +$allMatches = @() +foreach ($365tenant in $365domains) { + foreach ($domain in $365tenant.Domains) { + $itgContactMatches = $itgEmailRecords | Where-Object {$_.domain -contains $domain} + foreach ($match in $itgContactMatches) { + $hash = [ordered]@{ + Key = "$($365tenant.TenantId)-$($match.OrganizationID)" + TenantName = $365tenant.TenantName + Domains = ($365tenant.domains -join ", ") + TenantId = $365tenant.TenantId + InitialDomain = $365tenant.InitialDomain + OrganizationID = $match.OrganizationID + Licenses = $365tenant.Licenses + LicensedUsers = $365tenant.LicensedUsers + } + $object = New-Object psobject -Property $hash + $allMatches += $object + } + } +} + +$uniqueMatches = $allMatches | Sort-Object key -Unique + +foreach ($match in $uniqueMatches) { + $existingAssets = @() + $existingAssets += GetAllITGItems -Resource "flexible_assets?filter[organization_id]=$($match.OrganizationID)&filter[flexible_asset_type_id]=$assetTypeID" + $matchingAsset = $existingAssets | Where-Object {$_.attributes.traits.'tenant-id' -contains $match.TenantId} + + if ($matchingAsset) { + Write-Output "Updating Office 365 tenant for $($match.tenantName)" + $UpdatedBody = Build365TenantAsset -tenantInfo $match + $updatedItem = UpdateITGItem -resource flexible_assets -existingItem $matchingAsset -newBody $UpdatedBody + } + else { + Write-Output "Creating Office 365 tenant for $($match.tenantName)" + $newBody = Build365TenantAsset -tenantInfo $match + $newItem = CreateITGItem -resource flexible_assets -body $newBody + } +} \ No newline at end of file diff --git a/Office365Sync/single.ps1 b/Office365Sync/single.ps1 new file mode 100644 index 0000000..d912a5b --- /dev/null +++ b/Office365Sync/single.ps1 @@ -0,0 +1,182 @@ +$key = "ENTERITGLUEAPIKEYHERE" +$assettypeID = 9999 +$baseURI = "https://api.itglue.com" +$headers = @{ + "x-api-key" = $key +} + +$credential = Get-Credential +Connect-MsolService -Credential $credential + +function GetAllITGItems($Resource) { + $array = @() + + $body = Invoke-RestMethod -Method get -Uri "$baseUri/$Resource" -Headers $headers -ContentType application/vnd.api+json + $array += $body.data + Write-Host "Retrieved $($array.Count) items" + + if ($body.links.next) { + do { + $body = Invoke-RestMethod -Method get -Uri $body.links.next -Headers $headers -ContentType application/vnd.api+json + $array += $body.data + Write-Host "Retrieved $($array.Count) items" + } while ($body.links.next) + } + return $array +} + +function CreateITGItem ($resource, $body) { + $item = Invoke-RestMethod -Method POST -ContentType application/vnd.api+json -Uri $baseURI/$resource -Body $body -Headers $headers + return $item +} + +function UpdateITGItem ($resource, $existingItem, $newBody) { + $updatedItem = Invoke-RestMethod -Method Patch -Uri "$baseUri/$Resource/$($existingItem.id)" -Headers $headers -ContentType application/vnd.api+json -Body $newBody + return $updatedItem +} + +function Build365TenantAsset ($tenantInfo) { + + $body = @{ + data = @{ + type = "flexible-assets" + attributes = @{ + "organization-id" = $tenantInfo.OrganizationID + "flexible-asset-type-id" = $assettypeID + traits = @{ + "tenant-name" = $tenantInfo.TenantName + "tenant-id" = $tenantInfo.TenantID + "initial-domain" = $tenantInfo.InitialDomain + "verified-domains" = $tenantInfo.Domains + "licenses" = $tenantInfo.Licenses + "licensed-users" = $tenantInfo.LicensedUsers + } + } + } + } + + $tenantAsset = $body | ConvertTo-Json -Depth 10 + return $tenantAsset +} + + + +$customers = Get-MsolPartnerContract -All + +$365domains = @() + +foreach ($customer in $customers) { + Write-Host "Getting domains for $($customer.name)" -ForegroundColor Green + $companyInfo = Get-MsolCompanyInformation -TenantId $customer.TenantId + + $customerDomains = Get-MsolDomain -TenantId $customer.TenantId | Where-Object {$_.status -contains "Verified"} + $initialDomain = $customerDomains | Where-Object {$_.isInitial} + $Licenses = $null + $licenseTable = $null + $Licenses = Get-MsolAccountSku -TenantId $customer.TenantId + if ($licenses) { + $licenseTableTop = "
License NameActiveConsumedUnused
" + $licenseTableBottom = "
" + $licensesColl = @() + foreach ($license in $licenses) { + $licenseString = "$($license.SkuPartNumber)$($license.ActiveUnits) active$($license.ConsumedUnits) consumed$($license.ActiveUnits - $license.ConsumedUnits) unused" + $licensesColl += $licenseString + } + if ($licensesColl) { + $licenseString = $licensesColl -join "" + } + $licenseTable = "{0}{1}{2}" -f $licenseTableTop, $licenseString, $licenseTableBottom + } + $licensedUserTable = $null + $licensedUsers = $null + $licensedUsers = get-msoluser -TenantId $customer.TenantId -All | Where-Object {$_.islicensed} | Sort-Object UserPrincipalName + if ($licensedUsers) { + $licensedUsersTableTop = "
Display NameAddressesAssigned Licenses
" + $licensedUsersTableBottom = "
" + $licensedUserColl = @() + foreach ($user in $licensedUsers) { + + $aliases = (($user.ProxyAddresses | Where-Object {$_ -cnotmatch "SMTP" -and $_ -notmatch ".onmicrosoft.com"}) -replace "SMTP:", " ") -join "
" + $licensedUserString = "$($user.DisplayName)$($user.UserPrincipalName)
$aliases$(($user.Licenses.accountsku.skupartnumber) -join "
")" + $licensedUserColl += $licensedUserString + } + if ($licensedUserColl) { + $licensedUserString = $licensedUserColl -join "" + } + $licensedUserTable = "{0}{1}{2}" -f $licensedUsersTableTop, $licensedUserString, $licensedUsersTableBottom + + + } + + + $hash = [ordered]@{ + TenantName = $companyInfo.displayname + PartnerTenantName = $customer.name + Domains = $customerDomains.name + TenantId = $customer.TenantId + InitialDomain = $initialDomain.name + Licenses = $licenseTable + LicensedUsers = $licensedUserTable + } + $object = New-Object psobject -Property $hash + $365domains += $object + +} + +# Get all organisations +#$orgs = GetAllITGItems -Resource organizations + +# Get all Contacts +$itgcontacts = GetAllITGItems -Resource contacts + +$itgEmailRecords = @() +foreach ($contact in $itgcontacts) { + foreach ($email in $contact.attributes."contact-emails") { + $hash = @{ + Domain = ($email.value -split "@")[1] + OrganizationID = $contact.attributes.'organization-id' + } + $object = New-Object psobject -Property $hash + $itgEmailRecords += $object + } +} + +$allMatches = @() +foreach ($365tenant in $365domains) { + foreach ($domain in $365tenant.Domains) { + $itgContactMatches = $itgEmailRecords | Where-Object {$_.domain -contains $domain} + foreach ($match in $itgContactMatches) { + $hash = [ordered]@{ + Key = "$($365tenant.TenantId)-$($match.OrganizationID)" + TenantName = $365tenant.TenantName + Domains = ($365tenant.domains -join ", ") + TenantId = $365tenant.TenantId + InitialDomain = $365tenant.InitialDomain + OrganizationID = $match.OrganizationID + Licenses = $365tenant.Licenses + LicensedUsers = $365tenant.LicensedUsers + } + $object = New-Object psobject -Property $hash + $allMatches += $object + } + } +} + +$uniqueMatches = $allMatches | Sort-Object key -Unique + +foreach ($match in $uniqueMatches) { + $existingAssets = @() + $existingAssets += GetAllITGItems -Resource "flexible_assets?filter[organization_id]=$($match.OrganizationID)&filter[flexible_asset_type_id]=$assetTypeID" + $matchingAsset = $existingAssets | Where-Object {$_.attributes.traits.'tenant-id' -contains $match.TenantId} + + if ($matchingAsset) { + Write-Host "Updating Office 365 tenant for $($match.tenantName)" + $UpdatedBody = Build365TenantAsset -tenantInfo $match + $updatedItem = UpdateITGItem -resource flexible_assets -existingItem $matchingAsset -newBody $UpdatedBody + } + else { + Write-Host "Creating Office 365 tenant for $($match.tenantName)" + $newBody = Build365TenantAsset -tenantInfo $match + $newItem = CreateITGItem -resource flexible_assets -body $newBody + } +} \ No newline at end of file diff --git a/Office365UserActivityAndUsage/CreateApp.ps1 b/Office365UserActivityAndUsage/CreateApp.ps1 new file mode 100644 index 0000000..e3cdf60 --- /dev/null +++ b/Office365UserActivityAndUsage/CreateApp.ps1 @@ -0,0 +1,222 @@ +# This script needs to be run by an admin account in your Office 365 tenant +# This script will create an Azure AD app in your organisation with permission +# to access resources in yours and your customers' tenants. +# It will export information about the application to a CSV located at C:\temp\. +# The CSV will include the Client ID and Secret of the application, so keep it safe. + +# Confirm C:\temp exists +$temp = Test-Path -Path C:\temp +if ($temp) { + #Write-Host "Path exists" +} +else { + Write-Host "Creating Temp folder" + New-Item -Path C:\temp -ItemType directory +} + +$applicationName = "GCITS User Activity Report Reader" + +# Change this to true if you would like to overwrite any existing applications with matching names. +$removeExistingAppWithSameName = $false +# Modify the homePage, appIdURI and logoutURI values to whatever valid URI you like. +# They don't need to be actual addresses, so feel free to make something up (as long as it's on a verified domain in your Office 365 environment eg. https://anything.yourdomain.com). +$homePage = "https://secure.gcits.com" +$appIdURI = "https://secure.gcits.com/$((New-Guid).ToString())" +$logoutURI = "https://portal.office.com" + +$URIForApplicationPermissionCall = "https://graph.microsoft.com/beta/reports/getMailboxUsageDetail(period='D7')?`$format=application/json" +$ApplicationPermissions = "Reports.Read.All Directory.Read.All Sites.Manage.All" + +Function Add-ResourcePermission($requiredAccess, $exposedPermissions, $requiredAccesses, $permissionType) { + foreach ($permission in $requiredAccesses.Trim().Split(" ")) { + $reqPermission = $null + $reqPermission = $exposedPermissions | Where-Object {$_.Value -contains $permission} + Write-Host "Collected information for $($reqPermission.Value) of type $permissionType" -ForegroundColor Green + $resourceAccess = New-Object Microsoft.Open.AzureAD.Model.ResourceAccess + $resourceAccess.Type = $permissionType + $resourceAccess.Id = $reqPermission.Id + $requiredAccess.ResourceAccess.Add($resourceAccess) + } +} + +Function Get-RequiredPermissions($requiredDelegatedPermissions, $requiredApplicationPermissions, $reqsp) { + $sp = $reqsp + $appid = $sp.AppId + $requiredAccess = New-Object Microsoft.Open.AzureAD.Model.RequiredResourceAccess + $requiredAccess.ResourceAppId = $appid + $requiredAccess.ResourceAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.ResourceAccess] + if ($requiredDelegatedPermissions) { + Add-ResourcePermission $requiredAccess -exposedPermissions $sp.Oauth2Permissions -requiredAccesses $requiredDelegatedPermissions -permissionType "Scope" + } + if ($requiredApplicationPermissions) { + Add-ResourcePermission $requiredAccess -exposedPermissions $sp.AppRoles -requiredAccesses $requiredApplicationPermissions -permissionType "Role" + } + return $requiredAccess +} +Function New-AppKey ($fromDate, $durationInYears, $pw) { + $endDate = $fromDate.AddYears($durationInYears) + $keyId = (New-Guid).ToString() + $key = New-Object Microsoft.Open.AzureAD.Model.PasswordCredential($null, $endDate, $keyId, $fromDate, $pw) + return $key +} + +Function Test-AppKey($fromDate, $durationInYears, $pw) { + + $testKey = New-AppKey -fromDate $fromDate -durationInYears $durationInYears -pw $pw + while ($testKey.Value -match "\+" -or $testKey.Value -match "/") { + Write-Host "Secret contains + or / and may not authenticate correctly. Regenerating..." -ForegroundColor Yellow + $pw = Initialize-AppKey + $testKey = New-AppKey -fromDate $fromDate -durationInYears $durationInYears -pw $pw + } + Write-Host "Secret doesn't contain + or /. Continuing..." -ForegroundColor Green + $key = $testKey + + return $key +} + +Function Initialize-AppKey { + $aesManaged = New-Object "System.Security.Cryptography.AesManaged" + $aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC + $aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::Zeros + $aesManaged.BlockSize = 128 + $aesManaged.KeySize = 256 + $aesManaged.GenerateKey() + return [System.Convert]::ToBase64String($aesManaged.Key) +} +function Confirm-MicrosoftGraphServicePrincipal { + $graphsp = Get-AzureADServicePrincipal -SearchString "Microsoft Graph" + if (!$graphsp) { + $graphsp = Get-AzureADServicePrincipal -SearchString "Microsoft.Azure.AgregatorService" + } + if (!$graphsp) { + Login-AzureRmAccount -Credential $credentials + New-AzureRmADServicePrincipal -ApplicationId "00000003-0000-0000-c000-000000000000" + $graphsp = Get-AzureADServicePrincipal -SearchString "Microsoft Graph" + } + return $graphsp +} +Write-Host "Connecting to Azure AD. The login window may appear behind Visual Studio Code." +Connect-AzureAD + +Write-Host "Creating application in tenant: $((Get-AzureADTenantDetail).displayName)" + +# Check for the Microsoft Graph Service Principal. If it doesn't exist already, create it. +$graphsp = Confirm-MicrosoftGraphServicePrincipal + +$existingapp = $null +$existingapp = get-azureadapplication -SearchString $applicationName +if ($existingapp -and $removeExistingAppWithSameName) { + Remove-Azureadapplication -ObjectId $existingApp.objectId +} + +# RSPS +$rsps = @() +if ($graphsp) { + $rsps += $graphsp + $tenant_id = (Get-AzureADTenantDetail).ObjectId + $tenantName = (Get-AzureADTenantDetail).DisplayName + + # Add Required Resources Access (Microsoft Graph) + $requiredResourcesAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.RequiredResourceAccess] + $microsoftGraphRequiredPermissions = Get-RequiredPermissions -reqsp $graphsp -requiredApplicationPermissions $ApplicationPermissions -requiredDelegatedPermissions $DelegatedPermissions + $requiredResourcesAccess.Add($microsoftGraphRequiredPermissions) + + # Get an application key + $pw = Initialize-AppKey + $fromDate = [System.DateTime]::Now + $appKey = Test-AppKey -fromDate $fromDate -durationInYears 99 -pw $pw + + Write-Host "Creating the AAD application $applicationName" -ForegroundColor Blue + $aadApplication = New-AzureADApplication -DisplayName $applicationName ` + -HomePage $homePage ` + -ReplyUrls $homePage ` + -IdentifierUris $appIdURI ` + -LogoutUrl $logoutURI ` + -RequiredResourceAccess $requiredResourcesAccess ` + -PasswordCredentials $appKey ` + -AvailableToOtherTenants $true + + # Creating the Service Principal for the application + $servicePrincipal = New-AzureADServicePrincipal -AppId $aadApplication.AppId + + Write-Host "Assigning Permissions" -ForegroundColor Yellow + + # Assign application permissions to the application + foreach ($app in $requiredResourcesAccess) { + $reqAppSP = $rsps | Where-Object {$_.appid -contains $app.ResourceAppId} + Write-Host "Assigning Application permissions for $($reqAppSP.displayName)" -ForegroundColor DarkYellow + foreach ($resource in $app.ResourceAccess) { + if ($resource.Type -match "Role") { + New-AzureADServiceAppRoleAssignment -ObjectId $serviceprincipal.ObjectId ` + -PrincipalId $serviceprincipal.ObjectId -ResourceId $reqAppSP.ObjectId -Id $resource.Id + } + } + } + + # This provides the application with access to your customer tenants. + $group = Get-AzureADGroup -Filter "displayName eq 'Adminagents'" + Add-AzureADGroupMember -ObjectId $group.ObjectId -RefObjectId $servicePrincipal.ObjectId + + Write-Host "App Created" -ForegroundColor Green + + # Define parameters for Microsoft Graph access token retrieval + $client_id = $aadApplication.AppId; + $client_secret = $appkey.Value + $tenant_id = (Get-AzureADTenantDetail).ObjectId + $resource = "https://graph.microsoft.com" + $authority = "https://login.microsoftonline.com/$tenant_id" + $tokenEndpointUri = "$authority/oauth2/token" + + # Get the access token using grant type password for Delegated Permissions or grant type client_credentials for Application Permissions + + $content = "grant_type=client_credentials&client_id=$client_id&client_secret=$client_secret&resource=$resource" + + # Try to execute the API call 6 times + + $Stoploop = $false + [int]$Retrycount = "0" + do { + try { + $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing + Write-Host "Retrieved Access Token" -ForegroundColor Green + # Assign access token + $access_token = $response.access_token + $body = $null + + $body = Invoke-RestMethod ` + -Uri $UriForApplicationPermissionCall ` + -Headers @{"Authorization" = "Bearer $access_token"} ` + -ContentType "application/json" ` + -Method GET ` + + Write-Host "Retrieved Graph content" -ForegroundColor Green + $Stoploop = $true + } + catch { + if ($Retrycount -gt 5) { + Write-Host "Could not get Graph content after 6 retries." -ForegroundColor Red + $Stoploop = $true + } + else { + Write-Host "Could not get Graph content. Retrying in 5 seconds..." -ForegroundColor DarkYellow + Start-Sleep -Seconds 5 + $Retrycount ++ + } + } + } + While ($Stoploop -eq $false) + + $appInfo = [pscustomobject][ordered]@{ + ApplicationName = $ApplicationName + TenantName = $tenantName + TenantId = $tenant_id + clientId = $client_id + clientSecret = $client_secret + ApplicationPermissions = $ApplicationPermissions + } + + $AppInfo | Export-Csv C:\temp\AzureADApp.csv -Append -NoTypeInformation +} +else { + Write-Host "Microsoft Graph Service Principal could not be found or created" -ForegroundColor Red +} \ No newline at end of file diff --git a/Office365UserActivityAndUsage/SyncOffice365UserActivityAndUsage.ps1 b/Office365UserActivityAndUsage/SyncOffice365UserActivityAndUsage.ps1 new file mode 100644 index 0000000..1b557e9 --- /dev/null +++ b/Office365UserActivityAndUsage/SyncOffice365UserActivityAndUsage.ps1 @@ -0,0 +1,745 @@ +# Azure AD App Details +$client_id = "EnterYourClientIDHere" +$client_secret = "EnterYourClientSecretHere" +$ourTenantId = "EnterYourTenantIdHere" +$ourCompanyName = "EnterYourCompanyNameHere" # eg. GCITS +$ourDomainName = "EnterYourDefaultDomainHere" # eg. gcits.com +$ListName = "Office 365 - IT Glue match register" +$graphBaseUri = "https://graph.microsoft.com/v1.0/" +$siteid = "root" +$UserList = "AAD Users" + + +# IT Glue Details +# EU tenants may need to update this to "https://api.eu.itglue.com" +$ITGbaseURI = "https://api.itglue.com" +$ITGkey = "EnterYourITGlueAPIKeyHere" +$ITGheaders = @{"x-api-key" = $ITGkey } +$FlexibleAssetName = "Office 365 User Report" + +function Get-GCITSAccessToken($appCredential, $tenantId) { + $client_id = $appCredential.appID + $client_secret = $appCredential.secret + $tenant_id = $tenantid + $resource = "https://graph.microsoft.com" + $authority = "https://login.microsoftonline.com/$tenant_id" + $tokenEndpointUri = "$authority/oauth2/token" + $content = "grant_type=client_credentials&client_id=$client_id&client_secret=$client_secret&resource=$resource" + $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing + $access_token = $response.access_token + return $access_token +} + +function Get-GCITSMSGraphResource($Resource) { + $graphBaseUri = "https://graph.microsoft.com/beta" + $values = @() + $result = Invoke-RestMethod -Uri "$graphBaseUri/$resource" -Headers $headers + if ($result.value) { + $values += $result.value + if ($result."@odata.nextLink") { + do { + $result = Invoke-RestMethod -Uri $result."@odata.nextLink" -Headers $headers + $values += $result.value + } while ($result."@odata.nextLink") + } + } + else { + $values = $result + } + return $values +} +function New-GCITSSharePointColumn($Name, $Type, $Indexed, $lookupListName, $lookupColumnPrimaryName, $lookupColumnName, $longText) { + if ($longText) { + $column = [ordered]@{ + name = $Name + indexed = $Indexed + $Type = @{ + maxLength = 0 + allowMultipleLines = $True + #appendChangesToExistingText = $False + #linesForEditing = 6 + #textType = "plain" + } + + } + } + else { + $column = [ordered]@{ + name = $Name + indexed = $Indexed + $Type = @{ } + } + + if ($lookupListName -and $type -contains "lookup") { + $list = Get-GCITSSharePointList -ListName $lookupListName + if ($list) { + $column.lookup.listId = $list.id + $column.lookup.columnName = $lookupColumnName + } + } + } + + return $column +} +function New-GCITSSharePointList ($Name, $ColumnCollection) { + $list = @{ + displayName = $Name + columns = $columnCollection + } | Convertto-json -Depth 10 + + $newList = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method POST -Body $list + return $newList +} + +function Remove-GCITSSharePointList ($ListId) { + $removeList = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/$ListId" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method DELETE + return $removeList +} + +function Remove-GCITSSharePointListItem ($ListId, $ItemId) { + $removeItem = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/$ListId/items/$ItemId" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method DELETE + return $removeItem +} + +function New-GCITSSharePointListItem($ItemObject, $ListId) { + + $itemBody = @{ + fields = $ItemObject + } | ConvertTo-Json -Depth 10 + + $listItem = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/$listId/items" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method Post ` + -Body $itemBody +} + +function Get-GCITSSharePointListItem($ListId, $ItemId, $Query) { + + if ($ItemId) { + $listItem = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items/$ItemId ` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value = $listItem + } + elseif ($Query) { + $listItems = $null + $listItems = Invoke-RestMethod -Uri "$graphBaseUri/sites/$siteid/lists/$listId/items/?expand=fields&`$filter=$Query" ` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value = @() + $value = $listItems.value + if ($listitems."@odata.nextLink") { + $nextLink = $true + } + if ($nextLink) { + do { + $listItems = Invoke-RestMethod -Uri $listitems."@odata.nextLink"` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value += $listItems.value + if (!$listitems."@odata.nextLink") { + $nextLink = $false + } + } until (!$nextLink) + } + } + else { + $listItems = $null + $listItems = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items?expand=fields ` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value = @() + $value = $listItems.value + if ($listitems."@odata.nextLink") { + $nextLink = $true + } + if ($nextLink) { + do { + $listItems = Invoke-RestMethod -Uri $listitems."@odata.nextLink"` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value += $listItems.value + if (!$listitems."@odata.nextLink") { + $nextLink = $false + } + } until (!$nextLink) + } + } + return $value +} + +function Set-GCITSSharePointListItem($ListId, $ItemId, $ItemObject) { + $listItem = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items/$ItemId/fields ` + -Method Patch -headers $SPHeaders ` + -ContentType application/json ` + -Body ($itemObject | ConvertTo-Json) + $return = $listItem +} + +function Get-GCITSSharePointList($ListName) { + $list = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists?expand=columns&`$filter=displayName eq '$ListName'" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method GET + $list = $list.value + return $list +} +Function Get-GCITSMSGraphReport ($ReportName, $Resource) { + $Report = Get-GCITSMSGraphResource -Resource $Resource + if ($Report) { + $Report | Add-Member ReportName $ReportName -Force + } + Write-Host "$reportname - $($report.count)" + return $Report +} + +function Get-GCITSSpacingTitleCase ($String) { + $String = ($String -creplace '([A-Z\W_]|\d+)(?$(Get-GCITSSpacingTitleCase -String $objectproperty): $($object.$($objectProperty))" + } + } + $reportKeyValue = $stringArray -join "
" + } + elseif ($reportKey.OriginalPropertyName -eq "assignedLicenses") { + $stringArray = @() + foreach ($license in $reportKeyValue) { + $stringArray += ($licenses | Where-Object { $_.skuid -eq $license.skuid }).skupartnumber + } + $reportKeyValue = $stringArray -join ", " + } + elseif ($reportKey.OriginalPropertyName -eq "proxyAddresses") { + $stringArray = @() + foreach ($address in $reportKeyValue) { + $stringArray += ($address -split ":")[1] + } + $reportKeyValue = $stringArray -join ", " + + } + else { + if (($reportKeyValue[0].psobject.properties.name | Measure-Object).count -gt 0) { + $reportKeyValue = "Detected" + } + else { + $reportKeyValue = "None" + } + } + + } + if ($reportKeyValue -and $field) { + $userReportTraits | Add-member $field.'name-key' $reportKeyValue -Force + } + } + } + + foreach ($ITGOrg in $ITGOrgToSync) { + $FlexibleAssetBody = [pscustomobject]@{ + data = @{ + type = "flexible-assets" + attributes = [pscustomobject]@{ + "organization-id" = $ITGOrg.fields.ITGlueOrgId + "flexible-asset-type-id" = $flexibleAsset.id + traits = $userReportTraits + } + } + } + + $FlexibleAssetItem = $FlexibleAssetBody | ConvertTo-Json -Depth 10 + try { + $existingFlexibleAssetsForUser = $existingFlexibleAssetsForTenant | Where-Object { $_.attributes.traits.id -eq $user.id -and $_.attributes.'organization-id' -eq $ITGOrg.fields.ITGlueOrgId } | Select-Object -First 1 + if ($existingFlexibleAssetsForUser) { + $updateItem = Set-GCITSITGItem -Resource flexible_assets -existingItem $existingFlexibleAssetsForUser -Body $FlexibleAssetItem + } + else { + $newItem = New-GCITSITGItem -resource flexible_assets -body $FlexibleAssetItem + } + } + catch { + Write-Host "Error here: $($error[0])" + } + } + } + $TenantsToDisable = $ITGOrgsForTenant | where-object { $_.fields.DisableSync } + # Remove Office 365 user reports from tenants where sync is disabled in SharePoint List + foreach ($tenant in $TenantsToDisable) { + $assetsToRemove = $existingFlexibleAssetsForTenant | Where-Object { $_.attributes.'organization-id' -eq $tenant.fields.ITGlueOrgId } + if ($assetsToRemove) { + foreach ($item in $assetsToRemove) { + Write-Host "Removing $($item.attributes.traits.'user-principal-name') from $($tenant.fields.ITGlueOrg)" + Remove-GCITSITGItem -Resource flexible_assets -ExistingItem $item + } + } + } + } + } +} + +<# +# If you want to clear out all existing assets, just uncomment and run the following script block once you've initialised the variables and functions at the top of the script. +# You can do this by selecting the code and pressing F8. + +$flexibleAsset = Get-GCITSITGItem -Resource "flexible_asset_types?filter[name]=$FlexibleAssetName" +[array]$existingAssets = Get-GCITSITGItem -Resource "flexible_assets?filter[organization_id]=$($ITGCompany)&filter[flexible_asset_type_id]=$($flexibleasset.id)" + +foreach($item in $existingAssets){ + Write-Host "Removing $($item.attributes.traits.'user-principal-name')" + Remove-GCITSITGItem -Resource flexible_assets -ExistingItem $item +} +#> \ No newline at end of file diff --git a/Office365UserActivityAndUsage/sync-office-365-user-activity-and-usage-with-it-glue.md b/Office365UserActivityAndUsage/sync-office-365-user-activity-and-usage-with-it-glue.md new file mode 100644 index 0000000..282e999 --- /dev/null +++ b/Office365UserActivityAndUsage/sync-office-365-user-activity-and-usage-with-it-glue.md @@ -0,0 +1,1068 @@ +Sync Office 365 User Activity and Usage with IT Glue +==================================================== + +![Office 365 User Reports Overview in IT Glue](https://i2.wp.com/gcits.com/wp-content/uploads/UserReportsOverview.png?resize=1030%2C472&ssl=1) + +The Microsoft Graph API provides a bunch of useful user activity and usage reports that provide insight into how users are taking advantage of Office 365 services. This guide will demonstrate how to collect and sync that information with a dynamic flexible asset in IT Glue. + +The information we’ll be collecting includes: + +* Basic user information with licenses and aliases![Office 365 User Report Detail in IT Glue] + + ![Office 365 User Report Detail in IT Glue](https://i2.wp.com/gcits.com/wp-content/uploads/Office365UserReportDetail.png?resize=1030%2C621&ssl=1) + +* Office 365 app installs for each user![Office 365 App Usage per user in IT Glue](./Sync Office 365 User Activity and Usage with IT Glue - GCITS_files/Office365AppUsage.png) + + ![Office 365 App Usage per user in IT Glue](https://i2.wp.com/gcits.com/wp-content/uploads/Office365AppUsage.png?resize=1030%2C534&ssl=1) + +* Email app usage, SharePoint and Microsoft Teams Activity![Office 365 Email App And SharePoint Usage in IT Glue] + + ![Office 365 Email App And SharePoint Usage in IT Glue](https://i1.wp.com/gcits.com/wp-content/uploads/Office365EmailAppAndSharePoointUsage.png?resize=1030%2C814&ssl=1) + +* Mailbox usage and activity + + ![Office 365 Active User And Mailbox Usage in IT Glue](https://i0.wp.com/gcits.com/wp-content/uploads/Office365ActiveUserAndMailboxUsage.png?resize=1030%2C870&ssl=1) + +* OneDrive usage and activity![Office 365 OneDrive Usage in IT Glue] + + ![Office 365 OneDrive Usage in IT Glue](https://i1.wp.com/gcits.com/wp-content/uploads/Office365OneDriveUsageItGlue.png?resize=1030%2C657&ssl=1) + +* Yammer activity + +This guide is designed for Microsoft Partners who have delegated access to customer tenants. + +Prerequisites +------------- + +* To run the first script, you’ll need to install the Azure AD PowerShell Module. You can do this by opening PowerShell as an administrator and running: + + `Install-Module AzureAD` + +* To authorise the application to access your own and your customers’ tenants, you’ll need to be a Global Administrator. + +Solution outline +---------------- + +This solution consists of the following: + +### Script 1 – Authorise an Azure AD Application to access customers’ reports + +Creates an application with access to Mailbox usage reports for your own and customers’ tenants. This one needs to be run as a Global Admin. + +### Script 2 – Syncing Tenant to IT Glue Org matches with SharePoint and Office 365 Usage/Activity Reports with IT Glue + +Retrieves the usage reports, creates a SharePoint list of suggested matches between Office 365 tenants and IT Glue organisation. Once matches are confirmed, the Office 365 user details are synced with IT Glue. This script can be run as a regular as a scheduled task or Azure Function + +Authorise an Azure AD Application to access customers’ reports +-------------------------------------------------------------- + +1. Double click the below script to select it. +2. Copy and paste the script into a new file in Visual Studio Code and save it with a **.ps1** extension +3. Install the recommended PowerShell module if you haven’t already +4. Modify the $homePage and $logoutURI values to any valid URI that you like. They don’t need to be actual addresses, so feel free to make something up. Set the $appIDUri variable to a use a valid domain in your tenant. eg. https://yourdomain.com/$((New-Guid).ToString())![Update HomePage and AppIDUri] + + ![Update HomePage and AppIDUri](https://i1.wp.com/gcits.com/wp-content/uploads/UpdateHomePageandAppIDUri.png?resize=899%2C113&ssl=1) + +5. Press **F5** to run the script +6. Sign in to Azure AD using your global admin credentials. Note that the login window may appear behind Visual Studio Code. +7. Wait for the script to complete.![Creating Azure AD Application Via Power Shell] + + ![Creating Azure AD Application Via Power Shell](https://i0.wp.com/gcits.com/wp-content/uploads/CreatingAzureAdApplicationViaPowerShell.png?resize=1030%2C525&ssl=1) + +8. Retrieve the **client ID, client secret and tenant ID** from the exported CSV at C:\\temp\\azureadapp.csv. (below image is just an example.) + + ![Exported Info for Azure Ad App](https://i0.wp.com/gcits.com/wp-content/uploads/ExportedInfoAzureAdApp.png?resize=1030%2C260&ssl=1) + + +### PowerShell Script to create and authorise Azure AD Application + +```powershell +# This script needs to be run by an admin account in your Office 365 tenant +# This script will create an Azure AD app in your organisation with permission +# to access resources in yours and your customers' tenants. +# It will export information about the application to a CSV located at C:\temp\. +# The CSV will include the Client ID and Secret of the application, so keep it safe. + +# Confirm C:\temp exists +$temp = Test-Path -Path C:\temp +if ($temp) { + #Write-Host "Path exists" +} +else { + Write-Host "Creating Temp folder" + New-Item -Path C:\temp -ItemType directory +} + +$applicationName = "GCITS User Activity Report Reader" + +# Change this to true if you would like to overwrite any existing applications with matching names. +$removeExistingAppWithSameName = $false +# Modify the homePage, appIdURI and logoutURI values to whatever valid URI you like. +# They don't need to be actual addresses, so feel free to make something up (as long as it's on a verified domain in your Office 365 environment eg. https://anything.yourdomain.com). +$homePage = "https://secure.gcits.com" +$appIdURI = "https://secure.gcits.com/$((New-Guid).ToString())" +$logoutURI = "https://portal.office.com" + +$URIForApplicationPermissionCall = "https://graph.microsoft.com/beta/reports/getMailboxUsageDetail(period='D7')?`$format=application/json" +$ApplicationPermissions = "Reports.Read.All Directory.Read.All Sites.Manage.All" + +Function Add-ResourcePermission($requiredAccess, $exposedPermissions, $requiredAccesses, $permissionType) { + foreach ($permission in $requiredAccesses.Trim().Split(" ")) { + $reqPermission = $null + $reqPermission = $exposedPermissions | Where-Object {$_.Value -contains $permission} + Write-Host "Collected information for $($reqPermission.Value) of type $permissionType" -ForegroundColor Green + $resourceAccess = New-Object Microsoft.Open.AzureAD.Model.ResourceAccess + $resourceAccess.Type = $permissionType + $resourceAccess.Id = $reqPermission.Id + $requiredAccess.ResourceAccess.Add($resourceAccess) + } +} + +Function Get-RequiredPermissions($requiredDelegatedPermissions, $requiredApplicationPermissions, $reqsp) { + $sp = $reqsp + $appid = $sp.AppId + $requiredAccess = New-Object Microsoft.Open.AzureAD.Model.RequiredResourceAccess + $requiredAccess.ResourceAppId = $appid + $requiredAccess.ResourceAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.ResourceAccess] + if ($requiredDelegatedPermissions) { + Add-ResourcePermission $requiredAccess -exposedPermissions $sp.Oauth2Permissions -requiredAccesses $requiredDelegatedPermissions -permissionType "Scope" + } + if ($requiredApplicationPermissions) { + Add-ResourcePermission $requiredAccess -exposedPermissions $sp.AppRoles -requiredAccesses $requiredApplicationPermissions -permissionType "Role" + } + return $requiredAccess +} +Function New-AppKey ($fromDate, $durationInYears, $pw) { + $endDate = $fromDate.AddYears($durationInYears) + $keyId = (New-Guid).ToString() + $key = New-Object Microsoft.Open.AzureAD.Model.PasswordCredential($null, $endDate, $keyId, $fromDate, $pw) + return $key +} + +Function Test-AppKey($fromDate, $durationInYears, $pw) { + + $testKey = New-AppKey -fromDate $fromDate -durationInYears $durationInYears -pw $pw + while ($testKey.Value -match "\+" -or $testKey.Value -match "/") { + Write-Host "Secret contains + or / and may not authenticate correctly. Regenerating..." -ForegroundColor Yellow + $pw = Initialize-AppKey + $testKey = New-AppKey -fromDate $fromDate -durationInYears $durationInYears -pw $pw + } + Write-Host "Secret doesn't contain + or /. Continuing..." -ForegroundColor Green + $key = $testKey + + return $key +} + +Function Initialize-AppKey { + $aesManaged = New-Object "System.Security.Cryptography.AesManaged" + $aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC + $aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::Zeros + $aesManaged.BlockSize = 128 + $aesManaged.KeySize = 256 + $aesManaged.GenerateKey() + return [System.Convert]::ToBase64String($aesManaged.Key) +} +function Confirm-MicrosoftGraphServicePrincipal { + $graphsp = Get-AzureADServicePrincipal -SearchString "Microsoft Graph" + if (!$graphsp) { + $graphsp = Get-AzureADServicePrincipal -SearchString "Microsoft.Azure.AgregatorService" + } + if (!$graphsp) { + Login-AzureRmAccount -Credential $credentials + New-AzureRmADServicePrincipal -ApplicationId "00000003-0000-0000-c000-000000000000" + $graphsp = Get-AzureADServicePrincipal -SearchString "Microsoft Graph" + } + return $graphsp +} +Write-Host "Connecting to Azure AD. The login window may appear behind Visual Studio Code." +Connect-AzureAD + +Write-Host "Creating application in tenant: $((Get-AzureADTenantDetail).displayName)" + +# Check for the Microsoft Graph Service Principal. If it doesn't exist already, create it. +$graphsp = Confirm-MicrosoftGraphServicePrincipal + +$existingapp = $null +$existingapp = get-azureadapplication -SearchString $applicationName +if ($existingapp -and $removeExistingAppWithSameName) { + Remove-Azureadapplication -ObjectId $existingApp.objectId +} + +# RSPS +$rsps = @() +if ($graphsp) { + $rsps += $graphsp + $tenant_id = (Get-AzureADTenantDetail).ObjectId + $tenantName = (Get-AzureADTenantDetail).DisplayName + + # Add Required Resources Access (Microsoft Graph) + $requiredResourcesAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.RequiredResourceAccess] + $microsoftGraphRequiredPermissions = Get-RequiredPermissions -reqsp $graphsp -requiredApplicationPermissions $ApplicationPermissions -requiredDelegatedPermissions $DelegatedPermissions + $requiredResourcesAccess.Add($microsoftGraphRequiredPermissions) + + # Get an application key + $pw = Initialize-AppKey + $fromDate = [System.DateTime]::Now + $appKey = Test-AppKey -fromDate $fromDate -durationInYears 99 -pw $pw + + Write-Host "Creating the AAD application $applicationName" -ForegroundColor Blue + $aadApplication = New-AzureADApplication -DisplayName $applicationName ` + -HomePage $homePage ` + -ReplyUrls $homePage ` + -IdentifierUris $appIdURI ` + -LogoutUrl $logoutURI ` + -RequiredResourceAccess $requiredResourcesAccess ` + -PasswordCredentials $appKey ` + -AvailableToOtherTenants $true + + # Creating the Service Principal for the application + $servicePrincipal = New-AzureADServicePrincipal -AppId $aadApplication.AppId + + Write-Host "Assigning Permissions" -ForegroundColor Yellow + + # Assign application permissions to the application + foreach ($app in $requiredResourcesAccess) { + $reqAppSP = $rsps | Where-Object {$_.appid -contains $app.ResourceAppId} + Write-Host "Assigning Application permissions for $($reqAppSP.displayName)" -ForegroundColor DarkYellow + foreach ($resource in $app.ResourceAccess) { + if ($resource.Type -match "Role") { + New-AzureADServiceAppRoleAssignment -ObjectId $serviceprincipal.ObjectId ` + -PrincipalId $serviceprincipal.ObjectId -ResourceId $reqAppSP.ObjectId -Id $resource.Id + } + } + } + + # This provides the application with access to your customer tenants. + $group = Get-AzureADGroup -Filter "displayName eq 'Adminagents'" + Add-AzureADGroupMember -ObjectId $group.ObjectId -RefObjectId $servicePrincipal.ObjectId + + Write-Host "App Created" -ForegroundColor Green + + # Define parameters for Microsoft Graph access token retrieval + $client_id = $aadApplication.AppId; + $client_secret = $appkey.Value + $tenant_id = (Get-AzureADTenantDetail).ObjectId + $resource = "https://graph.microsoft.com" + $authority = "https://login.microsoftonline.com/$tenant_id" + $tokenEndpointUri = "$authority/oauth2/token" + + # Get the access token using grant type password for Delegated Permissions or grant type client_credentials for Application Permissions + + $content = "grant_type=client_credentials&client_id=$client_id&client_secret=$client_secret&resource=$resource" + + # Try to execute the API call 6 times + + $Stoploop = $false + [int]$Retrycount = "0" + do { + try { + $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing + Write-Host "Retrieved Access Token" -ForegroundColor Green + # Assign access token + $access_token = $response.access_token + $body = $null + + $body = Invoke-RestMethod ` + -Uri $UriForApplicationPermissionCall ` + -Headers @{"Authorization" = "Bearer $access_token"} ` + -ContentType "application/json" ` + -Method GET ` + + Write-Host "Retrieved Graph content" -ForegroundColor Green + $Stoploop = $true + } + catch { + if ($Retrycount -gt 5) { + Write-Host "Could not get Graph content after 6 retries." -ForegroundColor Red + $Stoploop = $true + } + else { + Write-Host "Could not get Graph content. Retrying in 5 seconds..." -ForegroundColor DarkYellow + Start-Sleep -Seconds 5 + $Retrycount ++ + } + } + } + While ($Stoploop -eq $false) + + $appInfo = [pscustomobject][ordered]@{ + ApplicationName = $ApplicationName + TenantName = $tenantName + TenantId = $tenant_id + clientId = $client_id + clientSecret = $client_secret + ApplicationPermissions = $ApplicationPermissions + } + + $AppInfo | Export-Csv C:\temp\AzureADApp.csv -Append -NoTypeInformation +} +else { + Write-Host "Microsoft Graph Service Principal could not be found or created" -ForegroundColor Red +} + +``` + +Script 2 – Syncing Office 365 User Reports with IT Glue +------------------------------------------------------- + +This script will run through your Office 365 customers and retrieve Office 365 usage reports. It will also create a SharePoint list containing a register of matches of Office 365 tenants to IT Glue organisations. This script uses the same match register as our [Secure Score to IT Glue guide](https://gcits.com/knowledge-base/sync-microsoft-secure-scores-with-it-glue/), so if you’re using that already, you won’t need to re-match anything. + +1. Double click the below script to select it. +2. Copy and paste the script into a new file in Visual Studio Code and save it with a .ps1 extension +3. Replace $appId, $secret, and $ourTenantId with your client ID, client secret and Tenant Id values respectively. +4. Create and retrieve an IT Glue API key by logging in as an IT Glue administrator and navigating to **Account, API Keys** and choosing **Generate API Key**. Paste this key into the $ITGApiKey value +5. Press F5 to run the script and wait for it to complete. +6. If you haven’t run our Secure Score to IT Glue script already, the script will stop once it has created a SharePoint list with a register of Office 365 tenant to IT Glue Org matches. To access this list, log onto your root SharePoint site at https://yourtenantname.sharepoint.com +7. Click the settings cog on the top right, and select **Site Contents** +8. Locate the **Office 365 – IT Glue match register** list. Edit any incorrect matches by setting DisableSync to Yes +9. Once you’re happy with your Office 365 Tenant to IT Glue company matches, return to Visual Studio Code and press Enter to continue +10. Wait for the script to complete, then log into IT Glue and navigate to **Account**, **Customise Sidebar** +11. Drag the **Office 365 User Report** flexible asset to the sidebar and click **Save.** + +### PowerShell script to sync Office 365 User Activity and Usage with IT Glue + +```powershell +# Azure AD App Details +$client_id = "EnterYourClientIDHere" +$client_secret = "EnterYourClientSecretHere" +$ourTenantId = "EnterYourTenantIdHere" +$ourCompanyName = "EnterYourCompanyNameHere" # eg. GCITS +$ourDomainName = "EnterYourDefaultDomainHere" # eg. gcits.com +$ListName = "Office 365 - IT Glue match register" +$graphBaseUri = "https://graph.microsoft.com/v1.0/" +$siteid = "root" +$UserList = "AAD Users" + + +# IT Glue Details +# EU tenants may need to update this to "https://api.eu.itglue.com" +$ITGbaseURI = "https://api.itglue.com" +$ITGkey = "EnterYourITGlueAPIKeyHere" +$ITGheaders = @{"x-api-key" = $ITGkey } +$FlexibleAssetName = "Office 365 User Report" + +function Get-GCITSAccessToken($appCredential, $tenantId) { + $client_id = $appCredential.appID + $client_secret = $appCredential.secret + $tenant_id = $tenantid + $resource = "https://graph.microsoft.com" + $authority = "https://login.microsoftonline.com/$tenant_id" + $tokenEndpointUri = "$authority/oauth2/token" + $content = "grant_type=client_credentials&client_id=$client_id&client_secret=$client_secret&resource=$resource" + $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing + $access_token = $response.access_token + return $access_token +} + +function Get-GCITSMSGraphResource($Resource) { + $graphBaseUri = "https://graph.microsoft.com/beta" + $values = @() + $result = Invoke-RestMethod -Uri "$graphBaseUri/$resource" -Headers $headers + if ($result.value) { + $values += $result.value + if ($result."@odata.nextLink") { + do { + $result = Invoke-RestMethod -Uri $result."@odata.nextLink" -Headers $headers + $values += $result.value + } while ($result."@odata.nextLink") + } + } + else { + $values = $result + } + return $values +} +function New-GCITSSharePointColumn($Name, $Type, $Indexed, $lookupListName, $lookupColumnPrimaryName, $lookupColumnName, $longText) { + if ($longText) { + $column = [ordered]@{ + name = $Name + indexed = $Indexed + $Type = @{ + maxLength = 0 + allowMultipleLines = $True + #appendChangesToExistingText = $False + #linesForEditing = 6 + #textType = "plain" + } + + } + } + else { + $column = [ordered]@{ + name = $Name + indexed = $Indexed + $Type = @{ } + } + + if ($lookupListName -and $type -contains "lookup") { + $list = Get-GCITSSharePointList -ListName $lookupListName + if ($list) { + $column.lookup.listId = $list.id + $column.lookup.columnName = $lookupColumnName + } + } + } + + return $column +} +function New-GCITSSharePointList ($Name, $ColumnCollection) { + $list = @{ + displayName = $Name + columns = $columnCollection + } | Convertto-json -Depth 10 + + $newList = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method POST -Body $list + return $newList +} + +function Remove-GCITSSharePointList ($ListId) { + $removeList = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/$ListId" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method DELETE + return $removeList +} + +function Remove-GCITSSharePointListItem ($ListId, $ItemId) { + $removeItem = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/$ListId/items/$ItemId" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method DELETE + return $removeItem +} + +function New-GCITSSharePointListItem($ItemObject, $ListId) { + + $itemBody = @{ + fields = $ItemObject + } | ConvertTo-Json -Depth 10 + + $listItem = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/$listId/items" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method Post ` + -Body $itemBody +} + +function Get-GCITSSharePointListItem($ListId, $ItemId, $Query) { + + if ($ItemId) { + $listItem = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items/$ItemId ` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value = $listItem + } + elseif ($Query) { + $listItems = $null + $listItems = Invoke-RestMethod -Uri "$graphBaseUri/sites/$siteid/lists/$listId/items/?expand=fields&`$filter=$Query" ` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value = @() + $value = $listItems.value + if ($listitems."@odata.nextLink") { + $nextLink = $true + } + if ($nextLink) { + do { + $listItems = Invoke-RestMethod -Uri $listitems."@odata.nextLink"` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value += $listItems.value + if (!$listitems."@odata.nextLink") { + $nextLink = $false + } + } until (!$nextLink) + } + } + else { + $listItems = $null + $listItems = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items?expand=fields ` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value = @() + $value = $listItems.value + if ($listitems."@odata.nextLink") { + $nextLink = $true + } + if ($nextLink) { + do { + $listItems = Invoke-RestMethod -Uri $listitems."@odata.nextLink"` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value += $listItems.value + if (!$listitems."@odata.nextLink") { + $nextLink = $false + } + } until (!$nextLink) + } + } + return $value +} + +function Set-GCITSSharePointListItem($ListId, $ItemId, $ItemObject) { + $listItem = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items/$ItemId/fields ` + -Method Patch -headers $SPHeaders ` + -ContentType application/json ` + -Body ($itemObject | ConvertTo-Json) + $return = $listItem +} + +function Get-GCITSSharePointList($ListName) { + $list = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists?expand=columns&`$filter=displayName eq '$ListName'" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method GET + $list = $list.value + return $list +} +Function Get-GCITSMSGraphReport ($ReportName, $Resource) { + $Report = Get-GCITSMSGraphResource -Resource $Resource + if ($Report) { + $Report | Add-Member ReportName $ReportName -Force + } + Write-Host "$reportname - $($report.count)" + return $Report +} + +function Get-GCITSSpacingTitleCase ($String) { + $String = ($String -creplace '([A-Z\W_]|\d+)(?$(Get-GCITSSpacingTitleCase -String $objectproperty): $($object.$($objectProperty))" + } + } + $reportKeyValue = $stringArray -join "
" + } + elseif ($reportKey.OriginalPropertyName -eq "assignedLicenses") { + $stringArray = @() + foreach ($license in $reportKeyValue) { + $stringArray += ($licenses | Where-Object { $_.skuid -eq $license.skuid }).skupartnumber + } + $reportKeyValue = $stringArray -join ", " + } + elseif ($reportKey.OriginalPropertyName -eq "proxyAddresses") { + $stringArray = @() + foreach ($address in $reportKeyValue) { + $stringArray += ($address -split ":")[1] + } + $reportKeyValue = $stringArray -join ", " + + } + else { + if (($reportKeyValue[0].psobject.properties.name | Measure-Object).count -gt 0) { + $reportKeyValue = "Detected" + } + else { + $reportKeyValue = "None" + } + } + + } + if ($reportKeyValue -and $field) { + $userReportTraits | Add-member $field.'name-key' $reportKeyValue -Force + } + } + } + + foreach ($ITGOrg in $ITGOrgToSync) { + $FlexibleAssetBody = [pscustomobject]@{ + data = @{ + type = "flexible-assets" + attributes = [pscustomobject]@{ + "organization-id" = $ITGOrg.fields.ITGlueOrgId + "flexible-asset-type-id" = $flexibleAsset.id + traits = $userReportTraits + } + } + } + + $FlexibleAssetItem = $FlexibleAssetBody | ConvertTo-Json -Depth 10 + try { + $existingFlexibleAssetsForUser = $existingFlexibleAssetsForTenant | Where-Object { $_.attributes.traits.id -eq $user.id -and $_.attributes.'organization-id' -eq $ITGOrg.fields.ITGlueOrgId } | Select-Object -First 1 + if ($existingFlexibleAssetsForUser) { + $updateItem = Set-GCITSITGItem -Resource flexible_assets -existingItem $existingFlexibleAssetsForUser -Body $FlexibleAssetItem + } + else { + $newItem = New-GCITSITGItem -resource flexible_assets -body $FlexibleAssetItem + } + } + catch { + Write-Host "Error here: $($error[0])" + } + } + } + $TenantsToDisable = $ITGOrgsForTenant | where-object { $_.fields.DisableSync } + # Remove Office 365 user reports from tenants where sync is disabled in SharePoint List + foreach ($tenant in $TenantsToDisable) { + $assetsToRemove = $existingFlexibleAssetsForTenant | Where-Object { $_.attributes.'organization-id' -eq $tenant.fields.ITGlueOrgId } + if ($assetsToRemove) { + foreach ($item in $assetsToRemove) { + Write-Host "Removing $($item.attributes.traits.'user-principal-name') from $($tenant.fields.ITGlueOrg)" + Remove-GCITSITGItem -Resource flexible_assets -ExistingItem $item + } + } + } + } + } +} + +<# +# If you want to clear out all existing assets, just uncomment and run the following script block once you've initialised the variables and functions at the top of the script. +# You can do this by selecting the code and pressing F8. + +$flexibleAsset = Get-GCITSITGItem -Resource "flexible_asset_types?filter[name]=$FlexibleAssetName" +[array]$existingAssets = Get-GCITSITGItem -Resource "flexible_assets?filter[organization_id]=$($ITGCompany)&filter[flexible_asset_type_id]=$($flexibleasset.id)" + +foreach($item in $existingAssets){ + Write-Host "Removing $($item.attributes.traits.'user-principal-name')" + Remove-GCITSITGItem -Resource flexible_assets -ExistingItem $item +``` \ No newline at end of file diff --git a/OrganisationSharePointSync/README.md b/OrganisationSharePointSync/README.md new file mode 100644 index 0000000..d19d536 --- /dev/null +++ b/OrganisationSharePointSync/README.md @@ -0,0 +1,323 @@ +[Source](https://gcits.com/knowledge-base/sync-it-glue-organisations-with-a-sharepoint-list-via-powershell/ "Permalink to Sync IT Glue organisations with a SharePoint List via PowerShell") + +# Sync IT Glue organisations with a SharePoint List via PowerShell + +This script will create a SharePoint list on your root SharePoint site called '**ITGlue Org Register**' populated with some basic details for each of your IT Glue organisations. + +We refer to this SharePoint List in some of our other guides. It's used as a reference for matching configurations and flexible assets with the correct IT Glue organisation. + +This script is intended to be run on a schedule within a timer triggered PowerShell Azure Function, however you can also run it as a scheduled task. + +### Prerequisites + +- You'll need to first create a SharePoint Application in your Azure AD tenant with the Sites.Manage.All permission. If you haven't done this already, [use this script][1] to create the application and retrieve the **Tenant ID**, **Client ID** and **Client Secret** from the exported CSV.![Retrieve Application Tenant ID Client ID And Secret From CSV][2] +- You'll also need to retrieve or generate an API key for IT Glue under **Account**, **Settings**, **API Keys. ![Get IT Glue API Key][3]** + +## How to sync your IT Glue organisations with a SharePoint List via PowerShell + +1. It's a good idea to run the script locally first to confirm that it works. Double click the script below to select it, then copy and paste it into a new file in Visual Studio Code +2. Save the file with a **.ps1** extension and, if you haven't already, install the recommended PowerShell extension. +3. Replace the **$key** variable with your IT Glue API key, then replace the **$tenant_id**, **$client_id** and **$client_secret** variables with the relevant values from the CSV at C:tempAzureADApp. If you haven't created this app yet, [follow this quick guide here][1]. ![Update Variables In Visual Studio Code][4] +4. Press **F5** to run the script. +5. On its first run, it will create the SharePoint list, then populate it with the basic information from your IT Glue organisations. On subsequent runs, it will update the item, or delete organisations from the list which no longer exist in IT Glue. Once you have tested the script in Visual Studio Code, continue following along below to set it up in an Azure Function.![Creating SharePoint List On First Run][5] + +## PowerShell script to sync IT Glue Organisations with a SharePoint List + +```powershell + <# + This script will sync IT Glue organisations with a SharePoint list in the root sharepoint site (eg tenantname.sharepoint.com). + The list is called 'ITGlue Org Register' + It should be run on a schedule to keep the SharePoint list up to date. + #> + + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + $key = "EnterYourITGlueAPIKeyHere" + # Note that EU hosted IT Glue Tenants may need to update the below value to "https://api.eu.itglue.com" + $ITGbaseURI = "https://api.itglue.com" + $headers = @{ + "x-api-key" = $key + } + $client_id = "EnterYourSharePointAppClientIDHere" + $client_secret = "EnterYourSharePointAppClientSecretHere" + $tenant_id = "EnterYourTenantIDHere" + $graphBaseUri = "https://graph.microsoft.com/v1.0/" + $siteid = "root" + $ListName = "ITGlue Org Register" + + function Get-GCITSITGlueItem($Resource) { + $array = @() + $body = Invoke-RestMethod -Method get -Uri "$ITGbaseUri/$Resource" -Headers $headers -ContentType application/vnd.api+json + $array += $body.data + if ($body.links.next) { + do { + $body = Invoke-RestMethod -Method get -Uri $body.links.next -Headers $headers -ContentType application/vnd.api+json + $array += $body.data + } while ($body.links.next) + } + return $array + } + + function New-GCITSSharePointColumn($Name, $Type, $Indexed) { + $column = [ordered]@{ + name = $Name + indexed = $Indexed + $Type = @{} + } + return $column + } + + function New-GCITSSharePointList ($Name, $ColumnCollection) { + $list = @{ + displayName = $Name + columns = $columnCollection + } | Convertto-json -Depth 10 + + $newList = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method POST -Body $list + return $newList + } + + function Remove-GCITSSharePointList ($ListId) { + $removeList = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/$ListId" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method DELETE + return $removeList + } + + function Remove-GCITSSharePointListItem ($ListId, $ItemId) { + $removeItem = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/$ListId/items/$ItemId" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method DELETE + return $removeItem + } + + function New-GCITSSharePointListItem($ItemObject, $ListId) { + + $itemBody = @{ + fields = $ItemObject + } | ConvertTo-Json -Depth 10 + + $listItem = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/$listId/items" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method Post ` + -Body $itemBody + } + + function Get-GCITSSharePointItem($ListId, $ItemId) { + + if ($ItemId) { + $listItem = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items/$ItemId ` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value = $listItem + } + else { + $listItems = $null + $listItems = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items?expand=fields ` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value = @() + $value = $listItems.value + if ($listitems."@odata.nextLink") { + $nextLink = $true + } + if ($nextLink) { + do { + $listItems = Invoke-RestMethod -Uri $listitems."@odata.nextLink"` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value += $listItems.value + if (!$listitems."@odata.nextLink") { + $nextLink = $false + } + } until (!$nextLink) + } + } + return $value + } + + function Set-GCITSSharePointListItem($ListId, $ItemId, $ItemObject) { + $listItem = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items/$ItemId/fields ` + -Method Patch -headers $SPHeaders ` + -ContentType application/json ` + -Body ($itemObject | ConvertTo-Json) + $return = $listItem + } + + function Get-GCITSAccessToken { + $authority = "https://login.microsoftonline.com/$tenant_id" + $tokenEndpointUri = "$authority/oauth2/token" + $resource = "https://graph.microsoft.com" + $content = "grant_type=client_credentials&client_id=$client_id&client_secret=$client_secret&resource=$resource" + $graphBaseUri = "https://graph.microsoft.com/v1.0" + $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing + $access_token = $response.access_token + return $access_token + } + + function Get-GCITSMSGraphResource($Resource) { + $graphBaseUri = "https://graph.microsoft.com/v1.0" + $values = @() + $result = Invoke-RestMethod -Uri "$graphBaseUri/$resource" -Headers $headers + $values += $result.value + if ($result.'@odata.nextLink') { + do { + $result = Invoke-RestMethod -Uri $result.'@odata.nextLink' -Headers $headers + $values += $result.value + } while ($result.'@odata.nextLink') + } + return $values + } + + function Get-GCITSSharePointList($ListName) { + $list = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists?expand=columns&`$filter=displayName eq '$ListName'" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method GET + $list = $list.value + return $list + } + + $organisations = Get-GCITSITGlueItem -Resource organizations + + $access_token = Get-GCITSAccessToken + $SPHeaders = @{Authorization = "Bearer $access_token"} + + $list = Get-GCITSSharePointList -ListName $ListName + + if (!$list) { + Write-Output "List not found, creating List" + # Initiate Columns + $columnCollection = @() + $columnCollection += New-GCITSSharePointColumn -Name ShortName -Type text -Indexed $true + $columnCollection += New-GCITSSharePointColumn -Name ITGlueID -Type number -Indexed $true + + $List = New-GCITSSharePointList -Name $ListName -ColumnCollection $columnCollection + } + else { + Write-Output "List Exists, retrieving existing items" + $existingItems = Get-GCITSSharePointItem -ListId $list.id + Write-Output "Retrieved $($existingItems.count) existing items" + } + + foreach ($organisation in $organisations) { + Write-Output "Checking $($organisation.attributes.Name)" + $existingitem = $existingItems | Where-Object {$_.fields.ITGlueID -contains $organisation.id} + + # if there is no match in SharePoint for the existing org, create the item + if (!$existingitem) { + $item = @{ + "Title" = $organisation.attributes.name + "ShortName" = $organisation.attributes.'short-name' + "ITGlueID" = $organisation.id + } + Write-Output "Creating $($organisation.attributes.Name)" + New-GCITSSharePointListItem -ListId $list.id -ItemObject $item + } + else { + if ($existingitem.fields.Title -notcontains $organisation.attributes.name ` + -or $existingitem.fields.ShortName -notcontains $organisation.attributes.'short-name') { + Write-Output "Updating $($organisation.attributes.Name)" + $item = @{ + "Title" = $organisation.attributes.name + "ShortName" = $organisation.attributes.'short-name' + "ITGlueID" = $organisation.id + } + Set-GCITSSharePointListItem -ListId $list.Id -ItemId $existingitem.id -ItemObject $item + } + } + } + Write-Output "Cleaning up" + + foreach ($existingItem in $existingItems) { + + if ($organisations.id -notcontains $existingitem.fields.itglueid) { + Write-Output "Couldn't resolve, removing $($existingItem.fields.title)" + $removeItem = Remove-GCITSSharePointListItem -ListId $listId -ItemId $existingitem.id + } + + } +``` + +## How to run this script as an Azure Function + +You might have noticed that this script uses Write-Output instead of Write-Host for it's outputs. This is because it's intended to be run in an Azure Function. Write-Host doesn't display any output in Azure Functions, while Write-Output does. If you'd prefer not to set this up as an Azure Function, you can also run it as a scheduled task. + +### Setting up the Azure Function App + +1. Login to as a user with the ability to create resources in a subscription +2. [Create an Azure Function app by following this guide][6]. Or choose one you've already created. + +Note: If you are creating a new Azure Function App, you will be given the choice between a function app on an App Service Plan, or a Consumption Plan. + +Consumption plans are generally much cheaper, however the scripts that you run on them will automatically time out after around 5 minutes. For many organisations, this script will finish within the 5 minute time period. If you anticipate that you'll be running other long running scripts on this Azure Function App, you may decide to create one with a dedicated App Service Plan. Pricing for the underlying app service is shown to you when setting up the function app. + +3. Once you have created your Azure Function App, you can create an Azure Function for the script. Click the **+** icon + ![Create New Azure Function][7] +4. Click the toggle for **Enable experimental language support** + ![Enable Experimental Language Support][8] +5. Choose **PowerShell** under **Timer Triggered Functions** + ![Create Timer Trigger PowerShell Azure Function][9] +6. Give it a name like\***\* TT-SyncITGOrgSP \*\***and define a cron schedule for it to run on. In this example we want it to run every 12 hours: + +```cron + 0 0 */12 * * * +``` + +![New Timer Trigger Function Name And Schedule][10] + +7. Copy and paste the script from Visual Studio Code into your new Azure Function. +8. Press the **Save and Run** button. +9. You will see the progress of the script in the logs at the bottom of the page.![IT Glue SharePoint Sync Running In Azure Functions][11] + +### Secure your keys in the Azure Function + +Once you have confirmed that the script runs without issue, you can hide your IT Glue API Key and SharePoint Client Secret in the Applications Settings of the function. + +1. Click the name of your Azure Function app on the left menu. In this example our Function App is called **gcitsops**. Then click **Application Settings**.![Open Application Settings For Azure Function][12] +2. Scroll down to Application Settings and add two new settings called **ITGAPIKey** and **SharePointClientSecret**. Paste the IT Glue API Key and the SharePoint app's client secret into these new values. Scroll to the top of the page and click **Save**.![Store API Keys In Azure Function Application Settings][13] +3. Return to your function and replace the values for $key and $client_secret with** $env:ITGAPIKey** and **$env:SharePointClientSecret![Update Variables In Azure Function With Application Setting Environment Variables][14]** +4. Save and Run your function to confirm it's working. + +## View your new List on SharePoint + +The list that is created by this script can be found on your root SharePoint site (eg. tenantname.sharepoint.com). If it is not appearing under recent items on the left menu, you'll find it under **Site Contents**, **ITGlue Org Register**. + +![SharePoint List Of IT Glue Organisations][15] + +### About The Author + +![Elliot Munro][16] + +#### [ Elliot Munro ][17] + +Elliot Munro is an Office 365 MCSA from the Gold Coast, Australia supporting hundreds of small businesses with GCITS. If you have an Office 365 or Azure issue that you'd like us to take a look at (or have a request for a useful script) send Elliot an email at [elliot@gcits.com][18] + +[1]: https://gcits.com/knowledge-base/create-a-sharepoint-application-for-the-microsoft-graph-via-powershell/ +[2]: https://gcits.com/wp-content/uploads/RetrieveApplicationTenantIDClientIDAndSecretFromCSV-1030x261.png +[3]: https://gcits.com/wp-content/uploads/GetITGlueAPIKey-1030x453.png +[4]: https://gcits.com/wp-content/uploads/UpdateVariablesInVisualStudioCode-1030x384.png +[5]: https://gcits.com/wp-content/uploads/CreatingSharePointListOnFirstRun.png +[6]: https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-function-app-portal +[7]: https://gcits.com/wp-content/uploads/CreateNewAzureFunction.png +[8]: https://gcits.com/wp-content/uploads/EnableExperimentalLanguageSupport.png +[9]: https://gcits.com/wp-content/uploads/CreateTimerTriggerPowerShellAzureFunction.png +[10]: https://gcits.com/wp-content/uploads/NewTimerTriggerFunctionNameAndSchedule-1030x598.png +[11]: https://gcits.com/wp-content/uploads/ITGlueSharePointSyncRunningInAzureFunctions-1030x239.png +[12]: https://gcits.com/wp-content/uploads/OpenApplicationSettingsForAzureFunction-1030x687.png +[13]: https://gcits.com/wp-content/uploads/StoreAPIKeysInAzureFunctionApplicationSettings-1030x362.png +[14]: https://gcits.com/wp-content/uploads/UpdateVariablesInAzureFunctionWithApplicationSettingEnvironmentVariables-1030x243.png +[15]: https://gcits.com/wp-content/uploads/SharePointListOfITGlueOrganisations-1030x426.png +[16]: https://gcits.com/wp-content/uploads/AAEAAQAAAAAAAA2QAAAAJDNlN2NmM2Y4LTU5YWYtNGRiNC1hMmI2LTBhMzdhZDVmNWUzNA-80x80.jpg +[17]: https://gcits.com/author/elliotmunro/ +[18]: mailto:elliot%40gcits.com diff --git a/OrganisationSharePointSync/single.ps1 b/OrganisationSharePointSync/single.ps1 new file mode 100644 index 0000000..cdd61cf --- /dev/null +++ b/OrganisationSharePointSync/single.ps1 @@ -0,0 +1,223 @@ +<# +This script will sync IT Glue organisations with a SharePoint list in the root sharepoint site (eg tenantname.sharepoint.com). +The list is called 'ITGlue Org Register' +It should be run on a schedule to keep the SharePoint list up to date. +#> + +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$key = "EnterYourITGlueAPIKeyHere" +# Note that EU hosted IT Glue Tenants may need to update the below value to "https://api.eu.itglue.com" +$ITGbaseURI = "https://api.itglue.com" +$headers = @{ + "x-api-key" = $key +} +$client_id = "EnterYourSharePointAppClientIDHere" +$client_secret = "EnterYourSharePointAppClientSecretHere" +$tenant_id = "EnterYourTenantIDHere" +$graphBaseUri = "https://graph.microsoft.com/v1.0/" +$siteid = "root" +$ListName = "ITGlue Org Register" + +function Get-GCITSITGlueItem($Resource) { + $array = @() + $body = Invoke-RestMethod -Method get -Uri "$ITGbaseUri/$Resource" -Headers $headers -ContentType application/vnd.api+json + $array += $body.data + if ($body.links.next) { + do { + $body = Invoke-RestMethod -Method get -Uri $body.links.next -Headers $headers -ContentType application/vnd.api+json + $array += $body.data + } while ($body.links.next) + } + return $array +} + +function New-GCITSSharePointColumn($Name, $Type, $Indexed) { + $column = [ordered]@{ + name = $Name + indexed = $Indexed + $Type = @{} + } + return $column +} + +function New-GCITSSharePointList ($Name, $ColumnCollection) { + $list = @{ + displayName = $Name + columns = $columnCollection + } | Convertto-json -Depth 10 + + $newList = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method POST -Body $list + return $newList +} + +function Remove-GCITSSharePointList ($ListId) { + $removeList = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/$ListId" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method DELETE + return $removeList +} + +function Remove-GCITSSharePointListItem ($ListId, $ItemId) { + $removeItem = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/$ListId/items/$ItemId" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method DELETE + return $removeItem +} + +function New-GCITSSharePointListItem($ItemObject, $ListId) { + + $itemBody = @{ + fields = $ItemObject + } | ConvertTo-Json -Depth 10 + + $listItem = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/$listId/items" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method Post ` + -Body $itemBody +} + +function Get-GCITSSharePointItem($ListId, $ItemId) { + + if ($ItemId) { + $listItem = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items/$ItemId ` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value = $listItem + } + else { + $listItems = $null + $listItems = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items?expand=fields ` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value = @() + $value = $listItems.value + if ($listitems."@odata.nextLink") { + $nextLink = $true + } + if ($nextLink) { + do { + $listItems = Invoke-RestMethod -Uri $listitems."@odata.nextLink"` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value += $listItems.value + if (!$listitems."@odata.nextLink") { + $nextLink = $false + } + } until (!$nextLink) + } + } + return $value +} + +function Set-GCITSSharePointListItem($ListId, $ItemId, $ItemObject) { + $listItem = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items/$ItemId/fields ` + -Method Patch -headers $SPHeaders ` + -ContentType application/json ` + -Body ($itemObject | ConvertTo-Json) + $return = $listItem +} + +function Get-GCITSAccessToken { + $authority = "https://login.microsoftonline.com/$tenant_id" + $tokenEndpointUri = "$authority/oauth2/token" + $resource = "https://graph.microsoft.com" + $content = "grant_type=client_credentials&client_id=$client_id&client_secret=$client_secret&resource=$resource" + $graphBaseUri = "https://graph.microsoft.com/v1.0" + $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing + $access_token = $response.access_token + return $access_token +} + +function Get-GCITSMSGraphResource($Resource) { + $graphBaseUri = "https://graph.microsoft.com/v1.0" + $values = @() + $result = Invoke-RestMethod -Uri "$graphBaseUri/$resource" -Headers $headers + $values += $result.value + if ($result.'@odata.nextLink') { + do { + $result = Invoke-RestMethod -Uri $result.'@odata.nextLink' -Headers $headers + $values += $result.value + } while ($result.'@odata.nextLink') + } + return $values +} + +function Get-GCITSSharePointList($ListName) { + $list = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists?expand=columns&`$filter=displayName eq '$ListName'" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method GET + $list = $list.value + return $list +} + +$organisations = Get-GCITSITGlueItem -Resource organizations + +$access_token = Get-GCITSAccessToken +$SPHeaders = @{Authorization = "Bearer $access_token"} + +$list = Get-GCITSSharePointList -ListName $ListName + +if (!$list) { + Write-Output "List not found, creating List" + # Initiate Columns + $columnCollection = @() + $columnCollection += New-GCITSSharePointColumn -Name ShortName -Type text -Indexed $true + $columnCollection += New-GCITSSharePointColumn -Name ITGlueID -Type number -Indexed $true + + $List = New-GCITSSharePointList -Name $ListName -ColumnCollection $columnCollection +} +else { + Write-Output "List Exists, retrieving existing items" + $existingItems = Get-GCITSSharePointItem -ListId $list.id + Write-Output "Retrieved $($existingItems.count) existing items" +} + +foreach ($organisation in $organisations) { + Write-Output "Checking $($organisation.attributes.Name)" + $existingitem = $existingItems | Where-Object {$_.fields.ITGlueID -contains $organisation.id} + + # if there is no match in SharePoint for the existing org, create the item + if (!$existingitem) { + $item = @{ + "Title" = $organisation.attributes.name + "ShortName" = $organisation.attributes.'short-name' + "ITGlueID" = $organisation.id + } + Write-Output "Creating $($organisation.attributes.Name)" + New-GCITSSharePointListItem -ListId $list.id -ItemObject $item + } + else { + if ($existingitem.fields.Title -notcontains $organisation.attributes.name ` + -or $existingitem.fields.ShortName -notcontains $organisation.attributes.'short-name') { + Write-Output "Updating $($organisation.attributes.Name)" + $item = @{ + "Title" = $organisation.attributes.name + "ShortName" = $organisation.attributes.'short-name' + "ITGlueID" = $organisation.id + } + Set-GCITSSharePointListItem -ListId $list.Id -ItemId $existingitem.id -ItemObject $item + } + } +} +Write-Output "Cleaning up" + +foreach ($existingItem in $existingItems) { + + if ($organisations.id -notcontains $existingitem.fields.itglueid) { + Write-Output "Couldn't resolve, removing $($existingItem.fields.title)" + $removeItem = Remove-GCITSSharePointListItem -ListId $listId -ItemId $existingitem.id + } + +} \ No newline at end of file diff --git a/SyncMicrosoftSecureScoreReports/CreateApp.ps1 b/SyncMicrosoftSecureScoreReports/CreateApp.ps1 new file mode 100644 index 0000000..a838ffb --- /dev/null +++ b/SyncMicrosoftSecureScoreReports/CreateApp.ps1 @@ -0,0 +1,222 @@ +# This script needs to be run by an admin account in your Office 365 tenant +# This script will create an Azure AD app in your organisation with permission +# to access resources in yours and your customers' tenants. +# It will export information about the application to a CSV located at C:\temp\. +# The CSV will include the Client ID and Secret of the application, so keep it safe. + +# Confirm C:\temp exists +$temp = Test-Path -Path C:\temp +if ($temp) { + #Write-Host "Path exists" +} +else { + Write-Host "Creating Temp folder" + New-Item -Path C:\temp -ItemType directory +} + +$applicationName = "GCITS Secure Score Reader" + +# Change this to true if you would like to overwrite any existing applications with matching names. +$removeExistingAppWithSameName = $false +# Modify the homePage, appIdURI and logoutURI values to whatever valid URI you like. +# They don't need to be actual addresses, so feel free to make something up. +$homePage = "https://secure.gcits.com" +$appIdURI = "https://secure.gcits.com/$((New-Guid).ToString())" +$logoutURI = "https://portal.office.com" + +$URIForApplicationPermissionCall = "https://graph.microsoft.com/beta/security/secureScores" +$ApplicationPermissions = "SecurityEvents.Read.All Directory.Read.All Sites.Manage.All" + +Function Add-ResourcePermission($requiredAccess, $exposedPermissions, $requiredAccesses, $permissionType) { + foreach ($permission in $requiredAccesses.Trim().Split(" ")) { + $reqPermission = $null + $reqPermission = $exposedPermissions | Where-Object {$_.Value -contains $permission} + Write-Host "Collected information for $($reqPermission.Value) of type $permissionType" -ForegroundColor Green + $resourceAccess = New-Object Microsoft.Open.AzureAD.Model.ResourceAccess + $resourceAccess.Type = $permissionType + $resourceAccess.Id = $reqPermission.Id + $requiredAccess.ResourceAccess.Add($resourceAccess) + } +} + +Function Get-RequiredPermissions($requiredDelegatedPermissions, $requiredApplicationPermissions, $reqsp) { + $sp = $reqsp + $appid = $sp.AppId + $requiredAccess = New-Object Microsoft.Open.AzureAD.Model.RequiredResourceAccess + $requiredAccess.ResourceAppId = $appid + $requiredAccess.ResourceAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.ResourceAccess] + if ($requiredDelegatedPermissions) { + Add-ResourcePermission $requiredAccess -exposedPermissions $sp.Oauth2Permissions -requiredAccesses $requiredDelegatedPermissions -permissionType "Scope" + } + if ($requiredApplicationPermissions) { + Add-ResourcePermission $requiredAccess -exposedPermissions $sp.AppRoles -requiredAccesses $requiredApplicationPermissions -permissionType "Role" + } + return $requiredAccess +} +Function New-AppKey ($fromDate, $durationInYears, $pw) { + $endDate = $fromDate.AddYears($durationInYears) + $keyId = (New-Guid).ToString() + $key = New-Object Microsoft.Open.AzureAD.Model.PasswordCredential($null, $endDate, $keyId, $fromDate, $pw) + return $key +} + +Function Test-AppKey($fromDate, $durationInYears, $pw) { + + $testKey = New-AppKey -fromDate $fromDate -durationInYears $durationInYears -pw $pw + while ($testKey.Value -match "\+" -or $testKey.Value -match "/") { + Write-Host "Secret contains + or / and may not authenticate correctly. Regenerating..." -ForegroundColor Yellow + $pw = Initialize-AppKey + $testKey = New-AppKey -fromDate $fromDate -durationInYears $durationInYears -pw $pw + } + Write-Host "Secret doesn't contain + or /. Continuing..." -ForegroundColor Green + $key = $testKey + + return $key +} + +Function Initialize-AppKey { + $aesManaged = New-Object "System.Security.Cryptography.AesManaged" + $aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC + $aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::Zeros + $aesManaged.BlockSize = 128 + $aesManaged.KeySize = 256 + $aesManaged.GenerateKey() + return [System.Convert]::ToBase64String($aesManaged.Key) +} +function Confirm-MicrosoftGraphServicePrincipal { + $graphsp = Get-AzureADServicePrincipal -SearchString "Microsoft Graph" + if (!$graphsp) { + $graphsp = Get-AzureADServicePrincipal -SearchString "Microsoft.Azure.AgregatorService" + } + if (!$graphsp) { + Login-AzureRmAccount -Credential $credentials + New-AzureRmADServicePrincipal -ApplicationId "00000003-0000-0000-c000-000000000000" + $graphsp = Get-AzureADServicePrincipal -SearchString "Microsoft Graph" + } + return $graphsp +} +Write-Host "Connecting to Azure AD. The login window may appear behind Visual Studio Code." +Connect-AzureAD + +Write-Host "Creating application in tenant: $((Get-AzureADTenantDetail).displayName)" + +# Check for the Microsoft Graph Service Principal. If it doesn't exist already, create it. +$graphsp = Confirm-MicrosoftGraphServicePrincipal + +$existingapp = $null +$existingapp = get-azureadapplication -SearchString $applicationName +if ($existingapp -and $removeExistingAppWithSameName) { + Remove-Azureadapplication -ObjectId $existingApp.objectId +} + +# RSPS +$rsps = @() +if ($graphsp) { + $rsps += $graphsp + $tenant_id = (Get-AzureADTenantDetail).ObjectId + $tenantName = (Get-AzureADTenantDetail).DisplayName + + # Add Required Resources Access (Microsoft Graph) + $requiredResourcesAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.RequiredResourceAccess] + $microsoftGraphRequiredPermissions = Get-RequiredPermissions -reqsp $graphsp -requiredApplicationPermissions $ApplicationPermissions -requiredDelegatedPermissions $DelegatedPermissions + $requiredResourcesAccess.Add($microsoftGraphRequiredPermissions) + + # Get an application key + $pw = Initialize-AppKey + $fromDate = [System.DateTime]::Now + $appKey = Test-AppKey -fromDate $fromDate -durationInYears 99 -pw $pw + + Write-Host "Creating the AAD application $applicationName" -ForegroundColor Blue + $aadApplication = New-AzureADApplication -DisplayName $applicationName ` + -HomePage $homePage ` + -ReplyUrls $homePage ` + -IdentifierUris $appIdURI ` + -LogoutUrl $logoutURI ` + -RequiredResourceAccess $requiredResourcesAccess ` + -PasswordCredentials $appKey ` + -AvailableToOtherTenants $true + + # Creating the Service Principal for the application + $servicePrincipal = New-AzureADServicePrincipal -AppId $aadApplication.AppId + + Write-Host "Assigning Permissions" -ForegroundColor Yellow + + # Assign application permissions to the application + foreach ($app in $requiredResourcesAccess) { + $reqAppSP = $rsps | Where-Object {$_.appid -contains $app.ResourceAppId} + Write-Host "Assigning Application permissions for $($reqAppSP.displayName)" -ForegroundColor DarkYellow + foreach ($resource in $app.ResourceAccess) { + if ($resource.Type -match "Role") { + New-AzureADServiceAppRoleAssignment -ObjectId $serviceprincipal.ObjectId ` + -PrincipalId $serviceprincipal.ObjectId -ResourceId $reqAppSP.ObjectId -Id $resource.Id + } + } + } + + # This provides the application with access to your customer tenants. + $group = Get-AzureADGroup -Filter "displayName eq 'Adminagents'" + Add-AzureADGroupMember -ObjectId $group.ObjectId -RefObjectId $servicePrincipal.ObjectId + + Write-Host "App Created" -ForegroundColor Green + + # Define parameters for Microsoft Graph access token retrieval + $client_id = $aadApplication.AppId; + $client_secret = $appkey.Value + $tenant_id = (Get-AzureADTenantDetail).ObjectId + $resource = "https://graph.microsoft.com" + $authority = "https://login.microsoftonline.com/$tenant_id" + $tokenEndpointUri = "$authority/oauth2/token" + + # Get the access token using grant type password for Delegated Permissions or grant type client_credentials for Application Permissions + + $content = "grant_type=client_credentials&client_id=$client_id&client_secret=$client_secret&resource=$resource" + + # Try to execute the API call 6 times + + $Stoploop = $false + [int]$Retrycount = "0" + do { + try { + $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing + Write-Host "Retrieved Access Token" -ForegroundColor Green + # Assign access token + $access_token = $response.access_token + $body = $null + + $body = Invoke-RestMethod ` + -Uri $UriForApplicationPermissionCall ` + -Headers @{"Authorization" = "Bearer $access_token"} ` + -ContentType "application/json" ` + -Method GET ` + + Write-Host "Retrieved Graph content" -ForegroundColor Green + $Stoploop = $true + } + catch { + if ($Retrycount -gt 5) { + Write-Host "Could not get Graph content after 6 retries." -ForegroundColor Red + $Stoploop = $true + } + else { + Write-Host "Could not get Graph content. Retrying in 5 seconds..." -ForegroundColor DarkYellow + Start-Sleep -Seconds 5 + $Retrycount ++ + } + } + } + While ($Stoploop -eq $false) + + $appInfo = [pscustomobject][ordered]@{ + ApplicationName = $ApplicationName + TenantName = $tenantName + TenantId = $tenant_id + clientId = $client_id + clientSecret = $client_secret + ApplicationPermissions = $ApplicationPermissions + } + + $AppInfo | Export-Csv C:\temp\AzureADApp.csv -Append -NoTypeInformation +} +else { + Write-Host "Microsoft Graph Service Principal could not be found or created" -ForegroundColor Red +} \ No newline at end of file diff --git a/SyncMicrosoftSecureScoreReports/SyncSecureScoreScript.ps1 b/SyncMicrosoftSecureScoreReports/SyncSecureScoreScript.ps1 new file mode 100644 index 0000000..2ee7cb7 --- /dev/null +++ b/SyncMicrosoftSecureScoreReports/SyncSecureScoreScript.ps1 @@ -0,0 +1,712 @@ +# Azure AD App Details +$client_id = "EnterClientIDHere" +$client_secret = "EnterClientSecretHere=" +$tenant_id = "EnterTenantIDHere" +$ListName = "Office 365 - IT Glue match register" +$graphBaseUri = "https://graph.microsoft.com/v1.0/" +$siteid = "root" +$TableHeaderColour = "#00a1f1" + +# IT Glue Details +$ITGbaseURI = "https://api.itglue.com" +$ITGkey = "EnterITGlueIDHere" +$ITGheaders = @{"x-api-key" = $ITGkey} +$SecureScoreAssetName = "Microsoft Secure Score" + +function New-GCITSSharePointColumn($Name, $Type, $Indexed, $lookupListName, $lookupColumnPrimaryName, $lookupColumnName) { + + $column = [ordered]@{ + name = $Name + indexed = $Indexed + $Type = @{} + } + + if ($lookupListName -and $type -contains "lookup") { + $list = Get-GCITSSharePointList -ListName $lookupListName + if ($list) { + $column.lookup.listId = $list.id + $column.lookup.columnName = $lookupColumnName + } + } + return $column +} + +function New-GCITSSharePointList ($Name, $ColumnCollection) { + $list = @{ + displayName = $Name + columns = $columnCollection + } | Convertto-json -Depth 10 + + $newList = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method POST -Body $list + return $newList +} + + +function New-GCITSSharePointListItem($ItemObject, $ListId) { + $itemBody = @{ + fields = $ItemObject + } | ConvertTo-Json -Depth 10 + + $listItem = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/$listId/items" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method Post ` + -Body $itemBody +} + +function Remove-GCITSITGItem ($Resource,$existingItem){ + $item = Invoke-RestMethod -Method DELETE -Uri "$ITGbaseURI/$Resource/$($existingItem.id)" -Headers $ITGheaders +} + +function Get-GCITSSharePointListItem($ListId, $ItemId, $Query) { + + if ($ItemId) { + $listItem = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items/$ItemId ` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value = $listItem + } + elseif ($Query) { + $listItems = $null + $listItems = Invoke-RestMethod -Uri "$graphBaseUri/sites/$siteid/lists/$listId/items/?expand=fields&`$filter=$Query" ` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value = @() + $value = $listItems.value + if ($listitems."@odata.nextLink") { + $nextLink = $true + } + if ($nextLink) { + do { + $listItems = Invoke-RestMethod -Uri $listitems."@odata.nextLink"` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value += $listItems.value + if (!$listitems."@odata.nextLink") { + $nextLink = $false + } + } until (!$nextLink) + } + } + else { + $listItems = $null + $listItems = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items?expand=fields ` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value = @() + $value = $listItems.value + if ($listitems."@odata.nextLink") { + $nextLink = $true + } + if ($nextLink) { + do { + $listItems = Invoke-RestMethod -Uri $listitems."@odata.nextLink"` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value += $listItems.value + if (!$listitems."@odata.nextLink") { + $nextLink = $false + } + } until (!$nextLink) + } + } + return $value +} + +function Get-GCITSSharePointList($ListName) { + $list = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists?expand=columns&`$filter=displayName eq '$ListName'" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method GET + $list = $list.value + return $list +} +function Get-GCITSAccessToken($appCredential, $tenantId) { + $client_id = $appCredential.appID + $client_secret = $appCredential.secret + $tenant_id = $tenantid + $resource = "https://graph.microsoft.com" + $authority = "https://login.microsoftonline.com/$tenant_id" + $tokenEndpointUri = "$authority/oauth2/token" + $content = "grant_type=client_credentials&client_id=$client_id&client_secret=$client_secret&username=$UserForDelegatedPermissions&password=$Password&resource=$resource" + $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing + $access_token = $response.access_token + return $access_token +} + +function Get-GCITSMSGraphResource($Resource) { + $graphBaseUri = "https://graph.microsoft.com/beta" + $values = @() + $result = Invoke-RestMethod -Uri "$graphBaseUri/$resource" -Headers $headers + if ($result.value) { + $values += $result.value + if ($result.'@odata.nextLink') { + do { + $result = Invoke-RestMethod -Uri $result.'@odata.nextLink' -Headers $headers + $values += $result.value + } while ($result.'@odata.nextLink') + } + } + else { + $values = $result + } + return $values +} + +function New-GCITSITGTableFromArray($Array, $HeaderColour) { + # Remove any empty properties from table + $properties = $Array | get-member -ErrorAction SilentlyContinue | Where-Object {$_.memberType -contains "NoteProperty"} + foreach ($property in $properties) { + try { + $members = $Array.$($property.name) | Get-Member -ErrorAction Stop + } + catch { + $Array = $Array | Select-Object -Property * -ExcludeProperty $property.name + } + } + $Table = $Array | ConvertTo-Html -Fragment + if ($Table[2] -match "") { + $Table[2] = $Table[2] -replace "", "" + } + return $Table +} + +function Get-GCITSITGItem($Resource) { + $array = @() + + $body = Invoke-RestMethod -Method get -Uri "$ITGbaseUri/$Resource" -Headers $ITGheaders -ContentType application/vnd.api+json + $array += $body.data + Write-Host "Retrieved $($array.Count) IT Glue items" + + if ($body.links.next) { + do { + $body = Invoke-RestMethod -Method get -Uri $body.links.next -Headers $ITGheaders -ContentType application/vnd.api+json + $array += $body.data + Write-Host "Retrieved $($array.Count) IT Glue items" + } while ($body.links.next) + } + return $array +} + +function New-GCITSITGItem ($Resource, $Body) { + $item = Invoke-RestMethod -Method POST -ContentType application/vnd.api+json -Uri $ITGBaseURI/$Resource -Body $Body -Headers $ITGHeaders + return $item +} + +function Set-GCITSITGItem ($Resource, $existingItem, $Body) { + $updatedItem = Invoke-RestMethod -Method Patch -Uri "$ITGbaseUri/$Resource/$($existingItem.id)" -Headers $ITGheaders -ContentType application/vnd.api+json -Body $Body + return $updatedItem +} + +function Remove-GCITSITGItem ($Resource,$existingItem){ + $item = Invoke-RestMethod -Method DELETE -Uri "$ITGbaseURI/$Resource/$($existingItem.id)" -Headers $ITGheaders +} + +function New-GCITSITGSecureScoreFlexibleAsset { + + $body = @{ + data = @{ + type = "flexible_asset_types" + attributes = @{ + name = $SecureScoreAssetName + description = "Microsoft Secure Score Summary and Controls" + icon = "check" + "show-in-menu" = $true + } + relationships = @{ + "flexible-asset-fields" = @{ + data = @( + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 1 + name = "Overview" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 2 + name = "Identity Controls" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 3 + name = "Data Controls" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 4 + name = "Device Controls" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 5 + name = "Apps Controls" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 6 + name = "Infrastructure Controls" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 7 + name = "Tenant Name" + kind = "Text" + required = $false + "show-in-list" = $true + "use-for-title" = $true + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 8 + name = "Secure Score" + kind = "Number" + required = $false + "show-in-list" = $true + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 9 + name = "Identity Score" + kind = "Number" + required = $false + "show-in-list" = $true + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 10 + name = "Data Score" + kind = "Number" + required = $false + "show-in-list" = $true + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 11 + name = "Device Score" + kind = "Number" + required = $false + "show-in-list" = $true + } + }, @{ + type = "flexible_asset_fields" + attributes = @{ + order = 12 + name = "Apps Score" + kind = "Number" + required = $false + "show-in-list" = $true + } + }, @{ + type = "flexible_asset_fields" + attributes = @{ + order = 13 + name = "Infrastructure Score" + kind = "Number" + required = $false + "show-in-list" = $true + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 14 + name = "Tenant Id" + kind = "Text" + required = $false + "show-in-list" = $true + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 15 + name = "Default Domain" + kind = "Text" + required = $false + "show-in-list" = $true + } + } + ) + } + } + } + } + $flexibleAssetType = $body | ConvertTo-Json -Depth 10 + return $flexibleAssetType +} + +function New-GCITSITGSecureScoreAsset ($OrganizationId, $OverView, $identityControls, ` + $DataControls, $DeviceControls, $AppsControls, $InfrastructureControls, $tenantName, ` + $secureScore, $identityScore, $dataScore, $deviceScore, $appsScore, $infrastructureScore, ` + $tenantid, $defaultDomain) { + + $body = @{ + data = @{ + type = "flexible-assets" + attributes = @{ + "organization-id" = $OrganizationId + "flexible-asset-type-id" = $SecureScoreAssetType + traits = @{ + "overview" = $OverView + "identity-controls" = $identityControls + "data-controls" = $dataControls + "device-controls" = $deviceControls + "apps-controls" = $appsControls + "infrastructure-controls" = $InfrastructureControls + "tenant-name" = $tenantName + "secure-score" = [int]$secureScore + "identity-score" = [int]$identityScore + "data-score" = [int]$dataScore + "device-score" = [int]$deviceScore + "apps-score" = [int]$appsScore + "infrastructure-score" = [int]$infrastructureScore + "tenant-id" = $tenantId + "default-domain" = $defaultDomain + + } + } + } + } + + $tenantAsset = $body | ConvertTo-Json -Depth 10 + return $tenantAsset +} + + +$appCredential = @{ + appid = $client_id + secret = $client_secret +} +#<# +$access_token = Get-GCITSAccessToken -appCredential $appCredential -tenantId $tenant_id + +$Headers = @{Authorization = "Bearer $access_token"} +$SPHeaders = $Headers +$yourTenant = Get-GCITSMSGraphResource -Resource organization + +[array]$contracts = @{ + customerId = $yourTenant.id + defaultDomainName = ($yourtenant.verifiedDomains | Where-Object {$_.isdefault}).name + displayname = $yourTenant.displayName +} + + +$contracts += Get-GCITSMSGraphResource -Resource contracts + +$reports = @() +foreach ($contract in $contracts) { + Write-Output "Compiling Secure Score Report for $($contract.displayname)" + try { + $access_token = Get-GCITSAccessToken -appCredential $appCredential -tenantId $contract.customerid + $Headers = @{Authorization = "Bearer $access_token"} + [array]$scores = Get-GCITSMSGraphResource -Resource "security/securescores" + [array]$domains = (Get-GCITSMSGraphResource -Resource domains | Where-Object {$_.isverified}).id + $profiles = Get-GCITSMSGraphResource -Resource "security/secureScoreControlProfiles" + $collectionError = $false + } + catch { + $collectionError = $true + Write-Output "Could not retrieve scores for $($contract.displayname)" + } + + if ($scores -and !$collectionError) { + $latestScore = $scores[0] + $HTMLCollection = @() + + foreach ($control in $latestScore.controlScores) { + $controlReport = $null + $launchButton = $null + $controlProfile = $profiles | Where-Object {$_.id -contains $control.controlname} + $controlTitle = "

$($controlProfile.title)

" + [int]$controlScoreInt = $control.score + [int]$maxScoreInt = $controlProfile.maxScore + [string]$controlScore = "

Score: $controlScoreInt/$maxScoreInt

" + $assessment = "Assessment
$($control.description)
" + $remediation = "Remediation
$($controlprofile.remediation)
" + $remediationImpact = "Remediation Impact
$($controlprofile.remediationImpact)
" + if ($controlProfile.actionUrl) { + $launchButton = "Launch
" + } + $userImpact = "User Impact: $($controlprofile.userImpact)" + $implementationCost = "Implementation Cost: $($controlprofile.implementationCost)" + $threats = "Threats: $($controlprofile.threats -join ", ")" + $tier = "Tier: $($controlprofile.tier)" + $hr = "
" + [array]$controlElements = $assessment, $remediation, $remediationImpact + if ($launchButton) { + $controlElements += $launchButton + } + $controlReport = "
$($controlElements -join "

")

$($userImpact,$implementationCost,$threats,$tier,$hr -join "
")
" + $controlReport = "$($controlTitle)$($controlScore)

$($controlReport)" + $HTMLCollection += [pscustomobject]@{ + category = $controlProfile.controlCategory + controlReport = [string]$controlReport + rank = $controlProfile.rank + deprecated = $controlProfile.deprecated + score = $control.score + + } + } + + $HTMLCollection = $HTMLCollection | Where-Object {!$_.deprecated} | Sort-Object rank + + $identityControls = $HTMLCollection | Where-Object {$_.category -contains "Identity"} + $DataControls = $HTMLCollection | Where-Object {$_.category -contains "Data"} + $DeviceControls = $HTMLCollection | Where-Object {$_.category -contains "Device"} + $AppsControls = $HTMLCollection | Where-Object {$_.category -contains "Apps"} + $InfrastructureControls = $HTMLCollection | Where-Object {$_.category -contains "Infrastructure"} + + $identityScore = 0 + $dataScore = 0 + $deviceScore = 0 + $appsScore = 0 + $infrastructureScore = 0 + $identityControls | ForEach-Object {$identityScore += $_.score} + $DataControls | ForEach-Object {$dataScore += $_.score} + $DeviceControls | ForEach-Object {$deviceScore += $_.score} + $AppsControls | ForEach-Object {$appsScore += $_.score} + $InfrastructureControls | ForEach-Object {$infrastructureScore += $_.score} + + [int]$identityScore = $identityScore + [int]$dataScore = $dataScore + [int]$deviceScore = $deviceScore + [int]$appsScore = $appsScore + [int]$infrastructureScore = $infrastructureScore + $categoryScores = @() + + $allTenantScores = $latestScore.averageComparativeScores | Where-Object {$_.basis -contains "AllTenants"} + $similarCompanyScores = $latestScore.averageComparativeScores | Where-Object {$_.basis -contains "TotalSeats"} + + [int]$maxScore = $latestScore.maxScore + [int]$similarCompanyAverage = $similarCompanyScores.averageScore + [int]$globalAverage = $allTenantScores.averageScore + $minSeat = $similarCompanyScores.seatSizeRangeLowerValue + $maxSeat = $similarCompanyScores.seatSizeRangeUpperValue + + $categoryScores += [pscustomobject][ordered]@{ + Identity = "Tenant score: $($identityScore)" + Data = "Tenant score: $($dataScore)" + Device = "Tenant score: $($deviceScore)" + } + $categoryScores += [pscustomobject][ordered]@{ + Identity = "Global average: $($allTenantScores.identityScore)" + Data = "Global average: $($allTenantScores.dataScore)" + Device = "Global average: $($allTenantScores.deviceScore)" + } + $categoryScores += [pscustomobject][ordered]@{ + Identity = "Similar sized company average: $($similarCompanyScores.identityScore)" + Data = "Similar sized company average: $($similarCompanyScores.dataScore)" + Device = "Similar sized company average: $($similarCompanyScores.deviceScore)" + } + # Add Apps and Infrastructure scores to the overview table if they exist. + if ($allTenantScores) { + if (($allTenantScores | get-member).name -contains "appsScore") { + $categoryScores[0] | Add-Member Apps "Tenant score: $appsScore" + $categoryScores[1] | Add-Member Apps "Global average: $($allTenantScores.appsScore)" + $categoryScores[2] | Add-Member Apps "Similar sized company average: $($similarCompanyScores.appsScore)" + } + if (($allTenantScores | get-member).name -contains "infrastructureScore") { + $categoryScores[0] | Add-Member Infrastructure "Tenant score: $infrastructureScore" + $categoryScores[1] | Add-Member Infrastructure "Global average: $($allTenantScores.infrastructureScore)" + $categoryScores[2] | Add-Member Infrastructure "Similar sized company average: $($similarCompanyScores.infrastructureScore)" + } + } + + + [int]$currentScore = $($latestScore.currentScore) + $scoreheading = "

Microsoft Secure Score: $currentScore

" + $maxScoreTitle = "Maximum attainable score: $maxScore" + $similarCompanyTitle = "Similar sized company average ($minSeat - $maxSeat users): $similarCompanyAverage" + $globalAverageTitle = "Global average: $globalAverage" + $scoreBreakDownTitle = "Score Breakdown:" + $scoreBreakdownTable = New-GCITSITGTableFromArray -Array $categoryScores -HeaderColour $TableHeaderColour + + $subHeadings = "
$($maxScoreTitle,$similarCompanyTitle,$globalAverageTitle -join "
")
" + $overviewHTML = "$($scoreheading,$subHeadings,$scoreBreakDownTitle -join "

")$scoreBreakdownTable" + $identityHTML = $identityControls.controlReport -join "

" + $dataHTML = $dataControls.controlReport -join "

" + $deviceHTML = $deviceControls.controlReport -join "

" + $appsHTML = $appsControls.controlReport -join "

" + $infrastructureHTML = $infrastructureControls.controlReport -join "

" + + $reports += @{ + TenantId = $contract.customerId + TenantName = $contract.displayName + DefaultDomain = $contract.defaultDomainName + Domains = $domains + CurrentScore = [int]$latestScore.currentScore + IdentityScore = $identityScore + DataScore = $dataScore + DeviceScore = $deviceScore + AppsScore = $AppsScore + InstrastructureScore = $InstrastructureScore + Overview = $overviewHTML + IdentityControls = $identityHTML + DataControls = $dataHTML + DeviceControls = $deviceHTML + AppsControls = $appsHTML + InfrastructureControls = $infrastructureHTML + } + } +} + +Write-Host "Retrieving IT Glue Organisations" +$itgOrgs = Get-GCITSITGItem -Resource organizations + +Write-Host "Retrieving IT Glue Contacts" +$itgContacts = Get-GCITSITGItem -Resource contacts + +$itgEmailRecords = @() +foreach ($contact in $itgcontacts) { + foreach ($email in $contact.attributes."contact-emails") { + $hash = @{ + Domain = ($email.value -split "@")[1] + OrganizationID = $contact.attributes.'organization-id' + } + $object = New-Object psobject -Property $hash + $itgEmailRecords += $object + } +} +Write-Host "Matching reports with IT Glue Organisations" +$allMatches = @() +foreach ($report in $reports) { + foreach ($domain in $report.domains) { + $itgContactMatches = $itgEmailRecords | Where-Object {$_.domain -contains $domain} + foreach ($match in $itgContactMatches) { + $MatchingOrg = $itgOrgs | Where-Object {$_.id -eq $match.OrganizationID} + $MatchedReport = New-Object -TypeName psobject -Property $report + $MatchedReport | Add-Member OrganizationID $match.OrganizationID -Force + $MatchedReport | Add-Member OrganizationName $MatchingOrg.attributes.name + $MatchedReport | Add-Member Key "$($report.TenantId)-$($match.OrganizationID)" + $allMatches += $MatchedReport + } + } +} + +[array]$uniqueMatches = $allMatches | sort-object Key -Unique + +try { + $list = Get-GCITSSharePointList -ListName $ListName +} catch { + # if SharePoint access token is expired, get a new one. + $access_token = Get-GCITSAccessToken -appCredential $appCredential -tenantId $tenant_id + $Headers = @{Authorization = "Bearer $access_token"} + $SPHeaders = $Headers + $list = Get-GCITSSharePointList -ListName $ListName +} + + +if (!$list) { + Write-Host "SharePoint List not found, creating List" + # Initiate Columns + $columnCollection = @() + $columnCollection += New-GCITSSharePointColumn -Name ITGlueOrg -Type text -Indexed $true + $columnCollection += New-GCITSSharePointColumn -Name DisableSync -Type boolean -Indexed $true + $columnCollection += New-GCITSSharePointColumn -Name DefaultDomain -Type text -Indexed $true + $columnCollection += New-GCITSSharePointColumn -Name TenantId -Type text -Indexed $true + $columnCollection += New-GCITSSharePointColumn -Name ITGlueOrgId -Type number -Indexed $true + $columnCollection += New-GCITSSharePointColumn -Name Key -Type text -Indexed $true + $List = New-GCITSSharePointList -Name $ListName -ColumnCollection $columnCollection +} +else { + Write-Host "SharePoint List Exists, retrieving existing items" + $existingItems = Get-GCITSSharePointListItem -ListId $list.id + Write-Host "Retrieved $($existingItems.count) existing SharePoint items" +} + +# Check for existing Secure Score flexible asset +$SecureScoreAssetType = (Get-GCITSITGItem -Resource "flexible_asset_types?filter[name]=$SecureScoreAssetName").id + +if (!$SecureScoreAssetType) { + Write-Host "Creating IT Glue Flexible Asset for Microsoft Secure Score" + $flexibleAssetType = New-GCITSITGSecureScoreFlexibleAsset + $SecureScoreAssetType = (New-GCITSITGItem -Resource flexible_asset_types -Body $flexibleAssetType).data.id +} + + +foreach ($match in $uniqueMatches) { + $sharePointItem = $existingItems | where-object {$_.fields.key -eq $match.key} + if (!$sharePointItem) { + $sharePointItem = @{ + Title = $match.TenantName + ITGlueOrg = $match.OrganizationName + DisableSync = $false + DefaultDomain = $match.defaultDomain + TenantId = $match.TenantId + ITGlueOrgId = $match.OrganizationId + Key = $match.key + } + $sharePointItem = New-GCITSSharePointListItem -ListId $list.id -ItemObject $sharePointItem + } + Write-Host "Checking for existing report for $($sharePointItem.fields.Title) in $($sharePointItem.fields.ITGlueOrg)" + [array]$existingAssets = Get-GCITSITGItem -Resource "flexible_assets?filter[organization_id]=$($match.OrganizationID)&filter[flexible_asset_type_id]=$SecureScoreAssetType" + $matchingAsset = $existingAssets | Where-Object {$_.attributes.traits.'tenant-id' -contains $match.TenantId} + + + $newAsset = New-GCITSITGSecureScoreAsset -OrganizationId $match.organizationId -OverView $match.overview ` + -identityControls $match.identityControls -DataControls $match.dataControls -DeviceControls $match.deviceControls ` + -AppsControls $match.appsControls -InfrastructureControls $match.infrastructureControls -tenantName $match.tenantName ` + -secureScore $match.CurrentScore -identityScore $match.identityScore -dataScore $match.dataScore -deviceScore $match.deviceScore ` + -tenantid $match.tenantid -defaultDomain $match.defaultDomain -appsScore $match.AppsScore -infrastructureScore $match.InfrastructureScore + if (!$matchingAsset) { + if(!$sharePointItem.fields.DisableSync){ + Write-Host "No existing report found, creating new report" -foregroundcolor Green + New-GCITSITGItem -resource flexible_assets -body $newAsset + } + } + else { + if (!$sharePointItem.fields.DisableSync) { + Write-Host "Existing report found, updating report" -foregroundcolor Green + Set-GCITSITGItem -Resource flexible_assets -existingItem $matchingAsset -Body $newAsset + } + else { + Write-Host "Sync Disabled, removing report" + Remove-GCITSITGItem -Resource flexible_assets -ExistingItem $matchingAsset + } + } +} \ No newline at end of file diff --git a/SyncMicrosoftSecureScoreReports/sync-microsoft-secure-scores.md b/SyncMicrosoftSecureScoreReports/sync-microsoft-secure-scores.md new file mode 100644 index 0000000..f3ea4dc --- /dev/null +++ b/SyncMicrosoftSecureScoreReports/sync-microsoft-secure-scores.md @@ -0,0 +1,790 @@ +Sync Microsoft Secure Scores with IT Glue +========================================= + +The Microsoft Secure Score rates how well you’re leveraging security controls for Office 365, Microsoft 365 and Windows 10. While it’s a great tool for determining a Microsoft Cloud tenant’s security standing, checking it for each environment can be a manual task. + +This solution imports the secure score report of your own, and your customers’, tenants into IT Glue. You can use it to determine how to increase your customers’ secure scores, educate your customers on their current security standing, and provide justification for additional licensing like Microsoft 365 or Microsoft Cloud App Security. + +You can see a quick overview of a tenant’s secure score, including how they compare to the global average, and similar sized businesses. ![Microsoft Secure Score Overview in IT GLue](https://i2.wp.com/gcits.com/wp-content/uploads/SecureScoreOverview.png?resize=1030%2C782&ssl=1) + +![Microsoft Secure Score Overview in IT GLue](https://i2.wp.com/gcits.com/wp-content/uploads/SecureScoreOverview.png?resize=1030%2C782&ssl=1) + +The report also includes a list of the security controls that make up your customers scores. Each control includes a link to the relevant information or area to address it.![Enable MFA for Azure AD Privileged Roles](https://i0.wp.com/gcits.com/wp-content/uploads/EnableMFAforAzureADPrivilegedRoles.png?resize=1030%2C666&ssl=1) + +![Enable MFA for Azure AD Privileged Roles](https://i0.wp.com/gcits.com/wp-content/uploads/EnableMFAforAzureADPrivilegedRoles.png?resize=1030%2C666&ssl=1) + +Like our existing Office 365 script, this one will match tenants with IT Glue organisations automatically by comparing the verified domains in the Azure AD tenant against the domains in your IT Glue contacts’ email addresses.![IT Glue Microsoft Secure Score Data Sheet](https://i2.wp.com/gcits.com/wp-content/uploads/ITGlueMicrosoftSecureScoreDataSheet.png?resize=1030%2C387&ssl=1) + +![IT Glue Microsoft Secure Score Data Sheet](https://i2.wp.com/gcits.com/wp-content/uploads/ITGlueMicrosoftSecureScoreDataSheet.png?resize=1030%2C387&ssl=1) + +It’ll keep a record of these matches in a SharePoint list in your own root SharePoint site. If you’d prefer that a particular Microsoft Cloud tenant not sync with an IT Glue Organisation, you can disable it from syncing in the ‘**Office 365 – IT Glue match register**‘ SharePoint list.![Office 365 IT Glue Match Register](https://i0.wp.com/gcits.com/wp-content/uploads/Office365ITGlueMatchRegister-1.png?resize=1030%2C624&ssl=1) + +![Office 365 IT Glue Match Register](https://i0.wp.com/gcits.com/wp-content/uploads/Office365ITGlueMatchRegister-1.png?resize=1030%2C624&ssl=1) + +### Prerequisites + +* You’ll need an admin account in an IT Glue subscription with access to the API +* You’ll need an account with global admin permissions in your own tenant +* You’ll need to be a Microsoft Partner with access to your customers environments via delegated administration +* You’ll need to have the Azure Active Directory Powershell module installed. If you don’t have it installed, open PowerShell as an administrator and run the following cmdlet and accept the prompts. + + Install-Module AzureAD + + +How to sync Microsoft Secure Scores with IT Glue +------------------------------------------------ + +1. The first part of this solution is to create an app in your own Azure Active Directory environment with permission to access your own and your customers’ directories. [You can easily create this app using the script in this quick guide](https://gcits.com/knowledge-base/create-an-azure-ad-application-with-access-to-customer-tenants/). +2. Once you’ve created the app, retrieve the **client ID**, **client secret** and **tenant ID** from the exported CSV file at **C:\\temp\\AzureADApp.csv**.![Exported Info for Azure Ad App](https://i0.wp.com/gcits.com/wp-content/uploads/ExportedInfoAzureAdApp.png?resize=1030%2C260&ssl=1) + + ![Exported Info for Azure Ad App](https://i0.wp.com/gcits.com/wp-content/uploads/ExportedInfoAzureAdApp.png?resize=1030%2C260&ssl=1) + +3. Double click on the script at the bottom of this page to select it, then copy and paste it into Visual Studio Code and save it with a **.ps1** extension. Install the recommended PowerShell extension if you haven’t already. +4. Add the client ID, client secret and tenant ID from your new Azure AD application into the relevant variables under Azure Ad App Details![Azure AD App Details](https://i0.wp.com/gcits.com/wp-content/uploads/AzureADAppDetails.png?resize=1030%2C195&ssl=1) + + ![Azure AD App Details](https://i0.wp.com/gcits.com/wp-content/uploads/AzureADAppDetails.png?resize=1030%2C195&ssl=1) + +5. Retrieve or create an IT Glue API key from **Account**, **Settings**, **API Keys ![Get IT Glue API Key](https://i0.wp.com/gcits.com/wp-content/uploads/GetITGlueAPIKey.png?resize=1030%2C453&ssl=1) + + ![Get IT Glue API Key](https://i0.wp.com/gcits.com/wp-content/uploads/GetITGlueAPIKey.png?resize=1030%2C453&ssl=1)** +6. Add the IT Glue API key into the **$ITGkey** variable in the script. + ![Add IT Glue API Key Variable](https://i0.wp.com/gcits.com/wp-content/uploads/AddITGlueAPIKeyVariable.png?resize=1008%2C192&ssl=1) + + ![Add IT Glue API Key Variable](https://i0.wp.com/gcits.com/wp-content/uploads/AddITGlueAPIKeyVariable.png?resize=1008%2C192&ssl=1) + +7. If your tenant is hosted on IT Glues EU infrastructure, you may need to update the $ITGbaseURI value to: **https://api.eu.itglue.com** +8. Press **F5** to run the script and wait for it to complete. This is a long running script and may take a while if you have delegated access to many tenants. + ![Running Script To Sync Microsoft Secure Score And IT Glue](https://i0.wp.com/gcits.com/wp-content/uploads/RunningScriptToSyncMicrosoftSecureScoreAndITGlue.png?resize=1030%2C254&ssl=1) + + ![Running Script To Sync Microsoft Secure Score And IT Glue](https://i0.wp.com/gcits.com/wp-content/uploads/RunningScriptToSyncMicrosoftSecureScoreAndITGlue.png?resize=1030%2C254&ssl=1) + +9. You can confirm that it has created a list called **Office 365 – IT Glue match register** in SharePoint by navigating to the **Site Contents** of your root SharePoint site. Eg. https://yourtenant.sharepoint.com. If you don’t want a tenant to sync with a company in IT Glue, you can disable it here, then delete the record from IT Glue.![Office 365 IT Glue Match Register](https://i0.wp.com/gcits.com/wp-content/uploads/Office365ITGlueMatchRegister-1.png?resize=1030%2C624&ssl=1) + + ![Office 365 IT Glue Match Register](https://i0.wp.com/gcits.com/wp-content/uploads/Office365ITGlueMatchRegister-1.png?resize=1030%2C624&ssl=1) + +10. Add the new **Microsoft Secure Score** flexible asset type to your IT Glue menu under **Account**, **Customize Sidebar.**![Customise IT Glue Side Bar](https://i0.wp.com/gcits.com/wp-content/uploads/CustomiseITGlueSideBar.png?resize=461%2C181&ssl=1) + + +11. You can set this up as a scheduled task, or timer triggered PowerShell Azure Function. If you’re using an Azure Function, you can paste this script in as-is, though remember to replace the hardcoded client secret and API keys with Azure Function Environment variables. [See the bottom of this article for instructions on this](https://gcits.com/knowledge-base/sync-it-glue-organisations-with-a-sharepoint-list-via-powershell/). + +**Note:** In some tenants the combined identity, data, device, apps and infrastructure scores don’t add up to the total secure score. The discrepancy is coming from the data score, and may be due to a disabled or non-scored control contributing to the total. + +PowerShell Script to Sync Microsoft Secure Scores with IT Glue companies +------------------------------------------------------------------------ + +```powershell +\# Azure AD App Details +$client\_id = "EnterClientIDHere" +$client\_secret = "EnterClientSecretHere=" +$tenant\_id = "EnterTenantIDHere" +$ListName = "Office 365 - IT Glue match register" +$graphBaseUri = "https://graph.microsoft.com/v1.0/" +$siteid = "root" +$TableHeaderColour = "#00a1f1" +``` + +# IT Glue Details +```powershell +$ITGbaseURI = "https://api.itglue.com" +$ITGkey = "EnterITGlueIDHere" +$ITGheaders = @{"x-api-key" = $ITGkey} +$SecureScoreAssetName = "Microsoft Secure Score" + +function New-GCITSSharePointColumn($Name, $Type, $Indexed, $lookupListName, $lookupColumnPrimaryName, $lookupColumnName) { + + $column = \[ordered\]@{ + name = $Name + indexed = $Indexed + $Type = @{} + } + + if ($lookupListName -and $type -contains "lookup") { + $list = Get-GCITSSharePointList -ListName $lookupListName + if ($list) { + $column.lookup.listId = $list.id + $column.lookup.columnName = $lookupColumnName + } + } + return $column +} + +function New-GCITSSharePointList ($Name, $ColumnCollection) { + $list = @{ + displayName = $Name + columns = $columnCollection + } | Convertto-json -Depth 10 + + $newList = Invoke-RestMethod \` + -Uri "$graphBaseUri/sites/$siteid/lists/" \` + -Headers $SPHeaders \` + -ContentType "application/json" \` + -Method POST -Body $list + return $newList +} + + +function New-GCITSSharePointListItem($ItemObject, $ListId) { + $itemBody = @{ + fields = $ItemObject + } | ConvertTo-Json -Depth 10 + + $listItem = Invoke-RestMethod \` + -Uri "$graphBaseUri/sites/$siteid/lists/$listId/items" \` + -Headers $SPHeaders \` + -ContentType "application/json" \` + -Method Post \` + -Body $itemBody +} + +function Remove-GCITSITGItem ($Resource,$existingItem){ + $item = Invoke-RestMethod -Method DELETE -Uri "$ITGbaseURI/$Resource/$($existingItem.id)" -Headers $ITGheaders +} + +function Get-GCITSSharePointListItem($ListId, $ItemId, $Query) { + + if ($ItemId) { + $listItem = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items/$ItemId \` + -Method Get -headers $SPHeaders \` + -ContentType application/json + $value = $listItem + } + elseif ($Query) { + $listItems = $null + $listItems = Invoke-RestMethod -Uri "$graphBaseUri/sites/$siteid/lists/$listId/items/?expand=fields&\`$filter=$Query" \` + -Method Get -headers $SPHeaders \` + -ContentType application/json + $value = @() + $value = $listItems.value + if ($listitems."@odata.nextLink") { + $nextLink = $true + } + if ($nextLink) { + do { + $listItems = Invoke-RestMethod -Uri $listitems."@odata.nextLink"\` + -Method Get -headers $SPHeaders \` + -ContentType application/json + $value += $listItems.value + if (!$listitems."@odata.nextLink") { + $nextLink = $false + } + } until (!$nextLink) + } + } + else { + $listItems = $null + $listItems = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items?expand=fields \` + -Method Get -headers $SPHeaders \` + -ContentType application/json + $value = @() + $value = $listItems.value + if ($listitems."@odata.nextLink") { + $nextLink = $true + } + if ($nextLink) { + do { + $listItems = Invoke-RestMethod -Uri $listitems."@odata.nextLink"\` + -Method Get -headers $SPHeaders \` + -ContentType application/json + $value += $listItems.value + if (!$listitems."@odata.nextLink") { + $nextLink = $false + } + } until (!$nextLink) + } + } + return $value +} + +function Get-GCITSSharePointList($ListName) { + $list = Invoke-RestMethod \` + -Uri "$graphBaseUri/sites/$siteid/lists?expand=columns&\`$filter=displayName eq '$ListName'" \` + -Headers $SPHeaders \` + -ContentType "application/json" \` + -Method GET + $list = $list.value + return $list +} +function Get-GCITSAccessToken($appCredential, $tenantId) { + $client\_id = $appCredential.appID + $client\_secret = $appCredential.secret + $tenant\_id = $tenantid + $resource = "https://graph.microsoft.com" + $authority = "https://login.microsoftonline.com/$tenant\_id" + $tokenEndpointUri = "$authority/oauth2/token" + $content = "grant\_type=client\_credentials&client\_id=$client\_id&client\_secret=$client\_secret&username=$UserForDelegatedPermissions&password=$Password&resource=$resource" + $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing + $access\_token = $response.access\_token + return $access\_token +} + +function Get-GCITSMSGraphResource($Resource) { + $graphBaseUri = "https://graph.microsoft.com/beta" + $values = @() + $result = Invoke-RestMethod -Uri "$graphBaseUri/$resource" -Headers $headers + if ($result.value) { + $values += $result.value + if ($result.'@odata.nextLink') { + do { + $result = Invoke-RestMethod -Uri $result.'@odata.nextLink' -Headers $headers + $values += $result.value + } while ($result.'@odata.nextLink') + } + } + else { + $values = $result + } + return $values +} + +function New-GCITSITGTableFromArray($Array, $HeaderColour) { + # Remove any empty properties from table + $properties = $Array | get-member -ErrorAction SilentlyContinue | Where-Object {$\_.memberType -contains "NoteProperty"} + foreach ($property in $properties) { + try { + $members = $Array.$($property.name) | Get-Member -ErrorAction Stop + } + catch { + $Array = $Array | Select-Object -Property \* -ExcludeProperty $property.name + } + } + $Table = $Array | ConvertTo-Html -Fragment + if ($Table\[2\] -match "") { + $Table\[2\] = $Table\[2\] -replace "", "" + } + return $Table +} + +function Get-GCITSITGItem($Resource) { + $array = @() + + $body = Invoke-RestMethod -Method get -Uri "$ITGbaseUri/$Resource" -Headers $ITGheaders -ContentType application/vnd.api+json + $array += $body.data + Write-Host "Retrieved $($array.Count) IT Glue items" + + if ($body.links.next) { + do { + $body = Invoke-RestMethod -Method get -Uri $body.links.next -Headers $ITGheaders -ContentType application/vnd.api+json + $array += $body.data + Write-Host "Retrieved $($array.Count) IT Glue items" + } while ($body.links.next) + } + return $array +} + +function New-GCITSITGItem ($Resource, $Body) { + $item = Invoke-RestMethod -Method POST -ContentType application/vnd.api+json -Uri $ITGBaseURI/$Resource -Body $Body -Headers $ITGHeaders + return $item +} + +function Set-GCITSITGItem ($Resource, $existingItem, $Body) { + $updatedItem = Invoke-RestMethod -Method Patch -Uri "$ITGbaseUri/$Resource/$($existingItem.id)" -Headers $ITGheaders -ContentType application/vnd.api+json -Body $Body + return $updatedItem +} + +function Remove-GCITSITGItem ($Resource,$existingItem){ + $item = Invoke-RestMethod -Method DELETE -Uri "$ITGbaseURI/$Resource/$($existingItem.id)" -Headers $ITGheaders +} + +function New-GCITSITGSecureScoreFlexibleAsset { + + $body = @{ + data = @{ + type = "flexible\_asset\_types" + attributes = @{ + name = $SecureScoreAssetName + description = "Microsoft Secure Score Summary and Controls" + icon = "check" + "show-in-menu" = $true + } + relationships = @{ + "flexible-asset-fields" = @{ + data = @( + @{ + type = "flexible\_asset\_fields" + attributes = @{ + order = 1 + name = "Overview" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible\_asset\_fields" + attributes = @{ + order = 2 + name = "Identity Controls" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible\_asset\_fields" + attributes = @{ + order = 3 + name = "Data Controls" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible\_asset\_fields" + attributes = @{ + order = 4 + name = "Device Controls" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible\_asset\_fields" + attributes = @{ + order = 5 + name = "Apps Controls" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible\_asset\_fields" + attributes = @{ + order = 6 + name = "Infrastructure Controls" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible\_asset\_fields" + attributes = @{ + order = 7 + name = "Tenant Name" + kind = "Text" + required = $false + "show-in-list" = $true + "use-for-title" = $true + } + }, + @{ + type = "flexible\_asset\_fields" + attributes = @{ + order = 8 + name = "Secure Score" + kind = "Number" + required = $false + "show-in-list" = $true + } + }, + @{ + type = "flexible\_asset\_fields" + attributes = @{ + order = 9 + name = "Identity Score" + kind = "Number" + required = $false + "show-in-list" = $true + } + }, + @{ + type = "flexible\_asset\_fields" + attributes = @{ + order = 10 + name = "Data Score" + kind = "Number" + required = $false + "show-in-list" = $true + } + }, + @{ + type = "flexible\_asset\_fields" + attributes = @{ + order = 11 + name = "Device Score" + kind = "Number" + required = $false + "show-in-list" = $true + } + }, @{ + type = "flexible\_asset\_fields" + attributes = @{ + order = 12 + name = "Apps Score" + kind = "Number" + required = $false + "show-in-list" = $true + } + }, @{ + type = "flexible\_asset\_fields" + attributes = @{ + order = 13 + name = "Infrastructure Score" + kind = "Number" + required = $false + "show-in-list" = $true + } + }, + @{ + type = "flexible\_asset\_fields" + attributes = @{ + order = 14 + name = "Tenant Id" + kind = "Text" + required = $false + "show-in-list" = $true + } + }, + @{ + type = "flexible\_asset\_fields" + attributes = @{ + order = 15 + name = "Default Domain" + kind = "Text" + required = $false + "show-in-list" = $true + } + } + ) + } + } + } + } + $flexibleAssetType = $body | ConvertTo-Json -Depth 10 + return $flexibleAssetType +} + +function New-GCITSITGSecureScoreAsset ($OrganizationId, $OverView, $identityControls, \` + $DataControls, $DeviceControls, $AppsControls, $InfrastructureControls, $tenantName, \` + $secureScore, $identityScore, $dataScore, $deviceScore, $appsScore, $infrastructureScore, \` + $tenantid, $defaultDomain) { + + $body = @{ + data = @{ + type = "flexible-assets" + attributes = @{ + "organization-id" = $OrganizationId + "flexible-asset-type-id" = $SecureScoreAssetType + traits = @{ + "overview" = $OverView + "identity-controls" = $identityControls + "data-controls" = $dataControls + "device-controls" = $deviceControls + "apps-controls" = $appsControls + "infrastructure-controls" = $InfrastructureControls + "tenant-name" = $tenantName + "secure-score" = \[int\]$secureScore + "identity-score" = \[int\]$identityScore + "data-score" = \[int\]$dataScore + "device-score" = \[int\]$deviceScore + "apps-score" = \[int\]$appsScore + "infrastructure-score" = \[int\]$infrastructureScore + "tenant-id" = $tenantId + "default-domain" = $defaultDomain + + } + } + } + } + + $tenantAsset = $body | ConvertTo-Json -Depth 10 + return $tenantAsset +} + + +$appCredential = @{ + appid = $client\_id + secret = $client\_secret +} +#<# +$access\_token = Get-GCITSAccessToken -appCredential $appCredential -tenantId $tenant\_id + +$Headers = @{Authorization = "Bearer $access\_token"} +$SPHeaders = $Headers +$yourTenant = Get-GCITSMSGraphResource -Resource organization + +\[array\]$contracts = @{ + customerId = $yourTenant.id + defaultDomainName = ($yourtenant.verifiedDomains | Where-Object {$\_.isdefault}).name + displayname = $yourTenant.displayName +} + + +$contracts += Get-GCITSMSGraphResource -Resource contracts + +$reports = @() +foreach ($contract in $contracts) { + Write-Output "Compiling Secure Score Report for $($contract.displayname)" + try { + $access\_token = Get-GCITSAccessToken -appCredential $appCredential -tenantId $contract.customerid + $Headers = @{Authorization = "Bearer $access\_token"} + \[array\]$scores = Get-GCITSMSGraphResource -Resource "security/securescores" + \[array\]$domains = (Get-GCITSMSGraphResource -Resource domains | Where-Object {$\_.isverified}).id + $profiles = Get-GCITSMSGraphResource -Resource "security/secureScoreControlProfiles" + $collectionError = $false + } + catch { + $collectionError = $true + Write-Output "Could not retrieve scores for $($contract.displayname)" + } + + if ($scores -and !$collectionError) { + $latestScore = $scores\[0\] + $HTMLCollection = @() + + foreach ($control in $latestScore.controlScores) { + $controlReport = $null + $launchButton = $null + $controlProfile = $profiles | Where-Object {$\_.id -contains $control.controlname} + $controlTitle = "

$($controlProfile.title)

" + \[int\]$controlScoreInt = $control.score + \[int\]$maxScoreInt = $controlProfile.maxScore + \[string\]$controlScore = "

Score: $controlScoreInt/$maxScoreInt

" + $assessment = "Assessment
$($control.description)
" + $remediation = "Remediation
$($controlprofile.remediation)
" + $remediationImpact = "Remediation Impact
$($controlprofile.remediationImpact)
" + if ($controlProfile.actionUrl) { + $launchButton = "Launch
" + } + $userImpact = "User Impact: $($controlprofile.userImpact)" + $implementationCost = "Implementation Cost: $($controlprofile.implementationCost)" + $threats = "Threats: $($controlprofile.threats -join ", ")" + $tier = "Tier: $($controlprofile.tier)" + $hr = "
" + \[array\]$controlElements = $assessment, $remediation, $remediationImpact + if ($launchButton) { + $controlElements += $launchButton + } + $controlReport = "
$($controlElements -join "

")

$($userImpact,$implementationCost,$threats,$tier,$hr -join "
")
" + $controlReport = "$($controlTitle)$($controlScore)

$($controlReport)" + $HTMLCollection += \[pscustomobject\]@{ + category = $controlProfile.controlCategory + controlReport = \[string\]$controlReport + rank = $controlProfile.rank + deprecated = $controlProfile.deprecated + score = $control.score + + } + } + + $HTMLCollection = $HTMLCollection | Where-Object {!$\_.deprecated} | Sort-Object rank + + $identityControls = $HTMLCollection | Where-Object {$\_.category -contains "Identity"} + $DataControls = $HTMLCollection | Where-Object {$\_.category -contains "Data"} + $DeviceControls = $HTMLCollection | Where-Object {$\_.category -contains "Device"} + $AppsControls = $HTMLCollection | Where-Object {$\_.category -contains "Apps"} + $InfrastructureControls = $HTMLCollection | Where-Object {$\_.category -contains "Infrastructure"} + + $identityScore = 0 + $dataScore = 0 + $deviceScore = 0 + $appsScore = 0 + $infrastructureScore = 0 + $identityControls | ForEach-Object {$identityScore += $\_.score} + $DataControls | ForEach-Object {$dataScore += $\_.score} + $DeviceControls | ForEach-Object {$deviceScore += $\_.score} + $AppsControls | ForEach-Object {$appsScore += $\_.score} + $InfrastructureControls | ForEach-Object {$infrastructureScore += $\_.score} + + \[int\]$identityScore = $identityScore + \[int\]$dataScore = $dataScore + \[int\]$deviceScore = $deviceScore + \[int\]$appsScore = $appsScore + \[int\]$infrastructureScore = $infrastructureScore + $categoryScores = @() + + $allTenantScores = $latestScore.averageComparativeScores | Where-Object {$\_.basis -contains "AllTenants"} + $similarCompanyScores = $latestScore.averageComparativeScores | Where-Object {$\_.basis -contains "TotalSeats"} + + \[int\]$maxScore = $latestScore.maxScore + \[int\]$similarCompanyAverage = $similarCompanyScores.averageScore + \[int\]$globalAverage = $allTenantScores.averageScore + $minSeat = $similarCompanyScores.seatSizeRangeLowerValue + $maxSeat = $similarCompanyScores.seatSizeRangeUpperValue + + $categoryScores += \[pscustomobject\]\[ordered\]@{ + Identity = "Tenant score: $($identityScore)" + Data = "Tenant score: $($dataScore)" + Device = "Tenant score: $($deviceScore)" + } + $categoryScores += \[pscustomobject\]\[ordered\]@{ + Identity = "Global average: $($allTenantScores.identityScore)" + Data = "Global average: $($allTenantScores.dataScore)" + Device = "Global average: $($allTenantScores.deviceScore)" + } + $categoryScores += \[pscustomobject\]\[ordered\]@{ + Identity = "Similar sized company average: $($similarCompanyScores.identityScore)" + Data = "Similar sized company average: $($similarCompanyScores.dataScore)" + Device = "Similar sized company average: $($similarCompanyScores.deviceScore)" + } + # Add Apps and Infrastructure scores to the overview table if they exist. + if ($allTenantScores) { + if (($allTenantScores | get-member).name -contains "appsScore") { + $categoryScores\[0\] | Add-Member Apps "Tenant score: $appsScore" + $categoryScores\[1\] | Add-Member Apps "Global average: $($allTenantScores.appsScore)" + $categoryScores\[2\] | Add-Member Apps "Similar sized company average: $($similarCompanyScores.appsScore)" + } + if (($allTenantScores | get-member).name -contains "infrastructureScore") { + $categoryScores\[0\] | Add-Member Infrastructure "Tenant score: $infrastructureScore" + $categoryScores\[1\] | Add-Member Infrastructure "Global average: $($allTenantScores.infrastructureScore)" + $categoryScores\[2\] | Add-Member Infrastructure "Similar sized company average: $($similarCompanyScores.infrastructureScore)" + } + } + + + \[int\]$currentScore = $($latestScore.currentScore) + $scoreheading = "

Microsoft Secure Score: $currentScore

" + $maxScoreTitle = "Maximum attainable score: $maxScore" + $similarCompanyTitle = "Similar sized company average ($minSeat - $maxSeat users): $similarCompanyAverage" + $globalAverageTitle = "Global average: $globalAverage" + $scoreBreakDownTitle = "Score Breakdown:" + $scoreBreakdownTable = New-GCITSITGTableFromArray -Array $categoryScores -HeaderColour $TableHeaderColour + + $subHeadings = "
$($maxScoreTitle,$similarCompanyTitle,$globalAverageTitle -join "
")
" + $overviewHTML = "$($scoreheading,$subHeadings,$scoreBreakDownTitle -join "

")$scoreBreakdownTable" + $identityHTML = $identityControls.controlReport -join "

" + $dataHTML = $dataControls.controlReport -join "

" + $deviceHTML = $deviceControls.controlReport -join "

" + $appsHTML = $appsControls.controlReport -join "

" + $infrastructureHTML = $infrastructureControls.controlReport -join "

" + + $reports += @{ + TenantId = $contract.customerId + TenantName = $contract.displayName + DefaultDomain = $contract.defaultDomainName + Domains = $domains + CurrentScore = \[int\]$latestScore.currentScore + IdentityScore = $identityScore + DataScore = $dataScore + DeviceScore = $deviceScore + AppsScore = $AppsScore + InstrastructureScore = $InstrastructureScore + Overview = $overviewHTML + IdentityControls = $identityHTML + DataControls = $dataHTML + DeviceControls = $deviceHTML + AppsControls = $appsHTML + InfrastructureControls = $infrastructureHTML + } + } +} + +Write-Host "Retrieving IT Glue Organisations" +$itgOrgs = Get-GCITSITGItem -Resource organizations + +Write-Host "Retrieving IT Glue Contacts" +$itgContacts = Get-GCITSITGItem -Resource contacts + +$itgEmailRecords = @() +foreach ($contact in $itgcontacts) { + foreach ($email in $contact.attributes."contact-emails") { + $hash = @{ + Domain = ($email.value -split "@")\[1\] + OrganizationID = $contact.attributes.'organization-id' + } + $object = New-Object psobject -Property $hash + $itgEmailRecords += $object + } +} +Write-Host "Matching reports with IT Glue Organisations" +$allMatches = @() +foreach ($report in $reports) { + foreach ($domain in $report.domains) { + $itgContactMatches = $itgEmailRecords | Where-Object {$\_.domain -contains $domain} + foreach ($match in $itgContactMatches) { + $MatchingOrg = $itgOrgs | Where-Object {$\_.id -eq $match.OrganizationID} + $MatchedReport = New-Object -TypeName psobject -Property $report + $MatchedReport | Add-Member OrganizationID $match.OrganizationID -Force + $MatchedReport | Add-Member OrganizationName $MatchingOrg.attributes.name + $MatchedReport | Add-Member Key "$($report.TenantId)-$($match.OrganizationID)" + $allMatches += $MatchedReport + } + } +} + +\[array\]$uniqueMatches = $allMatches | sort-object Key -Unique + +try { + $list = Get-GCITSSharePointList -ListName $ListName +} catch { + # if SharePoint access token is expired, get a new one. + $access\_token = Get-GCITSAccessToken -appCredential $appCredential -tenantId $tenant\_id + $Headers = @{Authorization = "Bearer $access\_token"} + $SPHeaders = $Headers + $list = Get-GCITSSharePointList -ListName $ListName +} + + +if (!$list) { + Write-Host "SharePoint List not found, creating List" + # Initiate Columns + $columnCollection = @() + $columnCollection += New-GCITSSharePointColumn -Name ITGlueOrg -Type text -Indexed $true + $columnCollection += New-GCITSSharePointColumn -Name DisableSync -Type boolean -Indexed $true + $columnCollection += New-GCITSSharePointColumn -Name DefaultDomain -Type text -Indexed $true + $columnCollection += New-GCITSSharePointColumn -Name TenantId -Type text -Indexed $true + $columnCollection += New-GCITSSharePointColumn -Name ITGlueOrgId -Type number -Indexed $true + $columnCollection += New-GCITSSharePointColumn -Name Key -Type text -Indexed $true + $List = New-GCITSSharePointList -Name $ListName -ColumnCollection $columnCollection +} +else { + Write-Host "SharePoint List Exists, retrieving existing items" + $existingItems = Get-GCITSSharePointListItem -ListId $list.id + Write-Host "Retrieved $($existingItems.count) existing SharePoint items" +} + +# Check for existing Secure Score flexible asset +$SecureScoreAssetType = (Get-GCITSITGItem -Resource "flexible\_asset\_types?filter\[name\]=$SecureScoreAssetName").id + +if (!$SecureScoreAssetType) { + Write-Host "Creating IT Glue Flexible Asset for Microsoft Secure Score" + $flexibleAssetType = New-GCITSITGSecureScoreFlexibleAsset + $SecureScoreAssetType = (New-GCITSITGItem -Resource flexible\_asset\_types -Body $flexibleAssetType).data.id +} + + +foreach ($match in $uniqueMatches) { + $sharePointItem = $existingItems | where-object {$\_.fields.key -eq $match.key} + if (!$sharePointItem) { + $sharePointItem = @{ + Title = $match.TenantName + ITGlueOrg = $match.OrganizationName + DisableSync = $false + DefaultDomain = $match.defaultDomain + TenantId = $match.TenantId + ITGlueOrgId = $match.OrganizationId + Key = $match.key + } + $sharePointItem = New-GCITSSharePointListItem -ListId $list.id -ItemObject $sharePointItem + } + Write-Host "Checking for existing report for $($sharePointItem.fields.Title) in $($sharePointItem.fields.ITGlueOrg)" + \[array\]$existingAssets = Get-GCITSITGItem -Resource "flexible\_assets?filter\[organization\_id\]=$($match.OrganizationID)&filter\[flexible\_asset\_type\_id\]=$SecureScoreAssetType" + $matchingAsset = $existingAssets | Where-Object {$\_.attributes.traits.'tenant-id' -contains $match.TenantId} + + + $newAsset = New-GCITSITGSecureScoreAsset -OrganizationId $match.organizationId -OverView $match.overview \` + -identityControls $match.identityControls -DataControls $match.dataControls -DeviceControls $match.deviceControls \` + -AppsControls $match.appsControls -InfrastructureControls $match.infrastructureControls -tenantName $match.tenantName \` + -secureScore $match.CurrentScore -identityScore $match.identityScore -dataScore $match.dataScore -deviceScore $match.deviceScore \` + -tenantid $match.tenantid -defaultDomain $match.defaultDomain -appsScore $match.AppsScore -infrastructureScore $match.InfrastructureScore + if (!$matchingAsset) { + if(!$sharePointItem.fields.DisableSync){ + Write-Host "No existing report found, creating new report" -foregroundcolor Green + New-GCITSITGItem -resource flexible\_assets -body $newAsset + } + } + else { + if (!$sharePointItem.fields.DisableSync) { + Write-Host "Existing report found, updating report" -foregroundcolor Green + Set-GCITSITGItem -Resource flexible\_assets -existingItem $matchingAsset -Body $newAsset + } + else { + Write-Host "Sync Disabled, removing report" + Remove-GCITSITGItem -Resource flexible\_assets -ExistingItem $matchingAsset + } + } +} +``` diff --git a/UniFiSync/README.md b/UniFiSync/README.md new file mode 100644 index 0000000..cfc1039 --- /dev/null +++ b/UniFiSync/README.md @@ -0,0 +1,1056 @@ +[Source](https://gcits.com/knowledge-base/sync-unifi-sites-with-it-glue/ "Permalink to Sync UniFi Sites with IT Glue") + +# Sync UniFi Sites with IT Glue + +![Sync UniFi Information with IT Glue][1] + +If you're managing your clients UniFi devices in one or more UniFi Controllers, you can use this guide to sync important info from those devices with IT Glue. + +This can give your help desk staff quick access to important information on UniFi Devices, Wifi Networks, LAN/WAN details, port forwarding, VPNs and more. + +Here's an example UniFi site in IT Glue: + +![UniFi Site Synced With IT Glue][2] + +This guide uses your SharePoint site to store information about your IT Glue organisations, as well as help match up your UniFi sites with the relevant customer. + +### Prerequisites + +To set this up, you'll need: + +- Admin access to your Office 365 tenant. +- Admin access to IT Glue with the ability to generate an API key +- Admin access to a UniFi Controller +- Access to an Azure Subscription to run these scripts on a schedule using Azure Functions. + +## Overview of the syncing solution + +To get this working, we'll need to run four scripts. + +- [The first script][3] will create an application in your Azure Active Directory Tenant with permission to work with your SharePoint sites. +- [The second script][4] will create a SharePoint List in your top level SharePoint site with a list of all IT Glue organisations, their IDs and short names. +- The third script is included on this page and will help match your UniFi sites with your IT Glue organisations using another SharePoint List +- The fourth script is also included below and will create a flexible asset type in IT Glue for UniFi site information and sync your UniFi site info and configurations with the relevant IT Glue organisations. + +Note: if you're running multiple controllers, this solution will still work. You'll just need to run separate instances of the third and fourth scripts for each controller. + +## Step 1: Create a SharePoint Application in your Azure AD tenant + +![Create a SharePoint Azure AD Application via PowerShell][5] + +[Follow this guide][3] to create a SharePoint Application in your Azure AD tenant. The application will be created with the **Sites.Manage.All** permission which will, among other things, allow it to create and edit lists. + +Since we'll refer to this script in multiple guides, it gets its own article. + +The Client ID and Secret and for this application will be exported to a CSV file under **C:tempAzureAdApps.csv**. Once you have retrieved these values, you should delete this CSV then move on to step 2. + +## Step 2: Sync your IT Glue organisations with a SharePoint list + +![Create a SharePoint List containing all IT Glue Organisations][6] + +[Follow this guide][4] to sync your IT Glue organisations with a SharePoint list in your root SharePoint site. We can use this list as a reference for matching configurations and flexible assets with the correct IT Glue organisation. + +We'll also refer to this script in multiple guides, so it gets its own article too. + +Once you have run this script and set it up on a schedule with either an Azure Function or scheduled task, you can move on to Step 3. + +## Step 3: Match IT Glue organisations with UniFi sites + +![IT Glue UniFi Match Register][7] + +The script below will connect to IT Glue, your Unifi Controller and SharePoint and attempt to match your Unifi Sites with the appropriate IT Glue Organisation. It will then add the site and its possible IT Glue organisation match to a SharePoint List called '**Unifi – IT Glue Match Register**' within your root SharePoint site (eg. yourtenantname.sharepoint.com). + +It will first attempt to match the name of the site with the name of the customer. If it can't find a match, it will try to match the MAC addresses from the configurations in IT Glue against any of the MAC addresses of the most recent 200 devices that have connected to your Unifi site. If it still cannot find a match, it will add the site to the SharePoint list where you can manually match it. + +### How to match your IT Glue organisations with your UniFi sites + +1. Double click the below script to select it, then copy and paste it into Visual Studio Code +2. Save it with a **.ps1** extension +3. Edit the variables with place holders, pasting in the following info into the following variables: + +- **$key**: enter your IT Glue API Key +- **$client_id**: enter your SharePoint App's Client ID +- **$client_secret**: enter your SharePoint App's Client Secret +- **$tenant_id**: enter your organisation's Tenant ID +- **$UniFiBaseUri**: enter the base url of your UniFi controller with the port number (eg. https://unifi.yourdomain.com:8443) +- **$UniFiCredentials**: enter your UniFi controllers username and password into the username and password properties of this PowerShell object. + +4. Press F5 to run the script and wait for it to complete. + ![Matching Organisations IT Glue And Unifi In SharePoint List][8] +5. Once you have run the script, navigate to your root SharePoint site (eg tenantname.sharepoint.com) and locate your match register in **Site Contents**.![Locate Match Register In SharePoint][9] +6. Confirm that the sites are matched with the correct IT Glue organisation. You can edit the list items and use the dropdown list to manually match any that were incorrect or missing.![Select Correct IT Glue Organisation For UniFi Site In SharePoint][10] +7. Once you are happy that your UniFi sites are matched with the correct IT Glue organisations, you can set this script up to run regularly in an Azure Function or scheduled task. Remember to replace the secrets in the scripts with Azure Function environment variables as demonstrated in Step 2. + +### Script to match UniFi sites with IT Glue organisations in a SharePoint List + +```powershell + <# This script will connect to IT Glue, your Unifi Controller and SharePoint and attempt to match your Unifi Sites with the appropriate IT Glue Organisation. It will then add the site and its possible IT Glue organisation match to a SharePoint List called 'Unifi - IT Glue Site Match Register' within your root SharePoint site (eg. yourtenantname.sharepoint.com). It will first attempt to match the name of the site with the name of the customer. If it can't find a match, it will try to match the MAC addresses from the configurations in IT Glue against any of the MAC addresses of the most recent 200 devices that have connected to your Unifi site. If it still cannot find a match, it will add the site to the SharePoint list where you can manually match it. #> + + # IT Glue Details + $ITGbaseURI = "https://api.itglue.com" + $key = "EnterITGlueAPIKeyHere" + + + # SharePoint Details + $client_id = "EnterSharePointClientIDHere" + $client_secret = "EnterSharePointClientSecretHere" + $tenant_id = "EnterAzureADTenantIDHere" + $ITGOrgRegisterListName = "ITGlue Org Register" + $ListName = "Unifi - IT Glue match register" + $graphBaseUri = "https://graph.microsoft.com/v1.0/" + $siteid = "root" + + # UniFi Details + $UnifiBaseUri = "https://unifi.yourdomain.com:8443" + $UniFiCredentials = @{ + username = "EnterUnifiAdminUserNameHere" + password = "EnterUnifiAdminPasswordHere" + remember = $true + } | ConvertTo-Json + + $UnifiBaseUri = "$UnifiBaseUri/api" + + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + + function New-GCITSSharePointColumn($Name, $Type, $Indexed, $lookupListName, $lookupColumnPrimaryName, $lookupColumnName) { + + $column = [ordered]@{ + name = $Name + indexed = $Indexed + $Type = @{} + } + + if ($lookupListName -and $type -contains "lookup") { + $list = Get-GCITSSharePointList -ListName $lookupListName + if ($list) { + $column.lookup.listId = $list.id + $column.lookup.columnName = $lookupColumnName + } + } + return $column + } + + function New-GCITSSharePointList ($Name, $ColumnCollection) { + $list = @{ + displayName = $Name + columns = $columnCollection + } | Convertto-json -Depth 10 + + $newList = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method POST -Body $list + return $newList + } + + function Remove-GCITSSharePointList ($ListId) { + $removeList = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/$ListId" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method DELETE + return $removeList + } + + function Remove-GCITSSharePointListItem ($ListId, $ItemId) { + $removeItem = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/$ListId/items/$ItemId" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method DELETE + return $removeItem + } + + function New-GCITSSharePointListItem($ItemObject, $ListId) { + + $itemBody = @{ + fields = $ItemObject + } | ConvertTo-Json -Depth 10 + + $listItem = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/$listId/items" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method Post ` + -Body $itemBody + } + + function Get-GCITSSharePointListItem($ListId, $ItemId, $Query) { + + if ($ItemId) { + $listItem = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items/$ItemId ` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value = $listItem + } + elseif ($Query) { + $listItems = $null + $listItems = Invoke-RestMethod -Uri "$graphBaseUri/sites/$siteid/lists/$listId/items/?expand=fields&`$filter=$Query" ` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value = @() + $value = $listItems.value + if ($listitems."@odata.nextLink") { + $nextLink = $true + } + if ($nextLink) { + do { + $listItems = Invoke-RestMethod -Uri $listitems."@odata.nextLink"` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value += $listItems.value + if (!$listitems."@odata.nextLink") { + $nextLink = $false + } + } until (!$nextLink) + } + } + else { + $listItems = $null + $listItems = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items?expand=fields ` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value = @() + $value = $listItems.value + if ($listitems."@odata.nextLink") { + $nextLink = $true + } + if ($nextLink) { + do { + $listItems = Invoke-RestMethod -Uri $listitems."@odata.nextLink"` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value += $listItems.value + if (!$listitems."@odata.nextLink") { + $nextLink = $false + } + } until (!$nextLink) + } + } + return $value + } + + function Set-GCITSSharePointListItem($ListId, $ItemId, $ItemObject) { + $listItem = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items/$ItemId/fields ` + -Method Patch -headers $SPHeaders ` + -ContentType application/json ` + -Body ($itemObject | ConvertTo-Json) + $return = $listItem + } + + function Get-GCITSAccessToken { + $authority = "https://login.microsoftonline.com/$tenant_id" + $tokenEndpointUri = "$authority/oauth2/token" + $resource = "https://graph.microsoft.com" + $content = "grant_type=client_credentials&client_id=$client_id&client_secret=$client_secret&resource=$resource" + $graphBaseUri = "https://graph.microsoft.com/v1.0" + $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing + $access_token = $response.access_token + return $access_token + } + function Get-GCITSSharePointList($ListName) { + $list = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists?expand=columns&`$filter=displayName eq '$ListName'" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method GET + $list = $list.value + return $list + } + + function Get-ITGlueItem($Resource) { + $array = @() + + $body = Invoke-RestMethod -Method get -Uri "$ITGbaseUri/$Resource" -Headers $headers -ContentType application/vnd.api+json + $array += $body.data + Write-Host "Retrieved $($array.Count) IT Glue items" + + if ($body.links.next) { + do { + $body = Invoke-RestMethod -Method get -Uri $body.links.next -Headers $headers -ContentType application/vnd.api+json + $array += $body.data + Write-Host "Retrieved $($array.Count) IT Glue items" + } while ($body.links.next) + } + return $array + } + + $access_token = Get-GCITSAccessToken + $SPHeaders = @{Authorization = "Bearer $access_token"} + + $list = Get-GCITSSharePointList -ListName $ListName + $ITGOrgRegisterList = Get-GCITSSharePointList -ListName $ITGOrgRegisterListName + + if (!$list -and $ITGOrgRegisterList) {g + Write-Output "List not found, creating List" + # Initiate Columns + $columnCollection = @() + $columnCollection += New-GCITSSharePointColumn -Name UnifiSiteName -Type text -Indexed $true + $columnCollection += New-GCITSSharePointColumn -Name ITGlue -Type lookup -Indexed $true -lookupListName $ITGOrgRegisterListName -lookupColumnName Title + $List = New-GCITSSharePointList -Name $ListName -ColumnCollection $columnCollection + } + else { + Write-Output "SharePoint list exists" + } + + $headers = @{ + "x-api-key" = $key + } + + if ($ITGOrgRegisterList) { + Write-Host "Getting IT Glue Configurations" + #$configurations = Get-ITGlueItem -Resource configurations + Write-Host "Getting IT Glue Organisations" + #$orgs = Get-ITGlueItem -Resource organizations + + # Connect to UniFi Controller + Invoke-RestMethod -Uri $UnifiBaseUri/login -Method POST -Body $uniFiCredentials -SessionVariable websession + + # Get Sites + $sites = (Invoke-RestMethod -Uri "$UnifiBaseUri/self/sites" -WebSession $websession).data + + foreach ($site in $sites) { + Write-Host "Matching $($site.desc)" + # Check Name of site against IT Glue organisations. if no primary Match, check the devices themselves. + $primarymatch = $orgs | Where-Object {$_.attributes.name -match $site.desc} | Select-Object -First 1 + if ($primarymatch) { + Write-Host "Matched $($site.desc) (UniFi) with $($primarymatch.attributes.name) (IT Glue)" -ForegroundColor Green + } + else { + Write-Host "Couldn't match by name. Attempting to match client devices from $($site.desc) by mac address with IT Glue Configurations" -ForegroundColor Yellow + $matches = @() + $clientDevices = Invoke-RestMethod -Uri "$UnifiBaseUri/s/$($site.name)/rest/user" -WebSession $websession + + foreach ($address in ($clientDevices.data.mac | Select-Object -first 200)) { + $address = $address -replace ":", "-" + $matches += $configurations | Where-Object {$_.attributes.'mac-address' -contains $address} + } + + $primaryMatch = $matches.attributes | group-object organization-id | Select-Object -First 1 + + if ($primaryMatch) { + Write-Host "Matching $($site.desc) (UniFi) with $($primaryMatch.group[0].'organization-name') (IT Glue) with match count of $($primaryMatch.group.count)" -ForegroundColor Green + $primaryMatch = $primarymatch.group[0] + } + } + + # If a match could be found, Create the SharePoint List Item + if ($primaryMatch) { + $ITGlueID = $primaryMatch.'organization-id' + if (!$ITGlueID) { + $ITGlueID = $primarymatch.id + } + $matchingOrg = $orgs | Where-Object {$_.id -eq $ITGlueID} + $query = Get-GCITSSharePointListItem -ListId $ITGOrgRegisterList.id -Query "fields/ITGlueID eq '$itglueid'" + if ($query) { + $existingMatch = Get-GCITSSharePointListItem -ListId $List.id -Query "fields/UnifiSiteName eq '$($site.name)'" + if (!$existingMatch) { + $NewObject = @{ + Title = $site.desc + UnifiSiteName = $site.name + ITGlueLookupId = $query.id + } + New-GCITSSharePointListItem -ListId $list.id -ItemObject $NewObject + } + else { + Write-Host "$($site.desc) is added to the match register already" + } + } + } + else { + Write-Host "Couldn't match $($site.desc)" -ForegroundColor Yellow + $existingMatch = Get-GCITSSharePointListItem -ListId $List.id -Query "fields/UnifiSiteName eq '$($site.name)'" + if (!$existingMatch) { + $NewObject = @{ + Title = $site.desc + UnifiSiteName = $site.name + } + New-GCITSSharePointListItem -ListId $list.id -ItemObject $NewObject + } + else { + Write-Host "$($site.desc) is added to the match register already" + } + } + Write-Host "`n" + } + } + else { + Write-Host "Couldn't find list with name: $ITGOrgRegisterListName" + } +``` + +## Step 4: Sync UniFi devices and site information with IT Glue + +![GCITS UniFi Site In IT Glue][11] + +This script will connect to IT Glue, your UniFi Controller and SharePoint and create configurations for each UniFi device as well as a site summary in a new flexible asset type. + +The new Flexible Asset Type will hold the following information from your UniFi Devices: + +- The site name +- Devices +- Basic LAN Information +- Basic WAN Information +- Basic WIFI Network Information +- Port Forwarding +- Basic Site to Site VPN Information +- Basic Remote User VPN Information +- The 10 most recent alarms for the site + +### Want more Unifi device or site information in IT Glue? + +If you are handy with PowerShell, you can configure the script to add more information in to the fields in the flexible asset type, or configure the Asset Type to take more properties. + +This script will be available in our GitHub repo at shortly. + +If you'd prefer not to sync the UniFi devices as configurations, just comment out the section between '**# Start UniFi configuration sync**' and '**# End UniFi configuration sync**'. The script will then only sync the site summary with the new flexible asset type. + +### How to sync UniFi devices and site information with IT Glue + +1. Double click the below script to select it, then copy and paste it into Visual Studio Code +2. Save it with a **.ps1** extension +3. Edit the variables with place holders, pasting the relevant info into the following variables: + +- **$key**: enter your IT Glue API Key +- **$client_id**: enter your SharePoint app's Client ID +- **$client_secret**: enter your SharePoint app's Client Secret +- **$tenant_id**: enter your organisation's Azure AD Tenant ID +- **$UniFiBaseUri**: enter the base url of your UniFi controller with the port number (eg. https://unifi.yourdomain.com:8443) +- **$UniFiCredentials**: enter your UniFi controllers username and password into the username and password properties of this PowerShell object. + +4. Press **F5** to run the script and wait for it to complete.![UniFi IT Glue Sync Script][12] +5. Once you have run the script, you should add the Unifi Site flexible asset type to the left menu in IT Glue. You can do this under **Account**, **Customize Sidebar![Customise IT Glue Side Bar][13]** +6. Just like before, set up this script to run regularly using a Timer Triggered Azure Function. Remember to replace the credentials with application settings so they are not stored in plain text within the script. + +### PowerShell Script to sync UniFi devices and site information with IT Glue + +```powershell + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + + # SharePoint Details + $client_id = "EnterSharePointClientIDHere" + $client_secret = "EnterSharePointClientSecretHere" + $tenant_id = "EnterAzureADTenantIDHere" + $graphBaseUri = "https://graph.microsoft.com/v1.0/" + $siteid = "root" + $UnifiITGlueMatchRegisterListName = "UniFi - IT Glue match register" + $ITGOrgRegisterListName = "ITGlue Org Register" + + # IT Glue Details + $ITGkey = "EnterITGlueAPIKeyHere" + $ITGbaseURI = "https://api.itglue.com" + $ITGheaders = @{"x-api-key" = $ITGkey} + $UnifiSiteAssetName = "UniFi Site" + + # Setting this overwriteExisting property to $true will cause the script to continuously ` + # overwrite any IT Glue Configuration with a matching serial number with the current details from the UniFi Site. + # I recommend keeping this as $false if you are storing extra information on the IT Glue configuration, or intend to + $overwriteExisting = $false + + # UniFi Details + $UnifiBaseUri = "https://unifi.yourdomain.com:8443" + $UnifiCredentials = @{ + username = "EnterUnifiAdminUserNameHere" + password = "EnterUnifiAdminPasswordHere" + remember = $true + } | ConvertTo-Json + + $UnifiBaseUri = "$UnifiBaseUri/api" + $credentials = $UnifiCredentials + + $TableHeaderColour = "#01a1dd" + # Synchronise Manufacturer and Models with IT Glue + + $unifiAllModels = @" + [{"c":"BZ2","t":"uap","n":"UniFi AP"},{"c":"BZ2LR","t":"uap","n":"UniFi AP-LR"},{"c":"U2HSR","t":"uap","n":"UniFi AP-Outdoor+"}, + {"c":"U2IW","t":"uap","n":"UniFi AP-In Wall"},{"c":"U2L48","t":"uap","n":"UniFi AP-LR"},{"c":"U2Lv2","t":"uap","n":"UniFi AP-LR v2"}, + {"c":"U2M","t":"uap","n":"UniFi AP-Mini"},{"c":"U2O","t":"uap","n":"UniFi AP-Outdoor"},{"c":"U2S48","t":"uap","n":"UniFi AP"}, + {"c":"U2Sv2","t":"uap","n":"UniFi AP v2"},{"c":"U5O","t":"uap","n":"UniFi AP-Outdoor 5G"},{"c":"U7E","t":"uap","n":"UniFi AP-AC"}, + {"c":"U7EDU","t":"uap","n":"UniFi AP-AC-EDU"},{"c":"U7Ev2","t":"uap","n":"UniFi AP-AC v2"},{"c":"U7HD","t":"uap","n":"UniFi AP-HD"}, + {"c":"U7SHD","t":"uap","n":"UniFi AP-SHD"},{"c":"U7NHD","t":"uap","n":"UniFi AP-nanoHD"},{"c":"UCXG","t":"uap","n":"UniFi AP-XG"}, + {"c":"UXSDM","t":"uap","n":"UniFi AP-BaseStationXG"},{"c":"UCMSH","t":"uap","n":"UniFi AP-MeshXG"},{"c":"U7IW","t":"uap","n":"UniFi AP-AC-In Wall"}, + {"c":"U7IWP","t":"uap","n":"UniFi AP-AC-In Wall Pro"},{"c":"U7MP","t":"uap","n":"UniFi AP-AC-Mesh-Pro"},{"c":"U7LR","t":"uap","n":"UniFi AP-AC-LR"}, + {"c":"U7LT","t":"uap","n":"UniFi AP-AC-Lite"},{"c":"U7O","t":"uap","n":"UniFi AP-AC Outdoor"},{"c":"U7P","t":"uap","n":"UniFi AP-Pro"}, + {"c":"U7MSH","t":"uap","n":"UniFi AP-AC-Mesh"},{"c":"U7PG2","t":"uap","n":"UniFi AP-AC-Pro"},{"c":"p2N","t":"uap","n":"PicoStation M2"}, + {"c":"US8","t":"usw","n":"UniFi Switch 8"},{"c":"US8P60","t":"usw","n":"UniFi Switch 8 POE-60W"},{"c":"US8P150","t":"usw","n":"UniFi Switch 8 POE-150W"}, + {"c":"S28150","t":"usw","n":"UniFi Switch 8 AT-150W"},{"c":"USC8","t":"usw","n":"UniFi Switch 8"},{"c":"US16P150","t":"usw","n":"UniFi Switch 16 POE-150W"}, + {"c":"S216150","t":"usw","n":"UniFi Switch 16 AT-150W"},{"c":"US24","t":"usw","n":"UniFi Switch 24"},{"c":"US24P250","t":"usw","n":"UniFi Switch 24 POE-250W"}, + {"c":"US24PL2","t":"usw","n":"UniFi Switch 24 L2 POE"},{"c":"US24P500","t":"usw","n":"UniFi Switch 24 POE-500W"},{"c":"S224250","t":"usw","n":"UniFi Switch 24 AT-250W"}, + {"c":"S224500","t":"usw","n":"UniFi Switch 24 AT-500W"},{"c":"US48","t":"usw","n":"UniFi Switch 48"},{"c":"US48P500","t":"usw","n":"UniFi Switch 48 POE-500W"}, + {"c":"US48PL2","t":"usw","n":"UniFi Switch 48 L2 POE"},{"c":"US48P750","t":"usw","n":"UniFi Switch 48 POE-750W"},{"c":"S248500","t":"usw","n":"UniFi Switch 48 AT-500W"}, + {"c":"S248750","t":"usw","n":"UniFi Switch 48 AT-750W"},{"c":"US6XG150","t":"usw","n":"UniFi Switch 6XG POE-150W"},{"c":"USXG","t":"usw","n":"UniFi Switch 16XG"}, + {"c":"UGW3","t":"ugw","n":"UniFi Security Gateway 3P"},{"c":"UGW4","t":"ugw","n":"UniFi Security Gateway 4P"},{"c":"UGWHD4","t":"ugw","n":"UniFi Security Gateway HD"}, + {"c":"UGWXG","t":"ugw","n":"UniFi Security Gateway XG-8"},{"c":"UP4","t":"uph","n":"UniFi Phone-X"},{"c":"UP5","t":"uph","n":"UniFi Phone"}, + {"c":"UP5t","t":"uph","n":"UniFi Phone-Pro"},{"c":"UP7","t":"uph","n":"UniFi Phone-Executive"},{"c":"UP5c","t":"uph","n":"UniFi Phone"}, + {"c":"UP5tc","t":"uph","n":"UniFi Phone-Pro"},{"c":"UP7c","t":"uph","n":"UniFi Phone-Executive"}] + "@ + + $configTypes = @" + [{"t":"uap","n":"Managed Network WiFi Access"},{"t":"usw","n":"Managed Network Switch"},{"t":"ugw","n":"Managed Network Router"},{"t":"uph","n":"Managed Network Voip Device"}] + "@ | ConvertFrom-Json + + $unifiAllModels = $unifiAllModels | ConvertFrom-Json + $unifiModels = $unifiAllModels | Sort-Object n -Unique + + function Get-GCITSITGItem($Resource) { + $array = @() + + $body = Invoke-RestMethod -Method get -Uri "$ITGbaseUri/$Resource" -Headers $ITGheaders -ContentType application/vnd.api+json + $array += $body.data + Write-Host "Retrieved $($array.Count) items" + + if ($body.links.next) { + do { + $body = Invoke-RestMethod -Method get -Uri $body.links.next -Headers $ITGheaders -ContentType application/vnd.api+json + $array += $body.data + Write-Host "Retrieved $($array.Count) items" + } while ($body.links.next) + } + return $array + } + + function New-GCITSITGItem ($Resource, $Body) { + $item = Invoke-RestMethod -Method POST -ContentType application/vnd.api+json -Uri $ITGBaseURI/$Resource -Body $Body -Headers $ITGHeaders + return $item + } + + function Update-GCITSITGItem ($Resource, $existingItem, $Body) { + $updatedItem = Invoke-RestMethod -Method Patch -Uri "$ITGbaseUri/$Resource/$($existingItem.id)" -Headers $ITGheaders -ContentType application/vnd.api+json -Body $Body + return $updatedItem + } + + function New-GCITSITGBasicAsset($Type, $Name) { + $newModelorManufacturer = @{ + data = @{ + type = $Type + attributes = @{ + name = $Name + } + } + } | ConvertTo-Json -Depth 10 + return $newModelorManufacturer + } + + function New-GCITSITGUnifiSiteFlexibleAsset { + + $body = @{ + data = @{ + type = "flexible_asset_types" + attributes = @{ + name = $UnifiSiteAssetName + description = "UniFi Site Summary" + icon = "magnet" + "show-in-menu" = $true + } + relationships = @{ + "flexible-asset-fields" = @{ + data = @( + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 1 + name = "Site Name" + kind = "Text" + required = $true + "show-in-list" = $true + "use-for-title" = $true + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 2 + name = "Devices" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 3 + name = "LAN Info" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 4 + name = "WAN Info" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 5 + name = "Wifi Networks" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 6 + name = "Port Forwarding" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 7 + name = "Site to Site VPN" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 8 + name = "Remote User VPN" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 9 + name = "Alarms" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 10 + name = "Site Id" + kind = "Text" + required = $false + "show-in-list" = $false + } + } + ) + } + } + } + } + $flexibleAssetType = $body | ConvertTo-Json -Depth 10 + return $flexibleAssetType + } + + function Get-GCITSSharePointListItem($ListId, $ItemId, $Query) { + + if ($ItemId) { + $listItem = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items/$ItemId ` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value = $listItem + } + elseif ($Query) { + $listItems = $null + $listItems = Invoke-RestMethod -Uri "$graphBaseUri/sites/$siteid/lists/$listId/items/?expand=fields&`$filter=$Query" ` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value = @() + $value = $listItems.value + if ($listitems."@odata.nextLink") { + $nextLink = $true + } + if ($nextLink) { + do { + $listItems = Invoke-RestMethod -Uri $listitems."@odata.nextLink"` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value += $listItems.value + if (!$listitems."@odata.nextLink") { + $nextLink = $false + } + } until (!$nextLink) + } + } + else { + $listItems = $null + $listItems = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items?expand=fields ` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value = @() + $value = $listItems.value + if ($listitems."@odata.nextLink") { + $nextLink = $true + } + if ($nextLink) { + do { + $listItems = Invoke-RestMethod -Uri $listitems."@odata.nextLink"` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value += $listItems.value + if (!$listitems."@odata.nextLink") { + $nextLink = $false + } + } until (!$nextLink) + } + } + return $value + } + + function Get-GCITSSharePointList($ListName) { + $list = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists?expand=columns&`$filter=displayName eq '$ListName'" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method GET + $list = $list.value + return $list + } + + function Get-GCITSAccessToken { + $authority = "https://login.microsoftonline.com/$tenant_id" + $tokenEndpointUri = "$authority/oauth2/token" + $resource = "https://graph.microsoft.com" + $content = "grant_type=client_credentials&client_id=$client_id&client_secret=$client_secret&resource=$resource" + $graphBaseUri = "https://graph.microsoft.com/v1.0" + $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing + $access_token = $response.access_token + return $access_token + } + + $alldevices = @() + + function New-GCITSITGConfigurationAsset ($OrganisationId, $Name, $ConfigurationTypeId, $ConfigurationStatusId, $ManufacturerId, $ModelId, $PrimaryIP, $SerialNumber, $MacAddress) { + + $body = @{ + data = @{ + type = "configurations" + attributes = @{ + "organization-id" = $OrganisationId + "name" = $Name + "configuration-type-id" = $ConfigurationTypeId + "configuration-status-id" = $ConfigurationStatusId + "manufacturer-id" = $ManufacturerId + "model-id" = $ModelId + "primary-ip" = $PrimaryIP + "serial-number" = $SerialNumber + "mac-address" = $MacAddress + } + } + } + + $ConfigurationAsset = $body | ConvertTo-Json -Depth 10 + return $ConfigurationAsset + } + + function New-GCITSITGUnifiSiteAsset ($OrganisationId, $SiteName, $Devices, ` + $WifiNetworks, $PortForwarding, $Alarms, $siteId, $LanInfo, $WanInfo, $SiteToSiteVPN, $RemoteUserVPN) { + + $body = @{ + data = @{ + type = "flexible-assets" + attributes = @{ + "organization-id" = $OrganisationId + "flexible-asset-type-id" = $UnifiSiteAsset + traits = @{ + "site-name" = $SiteName + "devices" = $Devices -join " " + "lan-info" = $LanInfo -join " " + "wan-info" = $WanInfo -join " " + "site-to-site-vpn" = $SiteToSiteVPN -join " " + "remote-user-vpn" = $RemoteUserVPN -join " " + "wifi-networks" = $WifiNetworks -join " " + "port-forwarding" = $PortForwarding -join " " + "alarms" = $Alarms -join " " + "site-id" = $SiteId + } + } + } + } + + $tenantAsset = $body | ConvertTo-Json -Depth 10 + return $tenantAsset + } + + function New-GCITSITGTableFromArray($Array, $HeaderColour) { + # Remove any empty properties from table + $properties = $Array | get-member -ErrorAction SilentlyContinue | Where-Object {$_.memberType -contains "NoteProperty"} + foreach ($property in $properties) { + try { + $members = $Array.$($property.name) | Get-Member -ErrorAction Stop + } + catch { + $Array = $Array | Select-Object -Property * -ExcludeProperty $property.name + } + } + $Table = $Array | ConvertTo-Html -Fragment + if ($Table[2] -match "") { + $Table[2] = $Table[2] -replace "", "" + } + return $Table + } + + # Synchronise Models and Manufacturer with IT Glue + + $manufacturer = Get-GCITSITGItem -Resource "manufacturers?filter[name]=UniFi" + + if (!$manufacturer) { + $newManufacturer = New-GCITSITGBasicAsset -Type manufacturers -Name UniFi + $manufacturer = New-GCITSITGItem -Resource manufacturers -Body $newManufacturer + } + [array]$itgModels = Get-GCITSITGItem -Resource "manufacturers/$($manufacturer.id)/relationships/models" + + foreach ($model in $unifiModels) { + if ($itgModels.attributes.name -notcontains $model.n) { + $newModel = New-GCITSITGBasicAsset -Type models -Name $model.n + New-GCITSITGItem -Resource "manufacturers/$($manufacturer.id)/relationships/models" -Body $newModel + } + } + + # Check for existing Unifi Site flexible asset + $UnifiSiteAsset = (Get-GCITSITGItem -Resource "flexible_asset_types?filter[name]=$UnifiSiteAssetName").id + + if (!$UnifiSiteAsset) { + Write-Host "Creating IT Glue Flexible Asset for UniFi Site" + $flexibleAssetType = New-GCITSITGUnifiSiteFlexibleAsset + $UnifiSiteAsset = (New-GCITSITGItem -Resource flexible_asset_types -Body $flexibleAssetType).data.id + } + + [array]$itgModels = Get-GCITSITGItem -Resource "manufacturers/$($manufacturer.id)/relationships/models" + + # Confirm Configuration Types exist in IT Glue + $itgConfigTypes = Get-GCITSITGItem -Resource "configuration_types" + foreach ($configType in $configTypes) { + if ($itgConfigTypes.attributes.name -notcontains $configType.n) { + $newModel = New-GCITSITGBasicAsset -Type "configuration-types" -Name $configType.n + New-GCITSITGItem -Resource "configuration_types" -Body $newModel + } + } + $itgConfigTypes = Get-GCITSITGItem -Resource "configuration_types" + + # Get Active Configuration Status + $ActiveStatus = Get-GCITSITGItem -Resource "configuration_statuses?filter[name]=Active" + + # Get SharePoint lists for retrieving Unifi Site - IT Glue Org matches + $access_token = Get-GCITSAccessToken + $SPHeaders = @{Authorization = "Bearer $access_token"} + + $ITGOrgRegisterList = Get-GCITSSharePointList -ListName $ITGOrgRegisterListName + $UnifiITGlueMatchList = Get-GCITSSharePointList -ListName $UnifiITGlueMatchRegisterListName + + Invoke-RestMethod -Uri https://unifi.gcits.com:8443/api/login -Method POST -Body $credentials -SessionVariable websession + + # Get Sites + $sites = (Invoke-RestMethod -Uri "$UnifiBaseUri/self/sites" -WebSession $websession).data + + foreach ($site in $sites) { + Write-Host "Checking devices for Unifi Site $($site.desc)" + + $MatchedSite = Get-GCITSSharePointListItem -ListId $UnifiITGlueMatchList.id -Query "fields/UnifiSiteName eq '$($site.name)'" + + if ($MatchedSite) { + $itGlueOrgID = (Get-GCITSSharePointListItem -ListId $ITGOrgRegisterList.id -ItemId $MatchedSite.fields.ITGlueLookupId).fields.itglueid + } + if ($MatchedSite -and $itGlueOrgID) { + + # Get UniFi Devices from Controller + $unifiDevices = Invoke-RestMethod -Uri "$UnifiBaseUri/s/$($site.name)/stat/device" -WebSession $websession + + # Get documented UniFi Devices from IT GLue + $existingDevices = Get-GCITSITGItem -Resource "configurations?filter[organization_id]=$itglueOrgId" + + # Start UniFi device configuration sync + foreach ($device in $unifiDevices.data) { + + $configType = $configTypes | Where-Object {$_.t -contains $device.type} + $itgConfigType = $itgConfigTypes | Where-Object {$_.attributes.name -contains $configType.n} + + Write-Host "$($device.type): $($device.name) - $($itgConfigType.attributes.name)" + + $modelName = ($unifiAllModels | Where-Object {$_.c -contains $device.model}).n + if (!$device.name) { + $device | Add-Member name $modelName -Force + if (!$device.name) { + $device.name = "Unifi Device" + } + } + $itgModel = $itgModels | Where-Object {$_.attributes.name -contains $modelName} + + $configAsset = New-GCITSITGConfigurationAsset -OrganisationId $itGlueOrgID -Name $device.name -ConfigurationTypeId $itgConfigType.id ` + -ConfigurationStatusId $ActiveStatus.id -ManufacturerId $manufacturer.id -ModelId $itgModel.id -PrimaryIP $device.ip -SerialNumber $device.serial -MacAddress $device.mac + $alldevices += $device + if ($existingDevices.attributes.'serial-number' -notcontains $device.serial) { + $NewItem = New-GCITSITGItem -Resource configurations -Body $configAsset + } + else { + if ($overwriteExisting) { + Write-Host "Updating Item" + $existingItem = $existingDevices | Where-Object {$_.attributes.'serial-number' -contains $device.serial} | Select-Object -First 1 + $updateItem = Update-GCITSITGItem -Resource configurations -existingItem $existingItem -Body $configAsset + } + } + } + # End UniFi device configuration sync + + $name = $site.desc + + if ($UnifiDevices.data) { + $devices = $UnifiDevices.data | Select-Object Name, @{n = "Type"; e = {$thisDevice = $_; ($configTypes | Where-Object {$_.t -contains $thisDevice.type}).n}}, ` + @{n = "Model"; e = {$thisDevice = $_; ($unifiAllModels | Where-Object {$_.c -contains $thisDevice.model}).n}}, ` + @{n = "Firmware"; e = {$_.Version}}, Mac + $devicesTable = New-GCITSITGTableFromArray -Array $devices -HeaderColour $TableHeaderColour + } + else { + $devicesTable = $null + } + + $uaps = $unifiDevices.data | Where-Object {$_.type -contains "uap"} + if ($uaps) { + $wifi = @() + foreach ($uap in $uaps) { + $networks = $uap.vap_table | Group-Object Essid + foreach ($network in $networks) { + $wifi += $network | Select-object @{n = "SSID"; e = {$_.Name}}, @{n = "Access Point"; e = {$uap.name}}, ` + @{n = "Channel"; e = {$_.group.channel -join ", "}}, @{n = "Usage"; e = {$_.group.usage | Sort-Object -Unique}}, ` + @{n = "Up"; e = {$_.group.up | sort-object -Unique}} + } + } + $wifiTable = New-GCITSITGTableFromArray -Array $wifi -HeaderColour $TableHeaderColour + } + else { + $wifiTable = $null + } + + $alarms = (Invoke-RestMethod -Uri "$UnifiBaseUri/s/$($site.name)/stat/alarm" -WebSession $websession).data + if ($alarms) { + $alarms = $alarms | Select-Object @{n = "Universal Time"; e = {[datetime]$_.datetime}}, ` + @{n = "Device Name"; e = {$_.$(($_ | Get-Member | Where-Object {$_.Name -match "_name"}).name)}}, ` + @{n = "Message"; e = {$_.msg}} -First 10 + $alarmsTable = New-GCITSITGTableFromArray -Array $alarms -HeaderColour $TableHeaderColour + } + else { + $alarmsTable = $null + } + + $portforward = (Invoke-RestMethod -Uri "$UnifiBaseUri/s/$($site.name)/rest/portforward" -WebSession $websession).data + if ($portforward) { + $portForward = $portforward | Select-Object Name, @{n = "Source"; e = {"$($_.src):$($_.dst_port)"}}, ` + @{n = "Destination"; e = {"$($_.fwd):$($_.fwd_port)"}}, @{n = "Protocol"; e = {$_.proto}} + $portForwardTable = New-GCITSITGTableFromArray -Array $portforward -HeaderColour $TableHeaderColour + } + else { + $portForwardTable = $null + } + + $networkConf = (Invoke-RestMethod -Uri "$UnifiBaseUri/s/$($site.name)/rest/networkconf" -WebSession $websession).data + + $c2svpn = @() + $s2svpn = @() + $lan = @() + $wan = @() + + foreach ($network in $networkConf) { + if ($network.purpose -contains "corporate") { + $lan += $network | Select-Object @{n = "Name"; e = {$_.name}}, ` + @{n = "vLAN"; e = {"$($_.vlan_enabled) $($_.vlan)"}}, ` + @{n = "Subnet"; e = {$_.ip_subnet}}, ` + @{n = "DNS 1"; e = {$_.dhcpd_dns_1}}, ` + @{n = "DNS 2"; e = {$_.dhcpd_dns_2}}, ` + @{n = "DHCP Range"; e = {"$($_.dhcpd_start) - $($_.dhcpd_stop)"}} + } + elseif ($network.purpose -contains "wan") { + $wan += $network | Select-Object @{n = "Name"; e = {$_.name}}, ` + @{n = "WAN IP"; e = {$_.wan_ip}}, ` + @{n = "Subnet Mask"; e = {$_.wan_netmask}}, ` + @{n = "Gateway"; e = {$_.wan_gateway}}, ` + @{n = "DNS 1"; e = {$_.wan_dns1}}, ` + @{n = "DNS 2"; e = {$_.wan_dns2}}, ` + @{n = "Wan Type"; e = {$_.wan_type}}, ` + @{n = "Load Balance Type"; e = {$_.wan_load_balance_type}} + } + elseif ($network.purpose -contains "site-vpn") { + $s2svpn += $network | Select-Object @{n = "Name"; e = {$_.name}}, ` + @{n = "Local Site IP"; e = {$_.local_site_ip}}, ` + @{n = "Remote Site IP"; e = {$_.remote_site_ip}}, ` + @{n = "Remote Site Name"; e = {($sites | Where-Object {$_._id -eq $network.remote_site_id}).desc}} + } + elseif ($network.purpose -contains "remote-user-vpn") { + $c2svpn += $network | Select-Object @{n = "Name"; e = {$_.name}}, ` + @{n = "IP Subnet"; e = {$_.ip_subnet}}, ` + @{n = "DHCP Range"; e = {"$($_.dhcpd_start) - $($_.dhcpd_stop)"}} + } + } + + if ($lan) { + $lanTable = New-GCITSITGTableFromArray -Array $lan -HeaderColour $TableHeaderColour + } + else { + $lanTable = $null + } + if ($wan) { + $wanTable = New-GCITSITGTableFromArray -Array $wan -HeaderColour $TableHeaderColour + } + else { + $wanTable = $null + } + if ($s2svpn) { + $s2svpnTable = New-GCITSITGTableFromArray -Array $s2svpn -HeaderColour $TableHeaderColour + } + else { + $s2svpnTable = $null + } + if ($c2svpn) { + $c2svpnTable = New-GCITSITGTableFromArray -Array $c2svpn -HeaderColour $TableHeaderColour + } + else { + $c2svpnTable = $null + } + + [array]$existingAssets = Get-GCITSITGItem -Resource "flexible_assets?filter[organization_id]=$itGlueOrgID&filter[flexible_asset_type_id]=$UnifiSiteAsset" + + $SiteAsset = New-GCITSITGUnifiSiteAsset -OrganisationId $itGlueOrgID -SiteName $name -Devices $devicesTable -WifiNetworks $wifiTable -PortForwarding $portForwardTable -Alarms $alarmsTable -SiteId $site.name -WanInfo $wanTable -LanInfo $LanTable -SiteToSiteVPN $s2svpnTable -RemoteUserVPN $c2svpnTable + + if ($existingassets.attributes.traits.'site-id' -contains $site.name) { + Write-Host "Updating Item" + $existingItem = $existingAssets | Where-Object {$_.attributes.traits.'site-id' -contains $site.name} + $updateItem = Update-GCITSITGItem -resource flexible_assets -body $SiteAsset -existingItem $existingItem + } + else { + Write-Host "Creating New Item" + $newItem = New-GCITSITGItem -resource flexible_assets -body $SiteAsset + } + } + } +``` + +## Want to improve or add to these scripts? + +These scripts and guide will be available shortly on our GitHub at and will be open for feedback and contributions. + +[1]: https://gcits.com/wp-content/uploads/UniFiITGlueSync-1030x436.png +[2]: https://gcits.com/wp-content/uploads/UniFiSiteGif.gif +[3]: https://gcits.com/knowledge-base/create-a-sharepoint-application-for-the-microsoft-graph-via-powershell/ +[4]: https://gcits.com/knowledge-base/sync-it-glue-organisations-with-a-sharepoint-list-via-powershell/ +[5]: https://gcits.com/wp-content/uploads/WaitForApplicationToCompleteAndTest-1030x571.png +[6]: https://gcits.com/wp-content/uploads/SharePointListOfITGlueOrganisations-1030x426.png +[7]: https://gcits.com/wp-content/uploads/ITGlueUniFiMatchRegister-1030x486.png +[8]: https://gcits.com/wp-content/uploads/MatchingOrganisationsITGlueAndUnifiInSharePointList-1030x270.png +[9]: https://gcits.com/wp-content/uploads/LocateMatchRegisterInSharePoint-1030x496.png +[10]: https://gcits.com/wp-content/uploads/SelectCorrectITGlueOrganisationForUniFiSiteInSharePoint-1030x378.png +[11]: https://gcits.com/wp-content/uploads/GCITSUniFiSiteInITGlue-1030x616.png +[12]: https://gcits.com/wp-content/uploads/UnifiITGlueSyncScript.png +[13]: https://gcits.com/wp-content/uploads/CustomiseITGlueSideBar.png diff --git a/UniFiSync/UniFiITGlueSync.ps1 b/UniFiSync/UniFiITGlueSync.ps1 new file mode 100644 index 0000000..d32ff9f --- /dev/null +++ b/UniFiSync/UniFiITGlueSync.ps1 @@ -0,0 +1,609 @@ +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +# SharePoint Details +$client_id = "EnterSharePointClientIDHere" +$client_secret = "EnterSharePointClientSecretHere" +$tenant_id = "EnterAzureADTenantIDHere" +$graphBaseUri = "https://graph.microsoft.com/v1.0/" +$siteid = "root" +$UnifiITGlueMatchRegisterListName = "UniFi - IT Glue match register" +$ITGOrgRegisterListName = "ITGlue Org Register" + +# IT Glue Details +$ITGkey = "EnterITGlueAPIKeyHere" +$ITGbaseURI = "https://api.itglue.com" +$ITGheaders = @{"x-api-key" = $ITGkey} +$UnifiSiteAssetName = "UniFi Site" + +# Setting this overwriteExisting property to $true will cause the script to continuously ` +# overwrite any IT Glue Configuration with a matching serial number with the current details from the UniFi Site. +# I recommend keeping this as $false if you are storing extra information on the IT Glue configuration, or intend to +$overwriteExisting = $false + +# UniFi Details +$UnifiBaseUri = "https://unifi.yourdomain.com:8443" +$UnifiCredentials = @{ + username = "EnterUnifiAdminUserNameHere" + password = "EnterUnifiAdminPasswordHere" + remember = $true +} | ConvertTo-Json + +$UnifiBaseUri = "$UnifiBaseUri/api" +$credentials = $UnifiCredentials + +$TableHeaderColour = "#01a1dd" +# Synchronise Manufacturer and Models with IT Glue + +$unifiAllModels = @" +[{"c":"BZ2","t":"uap","n":"UniFi AP"},{"c":"BZ2LR","t":"uap","n":"UniFi AP-LR"},{"c":"U2HSR","t":"uap","n":"UniFi AP-Outdoor+"}, +{"c":"U2IW","t":"uap","n":"UniFi AP-In Wall"},{"c":"U2L48","t":"uap","n":"UniFi AP-LR"},{"c":"U2Lv2","t":"uap","n":"UniFi AP-LR v2"}, +{"c":"U2M","t":"uap","n":"UniFi AP-Mini"},{"c":"U2O","t":"uap","n":"UniFi AP-Outdoor"},{"c":"U2S48","t":"uap","n":"UniFi AP"}, +{"c":"U2Sv2","t":"uap","n":"UniFi AP v2"},{"c":"U5O","t":"uap","n":"UniFi AP-Outdoor 5G"},{"c":"U7E","t":"uap","n":"UniFi AP-AC"}, +{"c":"U7EDU","t":"uap","n":"UniFi AP-AC-EDU"},{"c":"U7Ev2","t":"uap","n":"UniFi AP-AC v2"},{"c":"U7HD","t":"uap","n":"UniFi AP-HD"}, +{"c":"U7SHD","t":"uap","n":"UniFi AP-SHD"},{"c":"U7NHD","t":"uap","n":"UniFi AP-nanoHD"},{"c":"UCXG","t":"uap","n":"UniFi AP-XG"}, +{"c":"UXSDM","t":"uap","n":"UniFi AP-BaseStationXG"},{"c":"UCMSH","t":"uap","n":"UniFi AP-MeshXG"},{"c":"U7IW","t":"uap","n":"UniFi AP-AC-In Wall"}, +{"c":"U7IWP","t":"uap","n":"UniFi AP-AC-In Wall Pro"},{"c":"U7MP","t":"uap","n":"UniFi AP-AC-Mesh-Pro"},{"c":"U7LR","t":"uap","n":"UniFi AP-AC-LR"}, +{"c":"U7LT","t":"uap","n":"UniFi AP-AC-Lite"},{"c":"U7O","t":"uap","n":"UniFi AP-AC Outdoor"},{"c":"U7P","t":"uap","n":"UniFi AP-Pro"}, +{"c":"U7MSH","t":"uap","n":"UniFi AP-AC-Mesh"},{"c":"U7PG2","t":"uap","n":"UniFi AP-AC-Pro"},{"c":"p2N","t":"uap","n":"PicoStation M2"}, +{"c":"US8","t":"usw","n":"UniFi Switch 8"},{"c":"US8P60","t":"usw","n":"UniFi Switch 8 POE-60W"},{"c":"US8P150","t":"usw","n":"UniFi Switch 8 POE-150W"}, +{"c":"S28150","t":"usw","n":"UniFi Switch 8 AT-150W"},{"c":"USC8","t":"usw","n":"UniFi Switch 8"},{"c":"US16P150","t":"usw","n":"UniFi Switch 16 POE-150W"}, +{"c":"S216150","t":"usw","n":"UniFi Switch 16 AT-150W"},{"c":"US24","t":"usw","n":"UniFi Switch 24"},{"c":"US24P250","t":"usw","n":"UniFi Switch 24 POE-250W"}, +{"c":"US24PL2","t":"usw","n":"UniFi Switch 24 L2 POE"},{"c":"US24P500","t":"usw","n":"UniFi Switch 24 POE-500W"},{"c":"S224250","t":"usw","n":"UniFi Switch 24 AT-250W"}, +{"c":"S224500","t":"usw","n":"UniFi Switch 24 AT-500W"},{"c":"US48","t":"usw","n":"UniFi Switch 48"},{"c":"US48P500","t":"usw","n":"UniFi Switch 48 POE-500W"}, +{"c":"US48PL2","t":"usw","n":"UniFi Switch 48 L2 POE"},{"c":"US48P750","t":"usw","n":"UniFi Switch 48 POE-750W"},{"c":"S248500","t":"usw","n":"UniFi Switch 48 AT-500W"}, +{"c":"S248750","t":"usw","n":"UniFi Switch 48 AT-750W"},{"c":"US6XG150","t":"usw","n":"UniFi Switch 6XG POE-150W"},{"c":"USXG","t":"usw","n":"UniFi Switch 16XG"}, +{"c":"UGW3","t":"ugw","n":"UniFi Security Gateway 3P"},{"c":"UGW4","t":"ugw","n":"UniFi Security Gateway 4P"},{"c":"UGWHD4","t":"ugw","n":"UniFi Security Gateway HD"}, +{"c":"UGWXG","t":"ugw","n":"UniFi Security Gateway XG-8"},{"c":"UP4","t":"uph","n":"UniFi Phone-X"},{"c":"UP5","t":"uph","n":"UniFi Phone"}, +{"c":"UP5t","t":"uph","n":"UniFi Phone-Pro"},{"c":"UP7","t":"uph","n":"UniFi Phone-Executive"},{"c":"UP5c","t":"uph","n":"UniFi Phone"}, +{"c":"UP5tc","t":"uph","n":"UniFi Phone-Pro"},{"c":"UP7c","t":"uph","n":"UniFi Phone-Executive"}] +"@ + +$configTypes = @" +[{"t":"uap","n":"Managed Network WiFi Access"},{"t":"usw","n":"Managed Network Switch"},{"t":"ugw","n":"Managed Network Router"},{"t":"uph","n":"Managed Network Voip Device"}] +"@ | ConvertFrom-Json + +$unifiAllModels = $unifiAllModels | ConvertFrom-Json +$unifiModels = $unifiAllModels | Sort-Object n -Unique + +function Get-GCITSITGItem($Resource) { + $array = @() + + $body = Invoke-RestMethod -Method get -Uri "$ITGbaseUri/$Resource" -Headers $ITGheaders -ContentType application/vnd.api+json + $array += $body.data + Write-Host "Retrieved $($array.Count) items" + + if ($body.links.next) { + do { + $body = Invoke-RestMethod -Method get -Uri $body.links.next -Headers $ITGheaders -ContentType application/vnd.api+json + $array += $body.data + Write-Host "Retrieved $($array.Count) items" + } while ($body.links.next) + } + return $array +} + +function New-GCITSITGItem ($Resource, $Body) { + $item = Invoke-RestMethod -Method POST -ContentType application/vnd.api+json -Uri $ITGBaseURI/$Resource -Body $Body -Headers $ITGHeaders + return $item +} + +function Update-GCITSITGItem ($Resource, $existingItem, $Body) { + $updatedItem = Invoke-RestMethod -Method Patch -Uri "$ITGbaseUri/$Resource/$($existingItem.id)" -Headers $ITGheaders -ContentType application/vnd.api+json -Body $Body + return $updatedItem +} + +function New-GCITSITGBasicAsset($Type, $Name) { + $newModelorManufacturer = @{ + data = @{ + type = $Type + attributes = @{ + name = $Name + } + } + } | ConvertTo-Json -Depth 10 + return $newModelorManufacturer +} + +function New-GCITSITGUnifiSiteFlexibleAsset { + + $body = @{ + data = @{ + type = "flexible_asset_types" + attributes = @{ + name = $UnifiSiteAssetName + description = "UniFi Site Summary" + icon = "magnet" + "show-in-menu" = $true + } + relationships = @{ + "flexible-asset-fields" = @{ + data = @( + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 1 + name = "Site Name" + kind = "Text" + required = $true + "show-in-list" = $true + "use-for-title" = $true + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 2 + name = "Devices" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 3 + name = "LAN Info" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 4 + name = "WAN Info" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 5 + name = "Wifi Networks" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 6 + name = "Port Forwarding" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 7 + name = "Site to Site VPN" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 8 + name = "Remote User VPN" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 9 + name = "Alarms" + kind = "Textbox" + required = $false + "show-in-list" = $false + } + }, + @{ + type = "flexible_asset_fields" + attributes = @{ + order = 10 + name = "Site Id" + kind = "Text" + required = $false + "show-in-list" = $false + } + } + ) + } + } + } + } + $flexibleAssetType = $body | ConvertTo-Json -Depth 10 + return $flexibleAssetType +} + +function Get-GCITSSharePointListItem($ListId, $ItemId, $Query) { + + if ($ItemId) { + $listItem = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items/$ItemId ` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value = $listItem + } + elseif ($Query) { + $listItems = $null + $listItems = Invoke-RestMethod -Uri "$graphBaseUri/sites/$siteid/lists/$listId/items/?expand=fields&`$filter=$Query" ` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value = @() + $value = $listItems.value + if ($listitems."@odata.nextLink") { + $nextLink = $true + } + if ($nextLink) { + do { + $listItems = Invoke-RestMethod -Uri $listitems."@odata.nextLink"` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value += $listItems.value + if (!$listitems."@odata.nextLink") { + $nextLink = $false + } + } until (!$nextLink) + } + } + else { + $listItems = $null + $listItems = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items?expand=fields ` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value = @() + $value = $listItems.value + if ($listitems."@odata.nextLink") { + $nextLink = $true + } + if ($nextLink) { + do { + $listItems = Invoke-RestMethod -Uri $listitems."@odata.nextLink"` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value += $listItems.value + if (!$listitems."@odata.nextLink") { + $nextLink = $false + } + } until (!$nextLink) + } + } + return $value +} + +function Get-GCITSSharePointList($ListName) { + $list = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists?expand=columns&`$filter=displayName eq '$ListName'" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method GET + $list = $list.value + return $list +} + +function Get-GCITSAccessToken { + $authority = "https://login.microsoftonline.com/$tenant_id" + $tokenEndpointUri = "$authority/oauth2/token" + $resource = "https://graph.microsoft.com" + $content = "grant_type=client_credentials&client_id=$client_id&client_secret=$client_secret&resource=$resource" + $graphBaseUri = "https://graph.microsoft.com/v1.0" + $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing + $access_token = $response.access_token + return $access_token +} + +$alldevices = @() + +function New-GCITSITGConfigurationAsset ($OrganisationId, $Name, $ConfigurationTypeId, $ConfigurationStatusId, $ManufacturerId, $ModelId, $PrimaryIP, $SerialNumber, $MacAddress) { + + $body = @{ + data = @{ + type = "configurations" + attributes = @{ + "organization-id" = $OrganisationId + "name" = $Name + "configuration-type-id" = $ConfigurationTypeId + "configuration-status-id" = $ConfigurationStatusId + "manufacturer-id" = $ManufacturerId + "model-id" = $ModelId + "primary-ip" = $PrimaryIP + "serial-number" = $SerialNumber + "mac-address" = $MacAddress + } + } + } + + $ConfigurationAsset = $body | ConvertTo-Json -Depth 10 + return $ConfigurationAsset +} + +function New-GCITSITGUnifiSiteAsset ($OrganisationId, $SiteName, $Devices, ` + $WifiNetworks, $PortForwarding, $Alarms, $siteId, $LanInfo, $WanInfo, $SiteToSiteVPN, $RemoteUserVPN) { + + $body = @{ + data = @{ + type = "flexible-assets" + attributes = @{ + "organization-id" = $OrganisationId + "flexible-asset-type-id" = $UnifiSiteAsset + traits = @{ + "site-name" = $SiteName + "devices" = $Devices -join " " + "lan-info" = $LanInfo -join " " + "wan-info" = $WanInfo -join " " + "site-to-site-vpn" = $SiteToSiteVPN -join " " + "remote-user-vpn" = $RemoteUserVPN -join " " + "wifi-networks" = $WifiNetworks -join " " + "port-forwarding" = $PortForwarding -join " " + "alarms" = $Alarms -join " " + "site-id" = $SiteId + } + } + } + } + + $tenantAsset = $body | ConvertTo-Json -Depth 10 + return $tenantAsset +} + +function New-GCITSITGTableFromArray($Array, $HeaderColour) { + # Remove any empty properties from table + $properties = $Array | get-member -ErrorAction SilentlyContinue | Where-Object {$_.memberType -contains "NoteProperty"} + foreach ($property in $properties) { + try { + $members = $Array.$($property.name) | Get-Member -ErrorAction Stop + } + catch { + $Array = $Array | Select-Object -Property * -ExcludeProperty $property.name + } + } + $Table = $Array | ConvertTo-Html -Fragment + if ($Table[2] -match "") { + $Table[2] = $Table[2] -replace "", "" + } + return $Table +} + +# Synchronise Models and Manufacturer with IT Glue + +$manufacturer = Get-GCITSITGItem -Resource "manufacturers?filter[name]=UniFi" + +if (!$manufacturer) { + $newManufacturer = New-GCITSITGBasicAsset -Type manufacturers -Name UniFi + $manufacturer = New-GCITSITGItem -Resource manufacturers -Body $newManufacturer +} +[array]$itgModels = Get-GCITSITGItem -Resource "manufacturers/$($manufacturer.id)/relationships/models" + +foreach ($model in $unifiModels) { + if ($itgModels.attributes.name -notcontains $model.n) { + $newModel = New-GCITSITGBasicAsset -Type models -Name $model.n + New-GCITSITGItem -Resource "manufacturers/$($manufacturer.id)/relationships/models" -Body $newModel + } +} + +# Check for existing Unifi Site flexible asset +$UnifiSiteAsset = (Get-GCITSITGItem -Resource "flexible_asset_types?filter[name]=$UnifiSiteAssetName").id + +if (!$UnifiSiteAsset) { + Write-Host "Creating IT Glue Flexible Asset for UniFi Site" + $flexibleAssetType = New-GCITSITGUnifiSiteFlexibleAsset + $UnifiSiteAsset = (New-GCITSITGItem -Resource flexible_asset_types -Body $flexibleAssetType).data.id +} + +[array]$itgModels = Get-GCITSITGItem -Resource "manufacturers/$($manufacturer.id)/relationships/models" + +# Confirm Configuration Types exist in IT Glue +$itgConfigTypes = Get-GCITSITGItem -Resource "configuration_types" +foreach ($configType in $configTypes) { + if ($itgConfigTypes.attributes.name -notcontains $configType.n) { + $newModel = New-GCITSITGBasicAsset -Type "configuration-types" -Name $configType.n + New-GCITSITGItem -Resource "configuration_types" -Body $newModel + } +} +$itgConfigTypes = Get-GCITSITGItem -Resource "configuration_types" + +# Get Active Configuration Status +$ActiveStatus = Get-GCITSITGItem -Resource "configuration_statuses?filter[name]=Active" + +# Get SharePoint lists for retrieving Unifi Site - IT Glue Org matches +$access_token = Get-GCITSAccessToken +$SPHeaders = @{Authorization = "Bearer $access_token"} + +$ITGOrgRegisterList = Get-GCITSSharePointList -ListName $ITGOrgRegisterListName +$UnifiITGlueMatchList = Get-GCITSSharePointList -ListName $UnifiITGlueMatchRegisterListName + +Invoke-RestMethod -Uri https://unifi.gcits.com:8443/api/login -Method POST -Body $credentials -SessionVariable websession + +# Get Sites +$sites = (Invoke-RestMethod -Uri "$UnifiBaseUri/self/sites" -WebSession $websession).data + +foreach ($site in $sites) { + Write-Host "Checking devices for Unifi Site $($site.desc)" + + $MatchedSite = Get-GCITSSharePointListItem -ListId $UnifiITGlueMatchList.id -Query "fields/UnifiSiteName eq '$($site.name)'" + + if ($MatchedSite) { + $itGlueOrgID = (Get-GCITSSharePointListItem -ListId $ITGOrgRegisterList.id -ItemId $MatchedSite.fields.ITGlueLookupId).fields.itglueid + } + if ($MatchedSite -and $itGlueOrgID) { + + # Get UniFi Devices from Controller + $unifiDevices = Invoke-RestMethod -Uri "$UnifiBaseUri/s/$($site.name)/stat/device" -WebSession $websession + + # Get documented UniFi Devices from IT GLue + $existingDevices = Get-GCITSITGItem -Resource "configurations?filter[organization_id]=$itglueOrgId" + + # Start UniFi device configuration sync + foreach ($device in $unifiDevices.data) { + + $configType = $configTypes | Where-Object {$_.t -contains $device.type} + $itgConfigType = $itgConfigTypes | Where-Object {$_.attributes.name -contains $configType.n} + + Write-Host "$($device.type): $($device.name) - $($itgConfigType.attributes.name)" + + $modelName = ($unifiAllModels | Where-Object {$_.c -contains $device.model}).n + if (!$device.name) { + $device | Add-Member name $modelName -Force + if (!$device.name) { + $device.name = "Unifi Device" + } + } + $itgModel = $itgModels | Where-Object {$_.attributes.name -contains $modelName} + + $configAsset = New-GCITSITGConfigurationAsset -OrganisationId $itGlueOrgID -Name $device.name -ConfigurationTypeId $itgConfigType.id ` + -ConfigurationStatusId $ActiveStatus.id -ManufacturerId $manufacturer.id -ModelId $itgModel.id -PrimaryIP $device.ip -SerialNumber $device.serial -MacAddress $device.mac + $alldevices += $device + if ($existingDevices.attributes.'serial-number' -notcontains $device.serial) { + $NewItem = New-GCITSITGItem -Resource configurations -Body $configAsset + } + else { + if ($overwriteExisting) { + Write-Host "Updating Item" + $existingItem = $existingDevices | Where-Object {$_.attributes.'serial-number' -contains $device.serial} | Select-Object -First 1 + $updateItem = Update-GCITSITGItem -Resource configurations -existingItem $existingItem -Body $configAsset + } + } + } + # End UniFi device configuration sync + + $name = $site.desc + + if ($UnifiDevices.data) { + $devices = $UnifiDevices.data | Select-Object Name, @{n = "Type"; e = {$thisDevice = $_; ($configTypes | Where-Object {$_.t -contains $thisDevice.type}).n}}, ` + @{n = "Model"; e = {$thisDevice = $_; ($unifiAllModels | Where-Object {$_.c -contains $thisDevice.model}).n}}, ` + @{n = "Firmware"; e = {$_.Version}}, Mac + $devicesTable = New-GCITSITGTableFromArray -Array $devices -HeaderColour $TableHeaderColour + } + else { + $devicesTable = $null + } + + $uaps = $unifiDevices.data | Where-Object {$_.type -contains "uap"} + if ($uaps) { + $wifi = @() + foreach ($uap in $uaps) { + $networks = $uap.vap_table | Group-Object Essid + foreach ($network in $networks) { + $wifi += $network | Select-object @{n = "SSID"; e = {$_.Name}}, @{n = "Access Point"; e = {$uap.name}}, ` + @{n = "Channel"; e = {$_.group.channel -join ", "}}, @{n = "Usage"; e = {$_.group.usage | Sort-Object -Unique}}, ` + @{n = "Up"; e = {$_.group.up | sort-object -Unique}} + } + } + $wifiTable = New-GCITSITGTableFromArray -Array $wifi -HeaderColour $TableHeaderColour + } + else { + $wifiTable = $null + } + + $alarms = (Invoke-RestMethod -Uri "$UnifiBaseUri/s/$($site.name)/stat/alarm" -WebSession $websession).data + if ($alarms) { + $alarms = $alarms | Select-Object @{n = "Universal Time"; e = {[datetime]$_.datetime}}, ` + @{n = "Device Name"; e = {$_.$(($_ | Get-Member | Where-Object {$_.Name -match "_name"}).name)}}, ` + @{n = "Message"; e = {$_.msg}} -First 10 + $alarmsTable = New-GCITSITGTableFromArray -Array $alarms -HeaderColour $TableHeaderColour + } + else { + $alarmsTable = $null + } + + $portforward = (Invoke-RestMethod -Uri "$UnifiBaseUri/s/$($site.name)/rest/portforward" -WebSession $websession).data + if ($portforward) { + $portForward = $portforward | Select-Object Name, @{n = "Source"; e = {"$($_.src):$($_.dst_port)"}}, ` + @{n = "Destination"; e = {"$($_.fwd):$($_.fwd_port)"}}, @{n = "Protocol"; e = {$_.proto}} + $portForwardTable = New-GCITSITGTableFromArray -Array $portforward -HeaderColour $TableHeaderColour + } + else { + $portForwardTable = $null + } + + $networkConf = (Invoke-RestMethod -Uri "$UnifiBaseUri/s/$($site.name)/rest/networkconf" -WebSession $websession).data + + $c2svpn = @() + $s2svpn = @() + $lan = @() + $wan = @() + + foreach ($network in $networkConf) { + if ($network.purpose -contains "corporate") { + $lan += $network | Select-Object @{n = "Name"; e = {$_.name}}, ` + @{n = "vLAN"; e = {"$($_.vlan_enabled) $($_.vlan)"}}, ` + @{n = "Subnet"; e = {$_.ip_subnet}}, ` + @{n = "DNS 1"; e = {$_.dhcpd_dns_1}}, ` + @{n = "DNS 2"; e = {$_.dhcpd_dns_2}}, ` + @{n = "DHCP Range"; e = {"$($_.dhcpd_start) - $($_.dhcpd_stop)"}} + } + elseif ($network.purpose -contains "wan") { + $wan += $network | Select-Object @{n = "Name"; e = {$_.name}}, ` + @{n = "WAN IP"; e = {$_.wan_ip}}, ` + @{n = "Subnet Mask"; e = {$_.wan_netmask}}, ` + @{n = "Gateway"; e = {$_.wan_gateway}}, ` + @{n = "DNS 1"; e = {$_.wan_dns1}}, ` + @{n = "DNS 2"; e = {$_.wan_dns2}}, ` + @{n = "Wan Type"; e = {$_.wan_type}}, ` + @{n = "Load Balance Type"; e = {$_.wan_load_balance_type}} + } + elseif ($network.purpose -contains "site-vpn") { + $s2svpn += $network | Select-Object @{n = "Name"; e = {$_.name}}, ` + @{n = "Local Site IP"; e = {$_.local_site_ip}}, ` + @{n = "Remote Site IP"; e = {$_.remote_site_ip}}, ` + @{n = "Remote Site Name"; e = {($sites | Where-Object {$_._id -eq $network.remote_site_id}).desc}} + } + elseif ($network.purpose -contains "remote-user-vpn") { + $c2svpn += $network | Select-Object @{n = "Name"; e = {$_.name}}, ` + @{n = "IP Subnet"; e = {$_.ip_subnet}}, ` + @{n = "DHCP Range"; e = {"$($_.dhcpd_start) - $($_.dhcpd_stop)"}} + } + } + + if ($lan) { + $lanTable = New-GCITSITGTableFromArray -Array $lan -HeaderColour $TableHeaderColour + } + else { + $lanTable = $null + } + if ($wan) { + $wanTable = New-GCITSITGTableFromArray -Array $wan -HeaderColour $TableHeaderColour + } + else { + $wanTable = $null + } + if ($s2svpn) { + $s2svpnTable = New-GCITSITGTableFromArray -Array $s2svpn -HeaderColour $TableHeaderColour + } + else { + $s2svpnTable = $null + } + if ($c2svpn) { + $c2svpnTable = New-GCITSITGTableFromArray -Array $c2svpn -HeaderColour $TableHeaderColour + } + else { + $c2svpnTable = $null + } + + [array]$existingAssets = Get-GCITSITGItem -Resource "flexible_assets?filter[organization_id]=$itGlueOrgID&filter[flexible_asset_type_id]=$UnifiSiteAsset" + + $SiteAsset = New-GCITSITGUnifiSiteAsset -OrganisationId $itGlueOrgID -SiteName $name -Devices $devicesTable -WifiNetworks $wifiTable -PortForwarding $portForwardTable -Alarms $alarmsTable -SiteId $site.name -WanInfo $wanTable -LanInfo $LanTable -SiteToSiteVPN $s2svpnTable -RemoteUserVPN $c2svpnTable + + if ($existingassets.attributes.traits.'site-id' -contains $site.name) { + Write-Host "Updating Item" + $existingItem = $existingAssets | Where-Object {$_.attributes.traits.'site-id' -contains $site.name} + $updateItem = Update-GCITSITGItem -resource flexible_assets -body $SiteAsset -existingItem $existingItem + } + else { + Write-Host "Creating New Item" + $newItem = New-GCITSITGItem -resource flexible_assets -body $SiteAsset + } + } +} \ No newline at end of file diff --git a/UniFiSync/UniFiSharePointMapping.ps1 b/UniFiSync/UniFiSharePointMapping.ps1 new file mode 100644 index 0000000..c8368a2 --- /dev/null +++ b/UniFiSync/UniFiSharePointMapping.ps1 @@ -0,0 +1,294 @@ +<# This script will connect to IT Glue, your Unifi Controller and SharePoint and attempt to match your Unifi Sites with the appropriate IT Glue Organisation. It will then add the site and its possible IT Glue organisation match to a SharePoint List called 'Unifi - IT Glue Site Match Register' within your root SharePoint site (eg. yourtenantname.sharepoint.com). It will first attempt to match the name of the site with the name of the customer. If it can't find a match, it will try to match the MAC addresses from the configurations in IT Glue against any of the MAC addresses of the most recent 200 devices that have connected to your Unifi site. If it still cannot find a match, it will add the site to the SharePoint list where you can manually match it. #> + +# IT Glue Details +$ITGbaseURI = "https://api.itglue.com" +$key = "EnterITGlueAPIKeyHere" + + +# SharePoint Details +$client_id = "EnterSharePointClientIDHere" +$client_secret = "EnterSharePointClientSecretHere" +$tenant_id = "EnterAzureADTenantIDHere" +$ITGOrgRegisterListName = "ITGlue Org Register" +$ListName = "Unifi - IT Glue match register" +$graphBaseUri = "https://graph.microsoft.com/v1.0/" +$siteid = "root" + +# UniFi Details +$UnifiBaseUri = "https://unifi.yourdomain.com:8443" +$UniFiCredentials = @{ + username = "EnterUnifiAdminUserNameHere" + password = "EnterUnifiAdminPasswordHere" + remember = $true +} | ConvertTo-Json + +$UnifiBaseUri = "$UnifiBaseUri/api" + +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +function New-GCITSSharePointColumn($Name, $Type, $Indexed, $lookupListName, $lookupColumnPrimaryName, $lookupColumnName) { + + $column = [ordered]@{ + name = $Name + indexed = $Indexed + $Type = @{} + } + + if ($lookupListName -and $type -contains "lookup") { + $list = Get-GCITSSharePointList -ListName $lookupListName + if ($list) { + $column.lookup.listId = $list.id + $column.lookup.columnName = $lookupColumnName + } + } + return $column +} + +function New-GCITSSharePointList ($Name, $ColumnCollection) { + $list = @{ + displayName = $Name + columns = $columnCollection + } | Convertto-json -Depth 10 + + $newList = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method POST -Body $list + return $newList +} + +function Remove-GCITSSharePointList ($ListId) { + $removeList = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/$ListId" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method DELETE + return $removeList +} + +function Remove-GCITSSharePointListItem ($ListId, $ItemId) { + $removeItem = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/$ListId/items/$ItemId" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method DELETE + return $removeItem +} + +function New-GCITSSharePointListItem($ItemObject, $ListId) { + + $itemBody = @{ + fields = $ItemObject + } | ConvertTo-Json -Depth 10 + + $listItem = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists/$listId/items" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method Post ` + -Body $itemBody +} + +function Get-GCITSSharePointListItem($ListId, $ItemId, $Query) { + + if ($ItemId) { + $listItem = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items/$ItemId ` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value = $listItem + } + elseif ($Query) { + $listItems = $null + $listItems = Invoke-RestMethod -Uri "$graphBaseUri/sites/$siteid/lists/$listId/items/?expand=fields&`$filter=$Query" ` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value = @() + $value = $listItems.value + if ($listitems."@odata.nextLink") { + $nextLink = $true + } + if ($nextLink) { + do { + $listItems = Invoke-RestMethod -Uri $listitems."@odata.nextLink"` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value += $listItems.value + if (!$listitems."@odata.nextLink") { + $nextLink = $false + } + } until (!$nextLink) + } + } + else { + $listItems = $null + $listItems = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items?expand=fields ` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value = @() + $value = $listItems.value + if ($listitems."@odata.nextLink") { + $nextLink = $true + } + if ($nextLink) { + do { + $listItems = Invoke-RestMethod -Uri $listitems."@odata.nextLink"` + -Method Get -headers $SPHeaders ` + -ContentType application/json + $value += $listItems.value + if (!$listitems."@odata.nextLink") { + $nextLink = $false + } + } until (!$nextLink) + } + } + return $value +} + +function Set-GCITSSharePointListItem($ListId, $ItemId, $ItemObject) { + $listItem = Invoke-RestMethod -Uri $graphBaseUri/sites/$siteid/lists/$listId/items/$ItemId/fields ` + -Method Patch -headers $SPHeaders ` + -ContentType application/json ` + -Body ($itemObject | ConvertTo-Json) + $return = $listItem +} + +function Get-GCITSAccessToken { + $authority = "https://login.microsoftonline.com/$tenant_id" + $tokenEndpointUri = "$authority/oauth2/token" + $resource = "https://graph.microsoft.com" + $content = "grant_type=client_credentials&client_id=$client_id&client_secret=$client_secret&resource=$resource" + $graphBaseUri = "https://graph.microsoft.com/v1.0" + $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing + $access_token = $response.access_token + return $access_token +} +function Get-GCITSSharePointList($ListName) { + $list = Invoke-RestMethod ` + -Uri "$graphBaseUri/sites/$siteid/lists?expand=columns&`$filter=displayName eq '$ListName'" ` + -Headers $SPHeaders ` + -ContentType "application/json" ` + -Method GET + $list = $list.value + return $list +} + +function Get-ITGlueItem($Resource) { + $array = @() + + $body = Invoke-RestMethod -Method get -Uri "$ITGbaseUri/$Resource" -Headers $headers -ContentType application/vnd.api+json + $array += $body.data + Write-Host "Retrieved $($array.Count) IT Glue items" + + if ($body.links.next) { + do { + $body = Invoke-RestMethod -Method get -Uri $body.links.next -Headers $headers -ContentType application/vnd.api+json + $array += $body.data + Write-Host "Retrieved $($array.Count) IT Glue items" + } while ($body.links.next) + } + return $array +} + +$access_token = Get-GCITSAccessToken +$SPHeaders = @{Authorization = "Bearer $access_token"} + +$list = Get-GCITSSharePointList -ListName $ListName +$ITGOrgRegisterList = Get-GCITSSharePointList -ListName $ITGOrgRegisterListName + +if (!$list -and $ITGOrgRegisterList) {g + Write-Output "List not found, creating List" + # Initiate Columns + $columnCollection = @() + $columnCollection += New-GCITSSharePointColumn -Name UnifiSiteName -Type text -Indexed $true + $columnCollection += New-GCITSSharePointColumn -Name ITGlue -Type lookup -Indexed $true -lookupListName $ITGOrgRegisterListName -lookupColumnName Title + $List = New-GCITSSharePointList -Name $ListName -ColumnCollection $columnCollection +} +else { + Write-Output "SharePoint list exists" +} + +$headers = @{ + "x-api-key" = $key +} + +if ($ITGOrgRegisterList) { + Write-Host "Getting IT Glue Configurations" + #$configurations = Get-ITGlueItem -Resource configurations + Write-Host "Getting IT Glue Organisations" + #$orgs = Get-ITGlueItem -Resource organizations + + # Connect to UniFi Controller + Invoke-RestMethod -Uri $UnifiBaseUri/login -Method POST -Body $uniFiCredentials -SessionVariable websession + + # Get Sites + $sites = (Invoke-RestMethod -Uri "$UnifiBaseUri/self/sites" -WebSession $websession).data + + foreach ($site in $sites) { + Write-Host "Matching $($site.desc)" + # Check Name of site against IT Glue organisations. if no primary Match, check the devices themselves. + $primarymatch = $orgs | Where-Object {$_.attributes.name -match $site.desc} | Select-Object -First 1 + if ($primarymatch) { + Write-Host "Matched $($site.desc) (UniFi) with $($primarymatch.attributes.name) (IT Glue)" -ForegroundColor Green + } + else { + Write-Host "Couldn't match by name. Attempting to match client devices from $($site.desc) by mac address with IT Glue Configurations" -ForegroundColor Yellow + $matches = @() + $clientDevices = Invoke-RestMethod -Uri "$UnifiBaseUri/s/$($site.name)/rest/user" -WebSession $websession + + foreach ($address in ($clientDevices.data.mac | Select-Object -first 200)) { + $address = $address -replace ":", "-" + $matches += $configurations | Where-Object {$_.attributes.'mac-address' -contains $address} + } + + $primaryMatch = $matches.attributes | group-object organization-id | Select-Object -First 1 + + if ($primaryMatch) { + Write-Host "Matching $($site.desc) (UniFi) with $($primaryMatch.group[0].'organization-name') (IT Glue) with match count of $($primaryMatch.group.count)" -ForegroundColor Green + $primaryMatch = $primarymatch.group[0] + } + } + + # If a match could be found, Create the SharePoint List Item + if ($primaryMatch) { + $ITGlueID = $primaryMatch.'organization-id' + if (!$ITGlueID) { + $ITGlueID = $primarymatch.id + } + $matchingOrg = $orgs | Where-Object {$_.id -eq $ITGlueID} + $query = Get-GCITSSharePointListItem -ListId $ITGOrgRegisterList.id -Query "fields/ITGlueID eq '$itglueid'" + if ($query) { + $existingMatch = Get-GCITSSharePointListItem -ListId $List.id -Query "fields/UnifiSiteName eq '$($site.name)'" + if (!$existingMatch) { + $NewObject = @{ + Title = $site.desc + UnifiSiteName = $site.name + ITGlueLookupId = $query.id + } + New-GCITSSharePointListItem -ListId $list.id -ItemObject $NewObject + } + else { + Write-Host "$($site.desc) is added to the match register already" + } + } + } + else { + Write-Host "Couldn't match $($site.desc)" -ForegroundColor Yellow + $existingMatch = Get-GCITSSharePointListItem -ListId $List.id -Query "fields/UnifiSiteName eq '$($site.name)'" + if (!$existingMatch) { + $NewObject = @{ + Title = $site.desc + UnifiSiteName = $site.name + } + New-GCITSSharePointListItem -ListId $list.id -ItemObject $NewObject + } + else { + Write-Host "$($site.desc) is added to the match register already" + } + } + Write-Host "`n" + } +} +else { + Write-Host "Couldn't find list with name: $ITGOrgRegisterListName" +} \ No newline at end of file