Graph.ps1


[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification='Write host used for colored information message telling user to make a change and remove the message')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '', Justification='Initialization clears drive cache and makes access token available outside the module')]

param()
#Most functions for this are in files based on the application where they would surface
# OneDrive, OneNote, Outlook-Calendar, Outlook-Contacts, Outlook-Mail, Planner, SharePoint, Teams.
# Those in this file don't belong to an application.
Write-Host -ForegroundColor Red "Using the default / sample app ID. You should edit the .PSM1 file and either replace the ID with your own, or remove this message"
$Script:ClientID  = "bf546ecc-067d-4030-9edd-7b0d74913411"  #You can also try "1950a258-227b-4e31-a9cf-717495945fc2" # Well known client ID for PowerShell
#$script:Tenant = Guid-for-your-tennant if the Client ID is set up as below

<#
    You can create an app in Azure AD or at https://apps.dev.microsoft.com/
    I created mine as a native app, with a re-direct URI of https://login.microsoftonline.com/common/oauth2/nativeclient and
    gave it a set of Microsoft graph permissions in Azure AD
 
    You must use this route if you want people outside your Azure AD (Microsoft accounts) to use the app
    If you ONLY Want to work with accounts in Azure AD you can set up your app with these instructions which I lifted from
    https://msunified.net/2018/12/12/post-at-microsoftteams-channel-chat-message-from-powershell-using-graph-api/
    1. Log on to https://portal.azure.com with a GA administrator
    2. Navigate to Azure Active Directory
    3 Go to App registration (Preview)
    4. Click + New registration
    5. Call it PowerShellMSGraphAPI
    6. Leave Redirect URI blank
    7. Go to Authentication and under Redirect URIs choose urn:ietf:wg:oauth:2.0:oob
    8. Click Save
    9. Go to API permissions to grant the required group read and write permissions
    10. Click + Add a permission
    11. Choose Microsoft Graph, Delegated permissions and choose Group.Read.All and ReadWrite.All (remember you need to expand Group)
    12. Click Grant admin Consent from and click Yes
    13. You now have admin consent granted for your tenant
    14 Navigate to Overview
    15 Copy the Application (client) ID Paste it into the script as the value for $Script:ClientID;
    Also copy the tenant ID or domain name and make it the value for $script:Tenant
#>


#To prevent tokens being saved, remove the savePath or Set SaveCreds to FALSE
$Script:SavePath    = Join-Path -Path (Split-Path -Path $profile  -Parent) -ChildPath "graph.xml"
$Script:SaveCreds   = $true

#The scopes requested. You can shorten this of you don't need all things phovided in the module
$script:RequestedScope          = @(
      'Directory.AccessAsUser.All', #Grant same rights to the directory as the user has
           'User.ReadWrite.all',    # Read write users and groups may not be needed
          'Group.ReadWrite.All',    # if Directory is granted
      'Calendars.ReadWrite',
      'Calendars.ReadWrite.Shared'
       'Contacts.ReadWrite',
       'Contacts.ReadWrite.Shared',
          'Files.ReadWrite.All',
           'Mail.ReadWrite',
'MailboxSettings.ReadWrite',
          'Notes.ReadWrite',
          'Notes.Create',
         'People.Read.All',
        'Reports.Read.All',
          'Sites.ReadWrite.All',
          'Sites.Manage.All',       #Needed to create lists.
         'openid',
        'profile',
 'offline_access'
)

#Sometimes when we want to convert an opaque drive ID (e.g. on a file or folder) to a name; save extra calls to the server by caching the id-->name
$global:drivecache  = @{}

if (-not $script:Tenant) {
    #if we are not working against a known Azure AD tennant,
    #we will need windows forms to the display the logon, which limits where we can work
    Add-Type -AssemblyName System.Windows.Forms
}

