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=$true)] [string] $thumbprint ) # Set path to certificate $path = "Cert:\CurrentUser\My\" + $thumbprint # Set up token request $connectionDetails = @{ 'TenantId' = $tenantID 'ClientId' = $clientID 'ClientCertificate' = Get-Item -Path $path } $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 $LatestSigninData = Get-AadUsersLastSignin -MgGraphHeader $authHeader Connects to MS Graph using Invoke-WebRequest, collects data, and stores it in the object $LatestSigninData for further processing. .EXAMPLE $LatestSigninData = Get-AadUsersLastSignin -MgGraphHeader $authHeader $LatestSigninData | Export-Csv -Path "$((Get-Date).ToString('yyyy-MM-dd'))_LastLoginDate.csv" -NoTypeInformation 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" .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 function Connect-MgGraphAsMsi 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-LocalAdmins { <# .SYNOPSIS This code compensates for MSFT's laziness in fixing a flagship product. Issue here: https://github.com/PowerShell/PowerShell/issues/2996 Credits: @ganlbarone on GitHub for the base code @ConfigMgrRSC on Github for the localisation supplement .DESCRIPTION The script uses ADSI to fetch all members of the local Administrators group. MSFT are aware of this issue, but have closed it without a fix. It will output the SID of AzureAD objects such as roles, groups and users, and any others which cnanot be resolved. the AAD principals' SIDS need to be mapped to identities using MS Graph. Designed to run from the Intune MDM and thus accepts no parameters. .EXAMPLE $results = Get-localAdmins $results The above will store the output of the function in the $results variable, and output 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 #> [CmdletBinding()] $GroupSID='S-1-5-32-544' [string]$Groupname = (get-localgroup -SID $GroupSID)[0].Name $group = [ADSI]"WinNT://$env:COMPUTERNAME/$Groupname" $admins = $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 $admins } 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 .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 } Export-ModuleMember -Function 'Get-MSALAuthHeader', 'Connect-MgGraphAsMsi', 'Get-AadUsersLastSignin', 'Convert-AzureAdSidToObjectId', 'Convert-AzureAdObjectIdToSid', 'Get-LocalAdmins', 'Set-MSDfEDeviceTag', 'Get-RecursiveMgDirectReports' |