IDandCollabTools.psm1
|
<#
.SYNOPSIS A module containing helper functions for common tasks involving Azure AD and M365 #> function Get-MSALAuthHeader { <# .SYNOPSIS Helper function to generate and return on MS Graph auth header using MSAL.PS .DESCRIPTION This scrip uses the MSAL.PS module to get a Bearer token for the MS Graph API. It uses the combination of tenant ID, client (app) id and certificate thumbprint to do this. As such an existign App Registration is required in Azure AD, and a certificate linked ot that App Registration whose private key is installed in the user's Personal Certififcate Store. The resulting token will have the API permissions assigned to the service principal (i.e. the App Registration), so ensure that App Reg has the require permissions for the task. Requires the module MSAL.PS .PARAMETER tenantID The tenant ID or DNS name of the tenant to target .PARAMETER clientID The ID of the application to use .PARAMETER thumbprint The thumbprint of the certificate associated with the application This certificate must be installed in the user's Personal >> Certificates store on the computer running the script .OUTPUTS An authenticaiton header in hashtable format, containing the Bearer token #> [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string] $tenantID, [Parameter(Mandatory=$true)] [string] $clientID, [Parameter(Mandatory=$false)] [string] $thumbprint, [Parameter (Mandatory=$False)] [string] $secret ) # set our connection details depending on which auhtentication method is used if (-not [string]::IsNullOrEmpty($secret)) { # Set up token request using the secret $securesecret = ConvertTo-SecureString -String $secret -AsPlainText -Force $connectionDetails = @{ 'TenantId' = $tenantID 'ClientId' = $clientID 'ClientSecret' = $securesecret } } elseif (-not [string]::IsNullOrEmpty($thumbprint)) { # Set path to certificate $path = "Cert:\CurrentUser\My\" + $thumbprint # Set up token request $connectionDetails = @{ 'TenantId' = $tenantID 'ClientId' = $clientID 'ClientCertificate' = Get-Item -Path $path } } else { Write-Error "Get-MSALAuthHeader: Neither a secret nor a thumbprint were provided - cannot continue" return } $token = Get-MsalToken @connectionDetails # prepare auth header for main query $MSALAuthHeader = @{ 'Authorization' = $token.CreateAuthorizationHeader() } return $MSALAuthHeader } function Connect-MgGraphAsMsi { <# .SYNOPSIS Get a Bearer token for MS Graph for a Managed Identity and connect to MS Graph. .DESCRIPTION Intended for use in Azure Automation Runbooks. Use the Get-AzAccessToken cmdlet to acquire a Bearer token for the MSI running the script/Runbook, then runs Connect-MgGraph using that token to connect the Managed Identity to MS Graph via the PowerShell SDK. Optinally, returns the acquired token. .NOTES When returning the token for use with Connect-MgGraph, you will need to convert the token property to a SecureString object .EXAMPLE 'Connect-MgGraphAsMsi' -tenantID $tenantID Connects the Managed Service Identity running the scripot/Runbook to Azure and MgGraph .EXAMPLE '$MgBearerToken = Connect-MgGraphAsMsi -tenantID $tenantID -ReturnAccessToken -Verbose $authHeader = @{ 'Authorization' = "Bearer $($MgBearerToken.token)" }' 1. Connects to Azure using Connect-Az.Account and the MS Graph using Connect-MgGraph, returning the token object. Produces messages for each executed step. 2. Generates an auth header with that token, for e.g. use in direct calls to the MS Graph API via Invoke-Webrequest or Invoke-restMethod .PARAMETER ReturnAccessToken Switch - if present, function will return the BearerToken .PARAMETER tenantID the tenant on which to perform the action, used only when debugging .PARAMETER subscriptionID the subscription in which to perform the action, used only when debugging .OUTPUTS Microsoft.Azure.Commands.Profile.Models.PSAccessToken #> [CmdletBinding()] param ( [Parameter (Mandatory = $False)] [Switch] $ReturnAccessToken, [Parameter (Mandatory=$False)] [string] $tenantID, [Parameter (Mandatory=$False)] [string] $subscriptionID ) # Connect to Azure as the MSI $AzContext = Get-AzContext if (-not $AzContext) { Write-Verbose "Connect-MsgraphAsMsi: No existing connection, creating fresh connection" Connect-AzAccount -Identity } else { Write-Verbose "Connect-MsgraphAsMsi: Existing AzContext found, creating fresh connection" Disconnect-AzAccount | Out-Null Connect-AzAccount -Identity Write-Verbose "Connect-MsgraphAsMsi: Connected to Azure as Managed Identity" } # Get a Bearer token $BearerToken = Get-AzAccessToken -ResourceUrl 'https://graph.microsoft.com/' -TenantId $tenantID # Check that it worked $TokenExpires = $BearerToken | Select-Object -ExpandProperty ExpiresOn | Select-Object -ExpandProperty DateTime Write-Verbose "Bearer Token acquired: expires at $TokenExpires" # Convert the token to a SecureString $SecureToken = $BearerToken.Token | ConvertTo-SecureString -AsPlainText -Force # check for and close any existing MgGraph connections then create fresh connection $MgContext = Get-MgContext if (-not $MgContext) { Write-Verbose "Connect-MsgraphAsMsi: No existing MgContext found, connecting" Connect-MgGraph -AccessToken $SecureToken } else { Write-Verbose "Connect-MsgraphAsMsi: MgContext exists for account $($MgContext.Account) - creating fresh connection" Disconnect-MgGraph | Out-Null # Use the SecureString type for connection to MS Graph Connect-MgGraph -AccessToken $SecureToken Write-Verbose "Connect-MsgraphAsMsi: Connected to MgGraph using token generated by Azure" } # Check that it worked $currentPermissions = Get-MgContext | Select-Object -ExpandProperty Scopes Write-Verbose "Access scopes acquired for MgGraph are $currentPermissions" if ($ReturnAccessToken.IsPresent) { return $BearerToken } } function Get-AadUsersLastSignin { <# .SYNOPSIS Gets the Last Signin Date and Time for all users in an AzureAd .DESCRIPTION Using native calls to the MS Graph REST API using Invoke-Webrequest, this funciton gets all Azure AD users, some of their properties, and the Last Signin Date and time for the account. The LastSigninDateTime property is returned as a DateTime type if present, or a string "NONE" if it is empty (i.e. account has never signed in). Requires a properly-formatted header for auth - see parameters for more details. These properties are returned: UserPrincipalName DisplayName EmployeeID Office City Country AccountEnabled AccountType UsageLocation LastSignInDateTime .EXAMPLE PS> $authHeader = Get-MSALAuthHeader -tenantid contoso.com -clientid <id from your app reg> -thumbprint <certificate thumbprint> PS> $LatestSigninData = Get-AadUsersLastSignin -MgGraphHeader $authHeader Generate an auth header via the function Get-MSALAuthHeader found in the 'IDandCollabTools' module. Connects to MS Graph using Invoke-WebRequest, collects data, and stores it. in the object $LatestSigninData for further processing. See the note on parameter MgGraphHeader for alternative methods of generating a header if required .EXAMPLE PS> $authHeader = Get-MSALAuthHeader -tenantid contoso.com -clientid <id from your app reg> -thumbprint <certificate thumbprint> PS> $LatestSigninData = Get-AadUsersLastSignin -MgGraphHeader $authHeader PS> $LatestSigninData | Export-Csv -Path "$((Get-Date).ToString('yyyy-MM-dd'))_LastLoginDate.csv" -NoTypeInformation Generate an auth header via the function Get-MSALAuthHeader found in the 'IDandCollabTools' module. Connects to MS Graph using Invoke-WebRequest, collects data, and stores it in the object $LatestSigninData. Exports the collected data to a CSV named "current date + LastLoginDate.csv" See the note on parameter MgGraphHeader for alternative methods of generating a header if required .PARAMETER MgGraphHeader An auth header containing a bearer token as Authorization header. Can be generated using the object returned by Get-MsalToken in the module MSAL.PS, using an App Reg and certificate thumbprint, or the functions Connect-MgGraphAsMsi or Get-MSALAuthHeader in the IDandCollabTools module .OUTPUTS A collection containing the collected user data. #> [CmdletBinding()] param ( [Parameter (Mandatory=$True)] $MgGraphHeader ) # Get the first page $currentpage = Invoke-WebRequest -Headers $MgGraphHeader -Method GET -Uri "https://graph.microsoft.com/beta/users?`$select=displayName,userPrincipalName,mail,accountEnabled,employeeId,otherMails,usageLocation,officeLocation,city,country,userType,signInActivity" -UseBasicParsing # Exctract the nextlink if it is there $nextpage = ($currentpage.Content | ConvertFrom-Json) | Select-Object -ExpandProperty '@odata.nextlink' -ErrorAction SilentlyContinue # Store the first page of results $results = ($currentpage.Content | ConvertFrom-Json).Value # Get subsequent pages and nextlinks if they exist while ($nextpage) { $currentpage = Invoke-WebRequest -Headers $MgGraphHeader -Uri $nextpage -UseBasicParsing $nextpage = ($currentpage.Content | ConvertFrom-Json) | Select-Object -ExpandProperty '@odata.nextlink' -ErrorAction SilentlyContinue $results += ($currentpage.Content | ConvertFrom-Json).Value } $LastLoginData = @() # Process the query results so they can be output to file $results | ForEach-Object { $record = $_ $signInActivity = $record.SignInActivity # convert any string-form date-times into DateTime objects Clear-Variable LastLogon -ErrorAction SilentlyContinue if ($signInActivity.LastSignInDateTime) { $LastLogon = [Datetime]$signInActivity.LastSignInDateTime } else { $LastLogon = "NONE" } # custom object to store combined output from different objects $userData = [pscustomobject] @{ "UserPrincipalName" = $record.userPrincipalName "DisplayName" = $record.DisplayName "EmployeeID" = $record.employeeId "Email" = $record.mail "Office" = $record.officeLocation "City" = $record.city "Country" = $record.country "AccountEnabled" = $record.accountEnabled "AccountType" = $record.userType "UsageLocation" = $record.usageLocation "LastSignInDateTime" = $LastLogon } $LastLoginData+=$userData } return $LastLoginData } function Convert-AzureAdSidToObjectId { <# .SYNOPSIS Convert a Azure AD SID to Object ID .DESCRIPTION Converts an Azure AD SID to Object ID. Author: Oliver Kieselbach (oliverkieselbach.com) The script is provided "AS IS" with no warranties. .PARAMETER Sid The SID to convert #> [CmdletBinding()] param([String] $Sid) $text = $sid.Replace('S-1-12-1-', '') $array = [UInt32[]]$text.Split('-') $bytes = New-Object 'Byte[]' 16 [Buffer]::BlockCopy($array, 0, $bytes, 0, 16) [Guid]$guid = $bytes return $guid } function Convert-AzureAdObjectIdToSid { <# .SYNOPSIS Convert an Azure AD Object ID to SID .DESCRIPTION Converts an Azure AD Object ID to a SID. Author: Oliver Kieselbach (oliverkieselbach.com) The script is provided "AS IS" with no warranties. .PARAMETER ObjectID The Object ID to convert #> [CmdletBinding()] param([String] $ObjectId) $bytes = [Guid]::Parse($ObjectId).ToByteArray() $array = New-Object 'UInt32[]' 4 [Buffer]::BlockCopy($bytes, 0, $array, 0, 16) $sid = "S-1-12-1-$array".Replace(' ', '-') return $sid } function Get-AadDeviceLocalGroupMembers { <# .SYNOPSIS This code compensates for MSFT's unwillingness to fix Get-LocalGroupMember for AzureAD object members. If run without parameters, it will return the members of the local Administrators group using the well-known SID. .DESCRIPTION Uses ADSI to fetch all members of a local group. This code compensates for MSFT's unwillingness to fix Get-LocalGroupMember for AzureAD object members Issue here: https://github.com/PowerShell/PowerShell/issues/2996 Credits: @ganlbarone on GitHub for the base code @ConfigMgrRSC on Github for the localisation supplement It will output the SID of AzureAD objects such as roles, groups and users, and any others which cannot be resolved. It will also calculate the ObjectID of AzureAD objects and output those. If run without parameters, it will return the members of the local Administrators group using the well-known SID. .EXAMPLE $results = Get-AadDeviceLocalGroupMembers $results The above will get the members of the local Administrators group using the well-known SID. It stores the output of the function in the $results variable, and outputs the results to console .EXAMPLE $results = Get-AadDeviceLocalGroupMembers -targetGroupSID 'S-1-5-32-555' $results The above will get the members of the local Remote Desktop Users group using the well-known SID. It stores the output of the function in the $results variable, and outputs the results to console .EXAMPLE $results = Get-AadDeviceLocalGroupMembers -targetGroupName 'Remote Desktop Users' $results The above will get the members of the local Remote Desktop Users group using the group name. It stores the output of the function in the $results variable, and outputs the results to console .OUTPUTS System.Management.Automation.PSCustomObject Name MemberType Definition ---- ---------- ---------- Equals Method bool Equals(System.Object obj) GetHashCode Method int GetHashCode() GetType Method type GetType() ToString Method string ToString() Computer NoteProperty string Computer=Workstation1 Domain NoteProperty System.String Domain=Contoso User NoteProperty System.String User=Administrator ObjectID NoteProperty System.String ObjectID=00000000-0000-0000-0000-000000000000 #> [CmdletBinding()] param ( [Parameter (Mandatory=$false)] $targetGroupName, [Parameter (Mandatory=$false)] $targetGroupSID ) # Set our desired $groupName variable dependent on the parameters passed if ($null -eq $targetGroupSID -and $null -eq $targetGroupName) { # if no parameters are passed, get the name of the local admin group using the well-known SID $targetGroupSID = 'S-1-5-32-544' [string]$Groupname = (Get-LocalGroup -SID $targetGroupSID)[0].Name } elseif ($null -ne $targetGroupSID -and $null -eq $targetGroupName) { # if the SID is passed but not the name, get the name of the corresponding group [string]$Groupname = (Get-LocalGroup -SID $targetGroupSID)[0].Name } elseif ($null -ne $targetGroupName -and $null -eq $targetGroupSID) { # if the name is passed but not the SID, use that [string]$Groupname = $targetGroupName } $group = [ADSI]"WinNT://$env:COMPUTERNAME/$Groupname" $groupMembers = $group.Invoke('Members') | ForEach-Object { $path = ([adsi]$_).path $memberSID = $(Split-Path $path -Leaf) $AadObjectID = Convert-AzureAdSidToObjectId -Sid $memberSID -ErrorAction SilentlyContinue [pscustomobject]@{ Computer = $env:COMPUTERNAME Domain = $(Split-Path (Split-Path $path) -Leaf) User = $(Split-Path $path -Leaf) ObjectID = $AadObjectID } } return $groupMembers } function Set-MSDfEDeviceTag { <# .SYNOPSIS Sets a registry value (ItemProperty) to a hard-coded string passed in the parameter '-Tag' .DESCRIPTION Sets a registry value (ItemProperty) to a hard-coded string passed in the parameter '-Tag' The registry value is used by Microsoft Defender for Endpoint (MSDfE) in the Defender console for filtering It will determine if the rewquired key exists, and if not create it then set the desired ItemProperty and value In case the key exists but the ItemProperty does not, it will be created and set to '$value' If the ItemProperty exists but contains an incorrect value, the value will be set to '$value' .PARAMETER tag The tag to be set on the device. Should be the same as the Device group targeted by the script for consistency purposes #> [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string] $Tag ) # Define the registry path, property name and desired value $TagPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows Advanced Threat Protection\DeviceTagging\" $Name = "Group" # Ensure $value is correct before deployment!!! $value = $Tag # If the key does not exist, create it, then add the property and value If (!(Test-Path $TagPath)) { New-Item -Path $TagPath -Force | Out-Null New-ItemProperty -Path $TagPath -Name $name -Value $value -PropertyType String -Force | Out-Null } # If the registry value does not exist but the key does, create the value and add the correct data elseif ((Test-Path $TagPath) -and (!(Get-ItemProperty -Path $TagPath -Name $Name -ErrorAction SilentlyContinue))) { New-ItemProperty -Path $TagPath -Name $name -Value $value -PropertyType String -Force | Out-Null } # If the registry key AND value exist, but the value does not contain the required data, edit it elseif ((Test-Path $TagPath) -and (Get-ItemProperty -Path $TagPath -Name $Name -ErrorAction SilentlyContinue)) { $currentPropertyValue = (Get-ItemProperty -Path $TagPath -Name $Name).$Name if ($currentPropertyValue -ne $value) { Set-ItemProperty -Path $TagPath -Name $Name -Value $value -Force | Out-Null } } } function Get-RecursiveMgDirectReports { <# .SYNOPSIS Gets an employee reporting hierarchy using the Microsoft.Graph PowerShell cmdlets .DESCRIPTION Uses the Get-MgUsersDirectReport cmdlet to recursively get the details of all people who report to a given Manager. Manager is specified using UPN or objectID using the named parameter. Use Connect-MgGraph first to establish a connection with the required MS graph API permissions. Specific properties are returned in a colleciton of PSObjects for export to CSV or onward processing. Properties returned are: AccountEnabled User DisplayName EmployeeID User UPN User Email Manager .EXAMPLE .PARAMETER Manager REQUIIRED: the UPN of the initial manager #> [CmdletBinding()] param ( [Parameter(Mandatory)] [string] $Manager ) $directReports = Get-MgUserDirectReport -UserId $Manager | ForEach-Object { Get-MgUser -UserId $_.Id -Property AccountEnabled, DisplayName, EmployeeId, Mail, Manager, UserPrincipalName | Where-Object {$null -ne $_.EmployeeId} } $Output = foreach ($User in $directReports){ [pscustomobject]@{ Enabled = $User.AccountEnabled UserDisplayName = $User.DisplayName EmpID = $user.EmployeeId UserUPN = $User.UserPrincipalName UserMail = $User.Mail Manager = $Manager } # recurse through each looking for sub-reports GetReportingLine -Manager $User.UserPrincipalName } return $Output } function Get-GraphAadRoles { <# .SYNOPSIS Uses Invoke-RestMethod to fetch a list of Azure AD roles fro the MS Graph API, returning the role name and role id. .DESCRIPTION Uses Invoke-RestMethod to fetch a list of Azure AD roles fro the MS Graph API, returning the role name and role id. Whilst MSFT already provide PowerShell cmdlets for this, this method can be used in scenarios where the MSFT cmdlets are not available, or where the MSFT cmdlets are not suitable (e.g. when using a certificate for authentication). Refer to this link for more information on required permissions: https://docs.microsoft.com/en-us/graph/api/directoryrole-list?view=graph-rest-1.0&tabs=http Makes use of the Get-MSALAuthHeader function to generate the authentication header, which in turn makes use of the Get-MSALAccessToken function from the module MSAL.PS. .PARAMETER ClientId The ClientId of the Azure AD application that has been granted the required permissions. .PARAMETER TenantId The TenantId of the Azure AD tenant that contains the roles to be fetched. .PARAMETER CertThumbprint The thumbprint of the certificate that has been uploaded to the Azure AD application. The corresponding certificate keypair must be installed locally in the CurrentUser\My certificate store. .EXAMPLE PS> $AadRoles = Get-GraphAadRoles -ClientId "00000000-0000-0000-0000-000000000000" -TenantId "00000000-0000-0000-0000-000000000000" -CertThumbprint ABCDEF1234567890ABCDEF1234567890ABCDEF12 .OUTPUTS Selected.System.Management.Automation.PSCustomObject #> [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string] $ClientId, [Parameter(Mandatory=$true)] [string] $TenantId, [Parameter(Mandatory=$False)] [string] $CertThumbprint, [Parameter (Mandatory=$False)] [string] $secret ) # Generate the authentication header if (-not [string]::IsNullOrEmpty($secret)) { $header = Get-MSALAuthHeader -ClientId $clientId -TenantId $tenantId -secret $secret } elseif (-not [string]::IsNullOrEmpty($CertThumbprint)) { $header = Get-MSALAuthHeader -ClientId $clientId -TenantId $tenantId -thumbprint $certThumbprint } else { Write-Error "Get-GraphAadRoles: Neither a secret nor a thumbprint were provided - cannot continue" return } # Fetch the roles $uri = "https://graph.microsoft.com/v1.0/directoryRoles" try { Write-Verbose "Fetching roles from MS Graph API" $roles = Invoke-RestMethod -Uri $uri -Headers $header -Method Get } catch { Write-Error "Failed to fetch roles from MS Graph API. Error: $($_.Exception.Message)" return } if ($roles.value.count -eq 0) { Write-Warning "No roles were returned for tenant $tenantId" return } else { Write-Verbose "Fetched $($roles.value.count) roles from MS Graph API" $roleData = $roles.value | Select-Object displayName, id } # Return the role data return $roleData } Export-ModuleMember -Function 'Get-MSALAuthHeader', 'Connect-MgGraphAsMsi', 'Get-AadUsersLastSignin', 'Convert-AzureAdSidToObjectId', 'Convert-AzureAdObjectIdToSid', 'Get-AadDeviceLocalGroupMembers', 'Set-MSDfEDeviceTag', 'Get-RecursiveMgDirectReports','Get-GraphAadRoles' |