Function Connect-MSGraph {
    <#
      .Synopsis
        Connects to the Microsoft graph API; supporting Microsoft accounts (Live) and Office 365 (Azure AD)
      .Example
        >Connect-Msgraph
        Gets a new access token for the graph API if there isn't a current one.
        If a refresh token is avaialble from that will be used to re-establish a session, otherwise a
        logon dialog will be presented.
      .Example
        >Connect-Msgraph -forceNew
        Discards existing credentials and displays a logon dialog.
      .Example
        >Connect-Msgraph -CheckOnly
        Returns True or False as an answer to the question "Is there a current session"
      .Example
        >$AccessToken = Connect-MSGraph -PassThru -Verbose
        >Invoke-RestMethod -Method get -Uri "https://graph.microsoft.com/v1.0/me" -headers @{Authorization = "bearer $AccessToken" }
        Returns the accesstoken, and uses it to call the graph API to get the current users details
    #>

    [cmdletbinding()]
    [Outputtype([bool])]
    param (
        #If Specified disposes of any existing connection and creates a new one
        [Switch]$ForceNew,
        #If Specified returns the access token
        [Alias('PT')]
        [Switch]$PassThru,
        #If Specified returns true if we have a current session and false if not
        [Switch]$CheckOnly,
        #If specified, and the tenant is fixed, logs on as a temporary Session using that credential
        [pscredential]$Credential,
        #If specified creates prompts for a login for a temporary session (a login which doesn't save the token or use an existing saved one)
        [Switch]$Temp
    )

    Function Convert-AuthResponse {
        <#
        .Synopsis
            Sets Global variables from REST Response to an OAuth login
        .INPUTS
            Response
        #>

        [CmdletBinding()]
        Param (# The response either read from a file, or fetched from the web service
            [Parameter(Mandatory=$true,ValueFromPipeline=$true)] $Response,
            # A message to send to verbose to say what we did
            [String]$Action = "Processed Response for",
            # Dump the info to an XML file
            [switch]$Save,
            # If the info is fresh from the web, set the expiry time, otherwise don't
            [switch]$SetExpiry
        )
        if ($Save -and $Script:SavePath) {
              Export-Clixml -Path $Script:SavePath  -InputObject $Response -Depth 5
              Write-Verbose -Message "Saving response to $Script:SavePath"
        }
        else {Write-Verbose -Message "Coverting but not saving"}
        if ($Response.access_token) {
            $Script:AccessToken     = $Response.access_token
            $Script:AuthHeader      = 'Bearer ' + $Response.access_token
            $Script:DefaultHeader   = @{Authorization = $Script:AuthHeader}
            $Script:AuthorizedScope = $Response.scope -split " "
        }
        $Script:RefreshToken  = $Response.refresh_token
        if ($setExpiry) { #if we're reading from a file: life in seconds is meaningless, don't set expiry or get the username either
            $Script:TokenExpiry = (Get-Date).AddSeconds([int]$Response.expires_in -60 )
            if ($Response.access_token) {
                Write-Progress -Activity "Authenticating" -Status "Getting Token from Server" -PercentComplete 50
                $Script:GraphUser = (Invoke-RestMethod -Method Get -headers $Script:DefaultHeader -Uri "https://graph.microsoft.com/v1.0/me/")
                $Script:GraphUser.pstypenames.Add('GraphUser')
                Write-Verbose ($action + $Script:GraphUser.UserPrincipalName)
                $Organization     = (Invoke-RestMethod -Method Get -headers $Script:DefaultHeader -Uri "https://graph.microsoft.com/v1.0/organization/").Value
                if ($Organization.id) {
                    $script:TenantId     = $Organization.ID
                    $script:TenantName   = $Organization.DisplayName
                    $Script:WorkOrSchool = $true
                    Write-Verbose -Message "Account is from $($Organization.DisplayName)"
                }
                else {
                    $script:TenantId     = $Null
                    $script:TenantName   = $Null
                    $Script:WorkOrSchool = $false
                    Write-Verbose -Message "Account is from Windows live"
                }
            }
        }
    }

    if ($script:Tenant) {
        $tokenUri   = "https://login.microsoft.com/$script:Tenant/oauth2/token"
    }
    else                {
        #The URIs and parameters are set out at https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
        $CallBackUri = "https://login.microsoftonline.com/common/oauth2/nativeclient"   # windows live used "https://login.live.com/oauth20_desktop.srf"
        $tokenUri    = "https://login.microsoftonline.com/common/oauth2/v2.0/token"     # windows live used "https://login.live.com/oauth20_token.srf"
    }
    if ($ForceNew)      {
        Write-Verbose -Message "ForceNew Specified; removing any existing logon info"
        Remove-item -Path $Script:SavePath -ErrorAction SilentlyContinue
        $Script:RefreshToken = $Script:AccessToken = $Script:TokenExpiry =  $Script:GraphUser = $null
    }
    if ($Temp -or $Credential) {
        $Script:RefreshToken = $Script:AccessToken = $Script:TokenExpiry =  $Script:GraphUser = $null
    }
    $Script:SaveCreds = $saveCreds -and (-not ($Temp -or $Credential))
    <# Scenarios
        A. We have a current access token. Hooray! If called with checkonly return true, if called with passthrough return the token, if called with neither just return
        B. We don't. If called with check only return false. Otherwise we need to logon and that breaks down to
           1. We haven't logged on in this session (no refresh token) but there is a saved refresh token - load it
           2. The access token has expired (or we never had one) but we do have a refersh token (from 1 or from an earlier login) - Make a refresh call
           3 We don't have a current access token, or a referesh token. So we put up a dialog box for the user, and save the token so it can be used in (1).
              As a simple way to prevent tokens being saved, only save if $SavePath contains a path.
           The URLs and logic are described here https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
    #>

    #scenario A. we have a current token.
    if ($Script:AccessToken -and ((Get-Date) -lt $Script:TokenExpiry) ) { # if
        Write-Verbose  -Message ('Existing Access Token is good for {0:N0} seconds.' -f ($Script:TokenExpiry.Subtract([datetime]::Now).totalseconds))
        if     ($PassThru)  {return $script:AccessToken}
        elseif ($CheckOnly) {return $true}
        else                {return}
    }
    elseif     ($CheckOnly) {return $false}

    #Scenario B, 1 we have any tokens but there is a saved one, and we're not logging in with a temporary login (so $Script:SaveCreds is true)
    if ((-not $Script:RefreshToken) -and $Script:SavePath -and $Script:SaveCreds -and (Test-Path -Path $Script:SavePath) ) {
        Write-Verbose -Message "No Refresh token, loading data from $Script:SavePath"
        Import-Clixml -Path $Script:SavePath | Convert-AuthResponse
    }
    #Scenario B, 2 We have a refresh token (might have just loaded it or we had it already. So Refresh)
    if   ($Script:RefreshToken) {
        Write-Progress -Activity "Authenticating" -Status "Getting Token from Server"
        $tokenBody = @{'grant_type' = 'refresh_token'; 'refresh_token' = $Script:RefreshToken ; 'client_id' = $Script:ClientID;}
        if  ($Tenant)  {$tokenBody['resource']     = 'https://graph.microsoft.com'}
        else           {$tokenBody['redirect_uri'] = $CallBackUri}      #some cases need &client_secret=xxxyyyyzzz - but not here
        Invoke-RestMethod  -Method Post -Uri $tokenUri -Body $tokenBody  |  Convert-AuthResponse -Save -SetExpiry -Action "Refreshed token for "
        Write-Progress -Activity "Authenticating" -Status "Getting Token from Server" -Completed
    }
    else { #Scenario B, 3 we need to log on
        if ($script:Tenant) { #if we don't have the tennant we need to display the web UI. If do, we can just prompt for creds
            Write-Verbose -Message "Using a fixed tennant"
            if (-not $Credential) {$Credential = Get-Credential -Message "Please enter your credentials for Office 365" }
            if (-not $Credential) {Write-Warning -Message "Can't login without a credential !"; return}
            Write-Progress -Activity "Authenticating" -Status "Getting Token from Server"
            Invoke-RestMethod -Method Post -Uri $tokenUri -Body @{
                     'grant_type' = 'password'; 'username' = $Credential.username; 'password' = $Credential.GetNetworkCredential().Password;
                     'client_id'  = $clientID;  'resource' = 'https://graph.microsoft.com'
             }  | Convert-AuthResponse -Save:$Script:SaveCreds -SetExpiry -Action "Logged on as "
             Write-Progress -Activity "Authenticating" -Status "Getting Token from Server" -Completed
        }
        else {
            $AuthUri  = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?response_type=code'+
                          '&client_id='    + $Script:ClientID     +
                          '&scope='        + ($script:RequestedScope-join "%20") +
                          '&redirect_uri=' + $CallBackUri # for windows live: https://login.live.com/oauth20_authorize.srf?..."
            $DocComp  = { #script block for the on_document_complete event: Make URI accessible; close the form if URI has a code or an error
                $Script:uri = $web.Url.AbsoluteUri
                if ($Script:uri -match "error=[^&]*|code=[^&]*") {$form.Close() }
            }
            #Create a web browser control pointing at the Auth URI - which will contain the ClientID and be told to send back a code ...
            $pid            | Get-date  | Out-File -Append ~\graph.txt
            Get-date        | Out-File -Append ~\graph.txt
            Get-PSCallStack | Out-File -Append ~\graph.txt
            $web      = New-Object -TypeName System.Windows.Forms.WebBrowser -Property @{Width=600;Height=720;Url=($AuthUri) }
            $form     = New-Object -TypeName System.Windows.Forms.Form       -Property @{Width=800;Height=820}
            $web.Add_DocumentCompleted($DocComp) #Add the event handler to the web control
            $form.Controls.Add($web)             #Add the control to the form
            $form.Add_Shown({$form.Activate()})
           # $form.ShowDialog() | Out-Null

            #$URI will be set by the event handler ... so did we get a code - meaning the user logged in OK - or did we get an error ?
            if     ( $uri -match "error=([^&]*)") {Write-Warning ("Logon returned an error of " + $Matches[1]); return}
            elseif ( $uri -match "code=([^&]*)" ) {# If we got a code, request & process the token for it
                Write-Progress -Activity "Authenticating" -Status "Getting Token from Server"
                Invoke-RestMethod -Method Post -Uri $tokenUri  -Body @{
                    'grant_type'  ='authorization_code';  'code' = $Matches[1];
                    'client_id'   = $Script:ClientID;     'redirect_uri'= $CallBackUri  #some places also neet &client_secret=xxxyyyyzzz
                } | Convert-AuthResponse -Save:$Script:SaveCreds  -SetExpiry -Action "Logged on as "
                Write-Progress -Activity "Authenticating" -Status "Getting Token from Server" -Completed
            }
        }
    }
    if     ($PassThru) {return $script:AccessToken}
    elseif (-not ($Script:AccessToken -and ((Get-Date) -lt $Script:TokenExpiry) )) {
        Write-Warning -Message "It doesn't look like there was a valid access token."
    }
}

Function Show-GraphSession {
    <#
        .Synopsis
            Returns Basic information about the current sesssion
    #>

    [CmdletBinding(DefaultParameterSetName='None')]
    [OutputType([String])]
    Param(
        [Parameter(ParameterSetName='Who')]
        [switch]$Who,
        [Parameter(ParameterSetName='Scopes')]
        [switch]$Scopes
    )
    if (-not $script:AccessToken)   {
        Write-Warning -Message "Not Logged on"
    }
    elseif ($Scopes) {
        $Script:AuthorizedScope
    }
    elseif ($Who) {
        $Script:GraphUser
    }
    else {
        if ($Script:WorkOrSchool)  {'{0} logged on with an Azure AD Account from {1} (Tenant ID {2}).'  -f $Script:GraphUser.UserPrincipalName, $script:TenantName , $script:TenantId }
        else                       {'{0} logged on with a Windows Live account.'                        -f $Script:GraphUser.UserPrincipalName}
        'Access token has an expiry time of: {0}' -f $Script:TokenExpiry
        if ($script:RefreshToken)  {"Refresh token is present."}
        if (-not $Script:SavePath) {"Token is not being saved between sessions."}
        elseif (Test-Path -Path $Script:SavePath) {"Token has been saved."}
        'Token supports these scopes:'
        $Script:AuthorizedScope -join ", "
    }
}

Function Get-GraphOrganization  {
    <#
      .Synopsis
        Gets a summary of organization information from MSGraph
      .Description
        Can use msonline\Get-MsolCompanyInformation instead
        This needs consent to use either the User.Read or the Directory.Read.All scope
      .Example
        >(Get-GraphOrganization).verifiedDomains
        Displays a list of domains in the current subscription
    #>

    [cmdletbinding(DefaultParameterSetName="None")]
    Param(
    )
    Connect-MSGraph
    $webParams = @{ 'uri'     = 'https://graph.microsoft.com/v1.0/organization'
                    'Method'  = 'Get'
                    'Headers' = $Script:DefaultHeader
    }
    $result = Invoke-RestMethod @webParams
    foreach ($org in $result.value) {
        $org.pstypenames.Add('GraphOrganization')
        foreach ($d in $org.verifieddomains) {
            $d.pstypenames.add('GraphDomain')
        }
    }

    $result.value
}

Function Get-GraphDomain {
    <#
      .synopsis
        Gets domains in the current tenant
      .Description
        Requires consent to use at least the Directory.Read.All scope
    #>

    [cmdletbinding()]
    param (
    )
    Connect-MSGraph
    # if user is an admin, can add /NameReferences /serviceConfigurationRecords or /verificationDnsRecords to URI
    $result = Invoke-RestMethod  -Method get -Uri "https://graph.microsoft.com/v1.0/domains"  -headers $Script:DefaultHeader
    foreach ($r in $result.value) {
        $r.psTypeNames.add('GraphDomain')
    }
    $result.value
    # -Uri "https://graph.microsoft.com/v1.0/domains/{domain-name}/domainNameReferences" -Method Get -headers $Script:DefaultHeader
}

Function Get-GraphSKUList {
    <#
      .Synopsis
        Gets the SKUs organization an organization has subscribed to
      .Description
        Equivalent to msonline\Get-MsolAccountSku
        Requires consent to the Directory.Read.All or the Directory.AccessAsUser.All scope
      .Example
        >Get-GraphSKUList | where {($_.prePaidUnits.enabled - $_.consumedunits) -lt 10}
        Lists SKUs where the number of licenses consumed is getting close to the number purchased.
    #>

    [cmdletbinding(DefaultParameterSetName="None")]
    Param ()
    Connect-MSGraph
    $webParams = @{Method = "Get"
                   Headers = $Script:DefaultHeader
    }
    $subscribedSkus =  (Invoke-RestMethod @webParams -Uri "https://graph.microsoft.com/v1.0/subscribedSkus").value
    foreach ($s in $subscribedSkus) {$s.pstypenames.Add("GraphSKU")}

    $subscribedSkus
}

Function Get-GraphSKU {
    <#
      .Synopsis
        Gets details of SKUs organization an organization has subscribed to
      .Example
        Get-GraphSKUList | where skupartnumber -match "enterprise" | Get-GraphSKU -ServicePlans | sort servicePlanName | format-table
        Finds "Enterprise" SKUS and displays their service plans in alphabetical order.
    #>

    [cmdletbinding()]
    Param (
        #The SKU to get either as an ID or a SKU object containing an ID
        [parameter(Mandatory=$true,ValueFromPipeline=$true)]
        $SKU,
        #If specified just returns the Service plans for the SKU, otherwise returns the SKU with a service plans property
        [switch]$ServicePlans
    )
    Begin   {
        Connect-MSGraph
    }
    Process {
        $webParams = @{'Method'  = "Get"
                       'Headers' = $Script:DefaultHeader
        }
        foreach ($s in $sku) {
            if     ($s.id)          {$webParams["uri"] = "https://graph.microsoft.com/v1.0/subscribedSkus/$($s.id)" }
            elseif ($s -is [String]){$webParams["uri"] = "https://graph.microsoft.com/v1.0/subscribedSkus/$s" }
            else   {Write-Warning -Message 'Could not find the SKU ID from the parameter'; return}

            $result  = Invoke-RestMethod @webParams
            $result.pstypenames.Add("GraphSKU")
            foreach($s in $result.ServicePlans) {
                $s.pstypenames.Add("GraphServicePlan")
                Add-Member -InputObject $s -MemberType NoteProperty -Name "skuPartNumber" -Value $result.skuPartNumber
            }

            if ($ServicePlans) {$result.ServicePlans}
            else               {$result }
        }
    }
}

Function Get-GraphReport {
    <#
        .Synopsis
            Use BETA functionality to get reports from MS Graph
        .Example
            >Get-GraphReport -Report MailboxUsageDetail | ft "Display Name", "Storage Used (Byte)"
            Displays mailbox storage used by users - note that
            fields have 'friendly' names which need to be wrapped in quotes
    #>

    [cmdletbinding(DefaultParameterSetName="None")]
    param(
        #The report to Fetch
        [ValidateSet('TeamsUserActivityUserCounts',     'TeamsUserActivityCounts',       'TeamsUserActivityUserDetail',
                     'TeamsDeviceUsageUserDetail',      'TeamsDeviceUsageUserCounts',    'TeamsDeviceUsageDistributionUserCounts',
                    'EmailActivityCounts',              'EmailActivityUserCounts',       'EmailActivityUserDetail',
                    'EmailAppUsageAppsUserCounts',      'EmailAppUsageUserDetail',
                    'MailboxUsageMailboxCounts',        'MailboxUsageDetail',            'MailboxUsageQuotaStatusMailboxCounts', 'MailboxUsageStorage',
                    'Office365ActivationsUserDetail',   'Office365ActivationCounts',     'Office365ActivationsUserCounts',
                    'Office365ActiveUserDetail' ,       'Office365ActiveUserCounts',     'Office365ServicesUserCounts',
                    'Office365GroupsActivityDetail',    'Office365GroupsActivityCounts', 'Office365GroupsActivityGroupCounts',   'Office365GroupsActivityStorage',
                    'Office365GroupsActivityFileCounts','OneDriveActivityUserDetail',    'OneDriveActivityFileCounts',           'OneDriveActivityUserCounts',
                    'OneDriveUsageAccountDetail',       'OneDriveUsageAccountCounts',    'OneDriveUsageFileCounts' ,             'OneDriveUsageStorage',
                    'SharePointActivityUserDetail',     'SharePointActivityFileCounts',  'SharePointActivityUserCounts',         'SharePointActivityPages',
                    'SharePointSiteUsageDetail',        'SharePointSiteUsageFileCounts', 'SharePointSiteUsageSiteCounts',        'SharePointSiteUsageStorage',
                    'SharePointSiteUsagePages')]
        [parameter(Mandatory=$true)]
        $Report,
        #Date for the report - this should be a date in the past 30 days. If specified, -Period is ignored. Reports ending in Count, Storage or pages don't support date filtering
        [DateTime]$Date,
        #The range of time for the report in the form "Dn" where n is the number of days. The default is D7, except for Office365Activation activation reports
        [ValidateSet("D7", "D30", "D90", "D180")]
        $Period
    )
    if (-not $Script:WorkOrSchool) {Write-Warning   -Message "This command only works when you are logged in with a work or school account." ; return    }
    Connect-MSGraph
    $webParams = @{'Method' = 'Get'
                   'Headers' = $Script:DefaultHeader
    }
    if     ($Date)    {
        if ($report -match 'Counts$|Pages$|Storage$') {Write-Warning -Message 'Reports ending with Counts, Pages or Storage do not support date filtering' ; return }
        if ($report -match '^Office365Activation')    {Write-Warning -Message 'Office365Activation Reports do not support any filtering.'  ; return }
        if ($report -eq    'MailboxUsageDetail')      {Write-Warning -Message 'MailboxUsageDetail does not support date filtering.' ; return}
        $webParams['Uri'] = "https://graph.microsoft.com/beta/reports/get{0}(date={1:yyyy-MM-dd})" -f $Report , $Date
    }
    elseif ($Period)  {
        if ($report -match '^Office365Activation')    {Write-Warning -Message 'Office365Activation Reports do not support any filtering.'  ; return }
        $webParams['Uri'] = "https://graph.microsoft.com/beta/reports/get{0}(period='{1}')"        -f $Report , $Period
    }
    else              {
      if ($report -notmatch '^Office365Activation')  {
        $webParams['Uri'] = "https://graph.microsoft.com/beta/reports/get{0}(period='d7')"         -f $Report
      }
      else {
        $webParams['Uri'] = "https://graph.microsoft.com/beta/reports/get{0}"                      -f $Report
      }
    }
    (Invoke-RestMethod @webParams).substring(3) | ConvertFrom-Csv
}

Function Get-GraphSignInLog {
    <#
      .synopsis
        Gets the audit log -requires a priviledged account
      .Description
        This command calls https://graph.microsoft.com/beta/auditLogs/signIns
        which requires consent to use the AuditLog.Read.All Scope this can only be granted to Azure AD apps.
      .Example
        >
        >Get-GraphSignInLog |
        > select Date,UserPrincipalName,appDisplayName,ipAddress,clientAppUsed,browser,device,city,lat,long |
        > Export-Excel -Path .\signin.xlsx -AutoSize -IncludePivotTable -PivotTableName Signins -PivotRows appdisplayName -PivotColumns browser -PivotData @{date='Count'} -show
 
        Gets the sign-in Log and exports it Excel, creating a PivotTable
    #>

    [cmdletbinding()]
    param (
    )
    Connect-MSGraph
    $i = 1
    Write-Progress -Activity 'Getting Sign-in Auditlog'
    try   { $result  = Invoke-RestMethod  -Method get -Uri "https://graph.microsoft.com/beta/auditLogs/signIns"  -headers $Script:DefaultHeader  }
    catch {
        if ($_.exception.response.statuscode.value__ -eq 401) {
            Write-Warning -Message "The server responded 'Unauthorized' - check that $($script:GraphUser.userPrincipalName) has rights to access the log."; return
        }
    }
    $records = $result.value
    while ($result.'@odata.nextLink') {
        $i ++
        Write-Progress -Activity 'Getting Sign-in Auditlog' -CurrentOperation "Page $i"
        $result   = Invoke-RestMethod  -Method get -Uri $result.'@odata.nextLink'  -headers $Script:DefaultHeader
        $records += $result.value
    }
    foreach ($r in $records) {
        $r.pstypenames.add('GraphSigninLog')
        Add-Member -InputObject $r -MemberType ScriptProperty -Name City    -Value {$this.location.city}
        Add-Member -InputObject $r -MemberType ScriptProperty -Name State   -Value {$this.location.state}
        Add-Member -InputObject $r -MemberType ScriptProperty -Name Country -Value {$this.location.countryOrRegion}
        Add-Member -InputObject $r -MemberType ScriptProperty -Name Lat     -Value {$this.location.geoCoordinates.latitude}
        Add-Member -InputObject $r -MemberType ScriptProperty -Name Long    -Value {$this.location.geoCoordinates.longitude}
        Add-Member -InputObject $r -MemberType ScriptProperty -Name Browser -Value {$this.deviceDetail.browser}
        Add-Member -InputObject $r -MemberType ScriptProperty -Name Device  -Value {$this.deviceDetail.displayName;}
        Add-Member -InputObject $r -MemberType ScriptProperty -Name Date    -Value {[datetime]$this.createdDateTime}
    }
    Write-Progress -Activity 'Getting Sign-in Auditlog'-Completed

    $records
}


Function Get-GraphDirectoryLog {
    <#
      .synopsis
        Gets the Directory audit log -requires a priviledged account
      .Description
        This command calls https://graph.microsoft.com/beta/auditLogs/directoryAudits
        which requires consent to use the AuditLog.Read.All Scope this can only be granted to Azure AD apps.
 
    #>

    [cmdletbinding()]
    param (
    )
    Connect-MSGraph
    $i = 1
    Write-Progress -Activity 'Getting Directory Audits log'
    try   { $result  = Invoke-RestMethod  -Method get -Uri "https://graph.microsoft.com/beta/auditLogs/directoryAudits"  -headers $Script:DefaultHeader  }
    catch {
        if ($_.exception.response.statuscode.value__ -eq 401) {
            Write-Warning -Message "The server responded 'Unauthorized' - check that $($script:GraphUser.userPrincipalName) has rights to access the log."; return
        }
    }
    $records = $result.value
    while ($result.'@odata.nextLink') {
        $i ++
        Write-Progress -Activity 'Getting Directory Audits log' -CurrentOperation "Page $i"
        $result   = Invoke-RestMethod  -Method get -Uri $result.'@odata.nextLink'  -headers $Script:DefaultHeader
        $records += $result.value
    }
    $defaultProperties = @('Date','User','ActivityDisplayName','result')
    $defaultDisplayPropertySet = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet',[string[]]$defaultProperties)
    $psStandardMembers = [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet)
    foreach ($r in $records) {
        $r.pstypenames.add('GraphDirectoryLog')
        Add-Member -InputObject $r -MemberType ScriptProperty -Name User              -Value {$this.initiatedBy.user.userPrincipalName}
        Add-Member -InputObject $r -MemberType ScriptProperty -Name Date              -Value {[datetime]$this.activityDateTime}
        Add-Member -InputObject $r -MemberType MemberSet      -Name PSStandardMembers -Value $PSStandardMembers
    }
    Write-Progress -Activity 'Getting Directory Audits log' -Completed

    $records
}

<#
 Others to explore
 (irm -Method Get -headers $Script:DefaultHeader -Uri "https://graph.microsoft.com/v1.0/directoryRoles").value | ft id,displayname,description https://docs.microsoft.com/en-us/graph/api/directoryrole-list?view=graph-rest-1.0
 (irm -Method Get -headers $Script:DefaultHeader -Uri "https://graph.microsoft.com/v1.0/directoryRoleTemplates").value | sort displayname | ft id,displayname,description
 (irm -Method Get -headers $Script:DefaultHeader -Uri "https://graph.microsoft.com/v1.0/groupsettingTemplates").value | ft displayname,description -wrap -aut
 (irm -Method Get -headers $Script:DefaultHeader -Uri "https://graph.microsoft.com/v1.0/devices").value | ft approximateLastSignInDateTime,displayName,operatingsystem,operatingsystemversion
 (irm -Method Get -headers $Script:DefaultHeader -Uri "https://graph.microsoft.com/v1.0/devices/c2221377-d362-42e7-8e16-e7d6abf80e61/registeredOwners").value
 (irm -Method Get -headers $Script:DefaultHeader -Uri "https://graph.microsoft.com/v1.0/devices/c2221377-d362-42e7-8e16-e7d6abf80e61/memberof").value
 (irm -Method Get -headers $Script:DefaultHeader -Uri "https://graph.microsoft.com/v1.0/me/owneddevices").value | ft displayname,operatingsystemversion,trusttype
#>


Get-PSCallStack | Out-File -Append ~\graph.txt


Connect-MSGraph
$Global:AccessToken = $script:AccessToken