MSApiConnect.psm1

$script:ModuleRoot = $PSScriptRoot
$script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\MSApiConnect.psd1").ModuleVersion

# Detect whether at some level dotsourcing was enforced
$script:doDotSource = Get-PSFConfigValue -FullName MSApiConnect.Import.DoDotSource -Fallback $false
if ($MSApiConnect_dotsourcemodule) { $script:doDotSource = $true }

<#
Note on Resolve-Path:
All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator.
This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS.
Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist.
This is important when testing for paths.
#>


# Detect whether at some level loading individual module files, rather than the compiled module was enforced
$importIndividualFiles = Get-PSFConfigValue -FullName MSApiConnect.Import.IndividualFiles -Fallback $false
if ($MSApiConnect_importIndividualFiles) { $importIndividualFiles = $true }
if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true }
if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true }
    
function Import-ModuleFile
{
    <#
        .SYNOPSIS
            Loads files into the module on module import.
         
        .DESCRIPTION
            This helper function is used during module initialization.
            It should always be dotsourced itself, in order to proper function.
             
            This provides a central location to react to files being imported, if later desired
         
        .PARAMETER Path
            The path to the file to load
         
        .EXAMPLE
            PS C:\> . Import-ModuleFile -File $function.FullName
     
            Imports the file stored in $function according to import policy
    #>

    [CmdletBinding()]
    Param (
        [string]
        $Path
    )
    
    $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath
    if ($doDotSource) { . $resolvedPath }
    else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) }
}

#region Load individual files
if ($importIndividualFiles)
{
    # Execute Preimport actions
    . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\preimport.ps1"
    
    # Import all internal functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Import all public functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore))
    {
        . Import-ModuleFile -Path $function.FullName
    }
    
    # Execute Postimport actions
    . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\postimport.ps1"
    
    # End it here, do not load compiled code below
    return
}
#endregion Load individual files

#region Load compiled code
<#
This file loads the strings documents from the respective language folders.
This allows localizing messages and errors.
Load psd1 language files for each language you wish to support.
Partial translations are acceptable - when missing a current language message,
it will fallback to English or another available language.
#>

Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'MSApiConnect' -Language 'en-US'

<#
.SYNOPSIS
Convert UTC Time to Local Time
 
.DESCRIPTION
Convert UTC Time to Local Time
 
.PARAMETER UTCTime
UTC DATE Time Value
 
.EXAMPLE
Convert UTC time to Local Time
Convert-UTCtoLocal -UTCTime "2019-02-27 1:00:00"
 
.NOTES
PS5 Version of : https://devblogs.microsoft.com/scripting/powertip-convert-from-utc-to-my-local-time-zone/
#>


function Convert-UTCtoLocal
{
    [OutputType([DateTime])]
    [CmdletBinding()]
    Param(
        [parameter(Mandatory=$true)]
        [String] $UTCTime
    )

    $LocalTime = [System.TimeZoneInfo]::ConvertTimeFromUtc($UTCTime, $(Get-TimeZone))
    Return $LocalTime
}


<#
.SYNOPSIS
Convert Date Time to Unix Time
 
.DESCRIPTION
Convert Date Time to Unix Time
 
.PARAMETER ctime
Date and Time to convert to UNIX time
 
.EXAMPLE
Convert Time to Unix Time
ConvertFrom-Ctime -ctime "2019-02-27 1:00:00"
 
.NOTES
From : https://stackoverflow.com/questions/4192971/in-powershell-how-do-i-convert-datetime-to-unix-time/
#>


function ConvertFrom-Ctime
{
    [CmdletBinding()]
    param (
        [Int]
        $ctime
    )
    [datetime]$epoch = '1970-01-01 00:00:00'
    [datetime]$result = $epoch.AddSeconds($Ctime)
    return $result
}


<#
.SYNOPSIS
Do Proxy Auth with Default Network Credential
 
.DESCRIPTION
Do Proxy Auth with Default Network Credential
 
.EXAMPLE
Use Default Proxy with network Credential in Powershell
Get-AuthProxy
 
.NOTES
TODO - Add More scenario and add this as a option to other Function
#>


Function Get-AuthProxy
{
    [CmdletBinding()]
    param (

    )
    #Do Proxy Auth with Default Network Credential
    $wc = New-Object System.Net.WebClient
    $wc.Proxy.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials
}


<#
.SYNOPSIS
Find Azure Directory Authentication Librairy DLL
 
.DESCRIPTION
Find Azure Directory Authentication Librairy DLL from the installed module to prevent conflict
Install AzureAD PS Module if not installed
 
.PARAMETER InstallPreview
Switch to force the installation of AzureADPreview Module if no module are found.
 
.EXAMPLE
Return the Azure DLL Location
Get-AzureADDLL
 
.NOTES
TODO - Add dll in the bin folder and leverage this one if none are found
#>


Function Get-AzureADDLL
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $false)]
        [Switch]
        $InstallPreview
    )
    [array]$AzureADModules = Get-Module -ListAvailable | where-object {$_.name -eq "AzureAD" -or $_.name -eq "AzureADPreview"}
    Try
    {
        if($AzureADModules.count -eq 0 -and $InstallPreview -eq $True)
        {
            Install-Module AzureADPreview -Confirm:$False -Force
        }
        elseif($AzureADModules.count -eq 0)
        {
            Install-Module AzureAD -Confirm:$False -Force
        }
    }
    Catch
    {
        Throw "Can't find Azure AD DLL. Install the module manually 'Install-Module AzureAD'"
    }

    $AzureDLL = join-path (($AzureADModules | sort-object version -Descending | Select-object -first 1).Path | split-Path) Microsoft.IdentityModel.Clients.ActiveDirectory.dll
    Return $AzureDLL
}


<#
.SYNOPSIS
Get the UPN for the current logged user
 
.DESCRIPTION
Get the UPN for the current logged user. This PC need to be Domain-joined
 
.EXAMPLE
Return UserPrincipalName for the currently Logged user.
Get-CurrentUPN
 
.NOTES
#
#>


Function Get-CurrentUPN
{
    [CmdletBinding()]
    param (

    )
    $UserPrincipalName = ([ADSI] "LDAP://<SID=$(([System.Security.Principal.WindowsIdentity]::GetCurrent()).User)>").userPrincipalName
    Return $UserPrincipalName
}


<#
.SYNOPSIS
Send a Web Request to retrieve well known Tenant Login endpoint
 
.DESCRIPTION
Send a Web Request to retrieve well known Tenant Login endpoint
 
.PARAMETER TenantName
You need to specify the Tenant Name, Tenant ID or Registered Domain name on your Azure or Office 365 Tenant
 
.PARAMETER LoginSource
You can choose to leverage EvoSTS (work with both On-Premises and Azure AD) or MicrosoftOnline (Cloud Only)
 
.EXAMPLE
Retrieve the Autorization Endpoint for the tenant contoso.com
Get-TenantLoginEndPoint -TenantName contoso.com | Select authorization_endpoint
 
.NOTES
#
#>


Function Get-TenantLoginEndPoint
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $True)]
        [System.String]
        $TenantName,
        [Parameter(Mandatory = $false)]
        [System.String]
        [ValidateSet('MicrosoftOnline','EvoSTS')]
        $LoginSource = "EvoSTS"
    )
    $TenantInfo = @{}
    if($LoginSource -eq "EvoSTS")
    {
        $webrequest = Invoke-WebRequest -Uri https://login.windows.net/$($TenantName)/.well-known/openid-configuration -UseBasicParsing
    }
    else {
        $webrequest = Invoke-WebRequest -Uri https://login.microsoftonline.com/$($TenantName)/.well-known/openid-configuration -UseBasicParsing
    }
    if($webrequest.StatusCode -eq 200){
        $TenantInfo = $webrequest.Content |ConvertFrom-Json
    }
    Return $TenantInfo
}


<#
.SYNOPSIS
Validate than the tenant name is a domain, else add .onmicrosoft.com
 
.DESCRIPTION
Validate than the tenant name is a domain, else add .onmicrosoft.com
 
.PARAMETER TenantName
Value to validate
 
.EXAMPLE
Validate if the tenant name is correct or add .onmicrosoft.com if it isn't a domain
Test-TenantName -TenantName Contoso
 
.NOTES
#
#>


Function Test-TenantName
{
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $false)]
        [System.String]
        $TenantName
    )
    if($TenantName -notlike "*.onmicrosoft.com"){
        $TenantName = $TenantName + ".onmicrosoft.com"
    }
    Return $TenantName
}


<#
.SYNOPSIS
Connect to Exchange Online without the Click2Run
 
.DESCRIPTION
Connect to Exchange Online without the Click2Run
 
.PARAMETER UserPrincipalName
UserPrincipalName of the Admin Account
 
.EXAMPLE
Connect to Exchange Online
Connect-EXOPSSession -UserPrincipalName admin@contoso.com
 
.NOTES
Ref : https://www.michev.info/Blog/Post/1771/hacking-your-way-around-modern-authentication-and-the-powershell-modules-for-office-365
Only Support User Connection no Application Connect (As Of : 2019-05)
 
#>


Function Connect-EXOPSSession
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")]
    [cmdletbinding()]
    param (
    [parameter(Mandatory=$False)]
        $UserPrincipalName
    )
    if([string]::IsNullOrEmpty($UserPrincipalName))
    {
        $UserPrincipalName = Get-CurrentUPN
    }
    if([string]::IsNullOrEmpty($UserPrincipalName))
    {
        Throw "Can't determine User Principal Name, please use the parameter -UserPrincipalName to specify it."
    }
    else
    {
        $resourceUri = "https://outlook.office365.com"
        $redirectUri = "urn:ietf:wg:oauth:2.0:oob"
        $clientid = "a0c73c16-a7e3-4564-9a95-2bdf47383716"

        if($Script:UPNEXOHeader){
            # Setting DateTime to Universal time to work in all timezones
            $DateTime = (Get-Date).ToUniversalTime()

            # If the authToken exists checking when it expires
            $TokenExpires = ($Script:UPNEXOHeader.ExpiresOn.datetime - $DateTime).Minutes
            $UPNMismatch = $UserPrincipalName -ne $Script:UPNEXOHeader.UserID
            $AppIDMismatch = $ClientID -ne $Script:UPNEXOHeader.AppID
            if($TokenExpires -le 0 -or $UPNMismatch -or $AppIDMismatch){
                Write-PSFMessage -Level Host -Message "Authentication need to be refresh" -ForegroundColor Yellow
                $Script:UPNEXOHeader = Get-OAuthHeaderUPN -clientId $ClientID -redirectUri $redirectUri -resourceAppIdURI $resourceURI -UserPrincipalName $UserPrincipalName
            }
        }
        # Authentication doesn't exist, calling Get-GraphAuthHeaderBasedOnUPN function
        else {
            $Script:UPNEXOHeader = Get-OAuthHeaderUPN -clientId $ClientID -redirectUri $redirectUri -resourceAppIdURI $resourceURI -UserPrincipalName $UserPrincipalName
        }
        $Result = $Script:UPNEXOHeader

        $Authorization =  $Result.Authorization
        $Password = ConvertTo-SecureString -AsPlainText $Authorization -Force
        $Ctoken = New-Object System.Management.Automation.PSCredential -ArgumentList $UserPrincipalName, $Password
        $EXOSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/PowerShell-LiveId?BasicAuthToOAuthConversion=true -Credential $Ctoken -Authentication Basic -AllowRedirection
        Import-Module (Import-PSSession $EXOSession -AllowClobber) -Global -DisableNameChecking
    }
}


<#
.SYNOPSIS
Connect to Intune with Powershell
 
.DESCRIPTION
Connect to Intune with Powershell
 
.PARAMETER UserPrincipalName
UserPrincipalName of the Admin Account
 
.EXAMPLE
Connect to Intune API
Connect-Intune -UserPrincipalName admin@contoso.com
 
.NOTES
For a complete Intune module : https://github.com/Microsoft/Intune-PowerShell-SDK
Checking if authToken exists before running authentication
Only Support User Connection no Application Connect (As Of : 2019-05)
#>


Function Connect-Intune{
    [CmdletBinding()]
    param
(
    [Parameter(Mandatory = $False)]
    [string]$UserPrincipalName
)
    
    [string]$clientId = "d1ddf0e4-d672-4dae-b554-9d5bdfd93547"
    [string]$redirectUri = "urn:ietf:wg:oauth:2.0:oob"
    [string]$resourceUri = "https://graph.microsoft.com"

    if($Script:IntuneAuthToken){
        # Setting DateTime to Universal time to work in all timezones
        $DateTime = (Get-Date).ToUniversalTime()

        # If the authToken exists checking when it expires
        $TokenExpires = ($Script:IntuneAuthToken.ExpiresOn.datetime - $DateTime).Minutes

            if($TokenExpires -le 0){

            Write-PSFMessage -Level Host -Message "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow
            $Script:IntuneAuthToken = Get-OAuthHeaderUPN -clientId $clientid -redirectUri $redirectUri -resourceAppIdURI $resourceUri -UserPrincipalName $UserPrincipalName
            }
    }
    # Authentication doesn't exist, calling Get-GraphAuthHeaderBasedOnUPN function
    else {
        $Script:IntuneAuthToken = Get-OAuthHeaderUPN -clientId $clientid -redirectUri $redirectUri -resourceAppIdURI $resourceUri -UserPrincipalName $UserPrincipalName
    }
    $Script:IntuneAuthToken
}


<#
.SYNOPSIS
Send Query to the Microsofrt Graph Security API
 
.DESCRIPTION
Send Query to the Microsofrt Graph Security API with your UPN or Azure AD Application
 
.PARAMETER TenantName
For Azure AD Application Authentication, you need to specify the Tenant Name, Tenant ID or Registered Domain name on your Azure or Office 365 Tenant
 
.PARAMETER Query
Optional, Additionnal Graph URL to pass to https://graph.microsoft.com/V1.0/security like "alerts", if no value securescores are used
Currently, there no validation, so refer to the docs.microsoft.com API
#TODO - add url for docs
 
.PARAMETER ClientID
This is the Client ID (Application ID) of the registered Azure AD Application.
The Application need to have the right permission in your tenant.
#TODO = Document the minimal app permission
 
.PARAMETER ClientSecret
If you are leveraging an Azure AD Application with Client Secret authentication, you need to provide the Secret here
 
.PARAMETER CertificatePath
If you are leveraging an Azure AD Application with Certificate authentication, you need to provide the Certificate Path here
 
.PARAMETER CertificatePassword
If you are leveraging an Azure AD Application with Certificate authentication, you need to provide the Certificate Password here to access the private key
 
.PARAMETER APIVersion
Optional, default is V1.0
Specify the API version to which send the request.
V1.0 or Beta are the current accepted Value
 
.PARAMETER RedirectUri
Mandatory for UserPrincipalName Authentication, Optional for Azure AD Application Authentication
Redirect URI of the Azure AD Application that is registered.
 
.PARAMETER UserPrincipalName
UserPrincipalName of the Admin Account
 
.EXAMPLE
Return the Secure Scores for the Tenant
Get-GraphSecurityData -UserPrincipalName admin@contoso.com -ClientID $ClientIDUPN -redirectUri http://localhost
 
.NOTES
#
#>


function Get-GraphSecurityData {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingUsernameAndPasswordParams", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")]
    [CmdletBinding(DefaultParameterSetName='UPN')]
    param (
        [Parameter(ParameterSetName='ClientSecret', Mandatory=$True)]
        [Parameter(ParameterSetName='ClientCert', Mandatory=$True)]
        [Parameter(ParameterSetName='UPN', Mandatory=$False)]
        [String]
        $TenantName,
        [Parameter(ParameterSetName='ClientSecret', Mandatory=$false)]
        [Parameter(ParameterSetName='ClientCert', Mandatory=$false)]
        [Parameter(ParameterSetName='UPN', Mandatory=$false)]
        [String]
        $Query = "secureScores",
        [Parameter(ParameterSetName='ClientCert', Mandatory=$True)]
        [Parameter(ParameterSetName='ClientSecret', Mandatory=$True)]
        [Parameter(ParameterSetName='UPN', Mandatory=$True)]
        [String]
        $ClientID,
        [Parameter(ParameterSetName='ClientSecret', Mandatory=$True)]
        [String]
        $ClientSecret,
        [Parameter(ParameterSetName='ClientCert', Mandatory=$True)]
        [String]
        $CertificatePath,
        [Parameter(ParameterSetName='ClientCert', Mandatory=$True)]
        [String]
        $CertificatePassword,
        [Parameter(ParameterSetName='ClientSecret', Mandatory=$False)]
        [Parameter(ParameterSetName='ClientCert', Mandatory=$False)]
        [Parameter(ParameterSetName='UPN', Mandatory=$False)]
        [String]
        [ValidateSet(
            'V1.0',
            'beta'
        )]
        $APIVersion = "v1.0",
        [Parameter(ParameterSetName='ClientCert', Mandatory=$false)]
        [Parameter(ParameterSetName='ClientSecret', Mandatory=$false)]
        [Parameter(ParameterSetName='UPN', Mandatory=$true)]
          [string]$RedirectUri,
        [Parameter(ParameterSetName='UPN', Mandatory=$False)]
          [string]$UserPrincipalName
    )
    try {
    # Call Microsoft Graph
        switch ( $PsCmdlet.ParameterSetName )
        {
            "UPN"
            {
                if([string]::IsNullOrEmpty($UserPrincipalName))
                {
                    $UserPrincipalName = Get-CurrentUPN
                }
                $SecurityData = (Invoke-GraphApi -Resource security -QueryParams $Query -ClientID $ClientID -UserPrincipalName $UserPrincipalName -redirectUri $redirectUri)
            }
            "ClientSecret"
            {
                $SecurityData = (Invoke-GraphApi -TenantName $TenantName -Resource security -QueryParams $Query -ClientID $ClientID -ClientSecret $ClientSecret)
            }
            "ClientCert"
            {
                $SecurityData = (Invoke-GraphApi -TenantName $TenantName -Resource security -QueryParams $Query -ClientID $ClientID -CertificatePath $CertificatePath -CertificatePassword $CertificatePassword)
            }
        }
    }
    catch {
        $null
    }
    Return $SecurityData
}


<#
.SYNOPSIS
Send Query to the Microsofrt Reports API
 
.DESCRIPTION
Send Query to the Microsofrt Reports API with Azure AD Application
 
.PARAMETER ClientID
This is the Client ID (Application ID) of the registered Azure AD Application.
The Application need to have the right permission in your tenant.
#TODO = Document the minimal app permission
 
.PARAMETER ClientSecret
If you are leveraging an Azure AD Application with Client Secret authentication, you need to provide the Secret here
 
.PARAMETER TenantName
For Azure AD Application Authentication, you need to specify the Tenant Name, Tenant ID or Registered Domain name on your Azure or Office 365 Tenant
 
.PARAMETER Query
Optional, Additionnal Graph URL to pass to https://graph.microsoft.com/V1.0/reports like "getEmailActivityUser", if no value getEmailActivityUserDetail are used
Currently, there no validation, so refer to the docs.microsoft.com API
#TODO - add url for docs
 
.EXAMPLE
Get the Details Email Activities for the last 180 days
Get-GraphUsageReportData -ClientID $ClientID -ClientSecret $ClientSecret -TenantName contoso.com -Query "getEmailActivityUserDetail(period='D180')"
 
.NOTES
Based on https://www.altitude365.com/2018/09/23/retrieve-and-analyze-office-365-usage-data-with-powershell-and-microsoft-graph-api/
#TODO - Add Client Cert Auth
#TODO - Add API Version
#>


function Get-GraphUsageReportData {
    [CmdletBinding()]
    param (
    [parameter(Mandatory = $true)]
    [string]$ClientID,

   [parameter(Mandatory = $true)]
    [string]$ClientSecret,

   [parameter(Mandatory = $true)]
    [string]$TenantName,

    [parameter(Mandatory=$false)]
    $Query = "getEmailActivityUserDetail(period='D180')"
    )
   try {
    # Call Microsoft Graph and extract CSV content and convert data to PowerShell objects.
        $UsageData = (Invoke-GraphApi -TenantName $TenantName -Resource reports -QueryParams $Query -ClientID $ClientID -ClientSecret $ClientSecret)| ConvertFrom-Csv
    }
    catch {
        Throw "Couldn't complete"
    }
    Return $UsageData
}


<#
.SYNOPSIS
Authenticate to Azure AD with Azure Directory Authentication Librairy for an Azure Ad Application leveraging Certificate Authentication
 
.DESCRIPTION
Authenticate to Azure AD with Azure Directory Authentication Librairy for an Azure Ad Application leveraging Certificate Authentication
 
.PARAMETER ClientID
This is the Client ID (Application ID) of the registered Azure AD Application.
The Application need to have the right permission in your tenant.
 
.PARAMETER CertificatePath
If you are leveraging an Azure AD Application with Certificate authentication, you need to provide the Certificate Path here
 
.PARAMETER CertificatePassword
If you are leveraging an Azure AD Application with Certificate authentication, you need to provide the Certificate Password here to access the private key
 
.PARAMETER TenantName
You need to specify the Tenant Name, Tenant ID or Registered Domain name on your Azure or Office 365 Tenant
 
.PARAMETER ResourceURI
Resource URI of the Azure AD Application that is registered.
 
.EXAMPLE
TODO - Example
TODO - Line 2
 
.NOTES
#TODO : Check for to add thumbprint option for installed certificate
#>


Function Get-OAuthHeaderAppCert
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")]
    [OutputType([Hashtable])]
    [CmdletBinding()]
    param (
    [cmdletbinding()]
    [parameter(Mandatory=$true)]
        $ClientID,
    [parameter(Mandatory=$true)]
        $CertificatePath,
    [parameter(Mandatory=$true)]
        $CertificatePassword,
    [parameter(Mandatory=$true)]
        $TenantName,
    [Parameter(Mandatory = $True)]
          [string]$ResourceURI
    )
    $TenantName = Test-TenantName -TenantName $TenantName
    $AzureADDLL = Get-AzureADDLL

    #Load Certificate
    $flag = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet
    $AppCert  = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($CertificatePath, $CertificatePassword, $flag )

    #Login Endpoint info
    $authority = ($(Get-TenantLoginEndPoint -TenantName $TenantName)).authorization_endpoint

    #Can't sideload the DLL for this one since the AppCert isn't pass correclty.
    $NULL = [System.Reflection.Assembly]::LoadFrom($AzureADDLL)
    # Create Authentication Context tied to Azure AD Tenant
    $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority
    $cac = New-Object  Microsoft.IdentityModel.Clients.ActiveDirectory.ClientAssertionCertificate($clientID, $AppCert)
    Try{
        $authResult = $authContext.AcquireTokenSilentAsync($resourceAppIdURI, $clientId)
        $AuthHeader=$authResult.result.CreateAuthorizationHeader()
    }
    Catch{
        $authResult = $authContext.AcquireTokenASync($resourceURI, $cac)
        if ($authResult.IsFaulted -eq $True)
        {
            Throw "No Access Token"
        }
        else
        {
            $AuthHeader=$authResult.result.CreateAuthorizationHeader()
        }
    }

    # Perform REST call.
        $headers = @{
            "Authorization" = $AuthHeader
            "Content-Type"  = "application/json"
            "ExpiresOn"     = $authResult.Result.ExpiresOn
            "AppID"     = $ClientID
        }
    Return $headers

}


<#
.SYNOPSIS
Authenticate to Azure AD with Azure Directory Authentication Librairy for an Azure Ad Application leveraging Client Secret Authentication
 
.DESCRIPTION
Authenticate to Azure AD with Azure Directory Authentication Librairy for an Azure Ad Application leveraging Client Secret Authentication
 
.PARAMETER ClientID
This is the Client ID (Application ID) of the registered Azure AD Application.
The Application need to have the right permission in your tenant.
 
.PARAMETER ClientSecret
If you are leveraging an Azure AD Application with Client Secret authentication, you need to provide the Secret here
 
.PARAMETER TenantName
You need to specify the Tenant Name, Tenant ID or Registered Domain name on your Azure or Office 365 Tenant
 
.PARAMETER ResourceURI
Resource URI of the Azure AD Application that is registered.
 
.EXAMPLE
TODO - Example
TODO - Line 2
 
.NOTES
Based on https://www.altitude365.com/2018/09/23/retrieve-and-analyze-office-365-usage-data-with-powershell-and-microsoft-graph-api/
 
#>


function Get-OAuthHeaderAppClientSecretNoDLL
{
    [OutputType([Hashtable])]
    [cmdletbinding()]
    param(
    [Parameter(Mandatory = $True)]
          [string]$TenantName ,
    [Parameter(Mandatory = $True)]
        [string]$ClientID,
    [Parameter(Mandatory = $True)]
          [string]$ClientSecret,
    [Parameter(Mandatory = $True)]
          [string]$ResourceURI
    
    )
    
    $TenantName = Test-TenantName -TenantName $TenantName

    #Login Endpoint info
    $loginURL = ($(Get-TenantLoginEndPoint -TenantName $TenantName)).token_endpoint

    # Get an Oauth 2 access token based on client id, secret and tenant domain
    $body = @{grant_type="client_credentials";resource=$resourceURI;client_id=$ClientID;client_secret=$ClientSecret}
    $oauth = Invoke-RestMethod -Method Post -Uri "$($loginURL)?api-version=1.0" -Body $body

    #Let's put the oauth token in the header, where it belongs
    $ExpireOn = "$(ConvertFrom-Ctime -ctime $oauth.expires_on)"
    $headers = @{
        "Authorization" = "$($oauth.token_type) $($oauth.access_token)"
        "ExpiresOn"     = $ExpireOn
        "AppID"     = $ClientID
    }
    Return $headers
}


<#
.SYNOPSIS
Authenticate to Azure AD with Azure Directory Authentication Librairy with your UserPrincipalName
 
.DESCRIPTION
Authenticate to Azure AD with Azure Directory Authentication Librairy with your UserPrincipalName and and Azure Ad Application
 
.PARAMETER ClientID
This is the Client ID (Application ID) of the registered Azure AD Application.
The Application need to have the right permission in your tenant.
 
.PARAMETER RedirectUri
Redirect URI of the Azure AD Application that is registered.
 
.PARAMETER ResourceAppIdURI
Resource URI of the Azure AD Application that is registered.
 
.PARAMETER UserPrincipalName
UserPrincipalName of the Admin Account
 
.EXAMPLE
TODO - Example
TODO - Line 2
 
.NOTES
#
#>


function Get-OAuthHeaderUPN
{
    [OutputType([Hashtable])]
    [cmdletbinding()]
    param(
    [Parameter(Mandatory = $True)]
          [string]$ClientID,
    [Parameter(Mandatory = $True)]
          [string]$RedirectUri,
    [Parameter(Mandatory = $True)]
          [string]$ResourceAppIdURI,
    [Parameter(Mandatory = $False)]
          [string]$UserPrincipalName
    )
    $AzureADDLL = Get-AzureADDLL
    if([string]::IsNullOrEmpty($UserPrincipalName))
    {
        $UserPrincipalName = Get-CurrentUPN
    }
    $TenantName = $UserPrincipalName.split("@")[1]
    $TenantInfo = Get-TenantLoginEndPoint -TenantName $TenantName
    $NULL = [System.Reflection.Assembly]::LoadFrom($AzureADDLL)

    [string] $authority = $TenantInfo.authorization_endpoint
    $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority
    $PromptBehavior = [Microsoft.IdentityModel.Clients.ActiveDirectory.PromptBehavior]::Auto
    $platformParam = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList $PromptBehavior
    $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList $UserPrincipalName, "OptionalDisplayableId"
    Try{
        $authResult = $authContext.AcquireTokenSilentAsync($resourceAppIdURI, $clientId)
        $AuthHeader=$authResult.result.CreateAuthorizationHeader()
    }
    Catch{
    $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI, $clientId, $redirectUri, $platformParam, $userId)
    $AuthHeader=$authResult.result.CreateAuthorizationHeader()
    }

    $headers = @{
        "Authorization" = $AuthHeader
        "Content-Type"  = "application/json"
        "ExpiresOn"     = $authResult.Result.ExpiresOn
        "AppID"     = $ClientID
        "UserID"     = $UserPrincipalName
        }
    Return $headers
}


<#
.SYNOPSIS
Read the Token Cache from Azure Directory Authentication Librairy that are exposed to Powershell
 
.DESCRIPTION
Read the Token Cache from Azure Directory Authentication Librairy that are exposed to Powershell
 
.EXAMPLE
Retrive Token Cache
Get-TokenCache
 
.NOTES
Only the exposed Token will be showen, since ADAL V3 isn't exposing the Refresh Token it won't be shown
#>


Function Get-TokenCache
{
    [CmdletBinding()]
    param (

    )
    $AzureADDLL = Get-AzureADDLL
    $NULL = [System.Reflection.Assembly]::LoadFrom($AzureADDLL)
    $cache = [Microsoft.IdentityModel.Clients.ActiveDirectory.TokenCache]::DefaultShared
    if($cache.count -gt 0){
        Return $cache.ReadItems()
    }
}


<#
.SYNOPSIS
Send request to Microsoft Graph API and get Access Token
 
.DESCRIPTION
Send request to Microsoft Graph API and get Access Token
Can be leveraged against multiple Graph API by specifying the right Resource
 
.PARAMETER TenantName
For Azure AD Application Authentication, you need to specify the Tenant Name, Tenant ID or Registered Domain name on your Azure or Office 365 Tenant
 
.PARAMETER Resource
The API ressource to which the request will be sent
 
.PARAMETER QueryParams
Additionnal Graph URL to pass to Resource like "alerts"
Currently, there no validation, so refer to the API docs
 
.PARAMETER Body
Optional, Body content to send with the request
 
.PARAMETER Method
Optional, Default is GET
Valide Value Delete, Get, Head, Merge, Options, Patch, Post, Put, Trace
 
.PARAMETER ClientID
This is the Client ID (Application ID) of the registered Azure AD Application.
The Application need to have the right permission in your tenant.
#TODO = Document the minimal app permission
 
.PARAMETER ClientSecret
If you are leveraging an Azure AD Application with Client Secret authentication, you need to provide the Secret here
 
.PARAMETER CertificatePath
If you are leveraging an Azure AD Application with Certificate authentication, you need to provide the Certificate Path here
 
.PARAMETER CertificatePassword
If you are leveraging an Azure AD Application with Certificate authentication, you need to provide the Certificate Password here to access the private key
 
.PARAMETER APIVersion
Optional, default is V1.0
Specify the API version to which send the request.
V1.0 or Beta are the current accepted Value
 
.PARAMETER RedirectUri
Mandatory for UserPrincipalName Authentication, Optional for Azure AD Application Authentication
Redirect URI of the Azure AD Application that is registered.
 
.PARAMETER UserPrincipalName
UserPrincipalName of the Admin Account
 
.EXAMPLE
Get the Details Email Activities for the last 180 days
Invoke-GraphApi -TenantName contoso.com -Resource reports -QueryParams "getEmailActivityUserDetail(period='D180')" -ClientID $ClientID -ClientSecret $ClientSecret
 
.NOTES
#
#>


Function Invoke-GraphApi
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingUsernameAndPasswordParams", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")]
    [CmdletBinding(DefaultParameterSetName='UPN')]
    Param(
        [Parameter(ParameterSetName='ClientSecret', Mandatory=$True)]
        [Parameter(ParameterSetName='ClientCert', Mandatory=$True)]
        [String]
        $TenantName,
        [Parameter(ParameterSetName='ClientSecret', Mandatory=$True)]
        [Parameter(ParameterSetName='ClientCert', Mandatory=$True)]
        [Parameter(ParameterSetName='UPN', Mandatory=$True)]
        [String]
        $Resource,
        [Parameter(ParameterSetName='ClientSecret', Mandatory=$False)]
        [Parameter(ParameterSetName='ClientCert', Mandatory=$False)]
        [Parameter(ParameterSetName='UPN', Mandatory=$False)]
        [String]
        $QueryParams,
        [Parameter(ParameterSetName='ClientSecret', Mandatory=$False)]
        [Parameter(ParameterSetName='ClientCert', Mandatory=$False)]
        [Parameter(ParameterSetName='UPN', Mandatory=$False)]
        [String]
        $Body,
        [Parameter(ParameterSetName='ClientSecret', Mandatory=$False)]
        [Parameter(ParameterSetName='ClientCert', Mandatory=$False)]
        [Parameter(ParameterSetName='UPN', Mandatory=$False)]
        [String]
        [ValidateSet(
            'Default',
            'Delete',
            'Get',
            'Head',
            'Merge',
            'Options',
            'Patch',
            'Post',
            'Put',
            'Trace'
        )]
        $Method = "Get",
        [Parameter(ParameterSetName='ClientCert', Mandatory=$True)]
        [Parameter(ParameterSetName='ClientSecret', Mandatory=$True)]
        [Parameter(ParameterSetName='UPN', Mandatory=$True)]
        [String]
        $ClientID,
        [Parameter(ParameterSetName='ClientSecret', Mandatory=$True)]
        [String]
        $ClientSecret,
        [Parameter(ParameterSetName='ClientCert', Mandatory=$True)]
        [String]
        $CertificatePath,
        [Parameter(ParameterSetName='ClientCert', Mandatory=$True)]
        [String]
        $CertificatePassword,
        [Parameter(ParameterSetName='ClientSecret', Mandatory=$False)]
        [Parameter(ParameterSetName='ClientCert', Mandatory=$False)]
        [Parameter(ParameterSetName='UPN', Mandatory=$False)]
        [String]
        [ValidateSet(
            'V1.0',
            'beta'
        )]
        $APIVersion = "v1.0",
        [Parameter(ParameterSetName='ClientCert', Mandatory=$false)]
        [Parameter(ParameterSetName='ClientSecret', Mandatory=$false)]
        [Parameter(ParameterSetName='UPN', Mandatory=$True)]
          [string]$RedirectUri,
        [Parameter(ParameterSetName='UPN', Mandatory=$False)]
        [string]
        $UserPrincipalName
    )
    $resourceURI = "https://graph.microsoft.com"
    switch ( $PsCmdlet.ParameterSetName )
    {
        "UPN"
        {
            if($Script:UPNGraphHeader){
                # Setting DateTime to Universal time to work in all timezones
                $DateTime = (Get-Date).ToUniversalTime()

                # If the authToken exists checking when it expires
                $TokenExpires = ($Script:UPNGraphHeader.ExpiresOn.datetime - $DateTime).Minutes
                $UPNMismatch = $UserPrincipalName -ne $Script:UPNGraphHeader.UserID
                $AppIDMismatch = $ClientID -ne $Script:UPNGraphHeader.AppID
                if($TokenExpires -le 0 -or $UPNMismatch -or $AppIDMismatch){
                    Write-PSFMessage -Level Host -Message "Authentication need to be refresh"
                    $Script:UPNGraphHeader = Get-OAuthHeaderUPN -clientId $ClientID -redirectUri $redirectUri -resourceAppIdURI $resourceURI -UserPrincipalName $UserPrincipalName
                }
            }
            # Authentication doesn't exist, calling Get-GraphAuthHeaderBasedOnUPN function
            else {
                $Script:UPNGraphHeader = Get-OAuthHeaderUPN -clientId $ClientID -redirectUri $redirectUri -resourceAppIdURI $resourceURI -UserPrincipalName $UserPrincipalName
            }
            $GraphHeader = $Script:UPNGraphHeader
        }
        "ClientSecret"
        {
            if($Script:CSGraphHeader){
                # Setting DateTime to Universal time to work in all timezones
                $DateTime = (Get-Date).ToUniversalTime()

                # If the authToken exists checking when it expires
                $TokenExpires = ((Get-date ($Script:CSGraphHeader.ExpiresOn)) - $DateTime).Minutes
                $AppIDMismatch = $ClientID -ne $Script:CSGraphHeader.AppID
                if($TokenExpires -le 0 -or $AppIDMismatch){
                    Write-PSFMessage -Level Host -Message "Authentication need to be refresh"
                $Script:CSGraphHeader = Get-OAuthHeaderAppClientSecretNoDLL -TenantName $TenantName -clientId $ClientID -ClientSecret $ClientSecret -resourceURI $ResourceURI
                }
            }
            # Authentication doesn't exist, calling Get-GraphAuthHeaderBasedOnUPN function
            else {
                $Script:CSGraphHeader = Get-OAuthHeaderAppClientSecretNoDLL -TenantName $TenantName -clientId $ClientID -ClientSecret $ClientSecret -resourceURI $ResourceURI
            }
            $GraphHeader = $Script:CSGraphHeader
        }
        "ClientCert"
        {
            if($Script:CCGraphHeader){
                # Setting DateTime to Universal time to work in all timezones
                $DateTime = (Get-Date).ToUniversalTime()

                # If the authToken exists checking when it expires
                $TokenExpires = ($Script:CCGraphHeader.ExpiresOn.datetime - $DateTime).Minutes
                $AppIDMismatch = $ClientID -ne $Script:CCGraphHeader.AppID
                if($TokenExpires -le 0 -or $AppIDMismatch){
                    Write-PSFMessage -Level Host -Message "Authentication need to be refresh"
                    $Script:CCGraphHeader = Get-OAuthHeaderAppCert -ClientID $ClientID -CertificatePath $CertificatePath -CertificatePassword $CertificatePassword -TenantName $TenantName -resourceURI $ResourceURI
                }
            }
            # Authentication doesn't exist, calling Get-GraphAuthHeaderBasedOnUPN function
            else {
                $Script:CCGraphHeader = Get-OAuthHeaderAppCert -ClientID $ClientID -CertificatePath $CertificatePath -CertificatePassword $CertificatePassword -TenantName $TenantName -resourceURI $ResourceURI
            }
            $GraphHeader = $Script:CCGraphHeader
        }
    }

    #Allow larger data set with multiple read.
    #From :https://smsagent.blog/2018/10/22/querying-for-devices-in-azure-ad-and-intune-with-powershell-and-microsoft-graph/
    try {
        if (([string]::IsNullOrEmpty($QueryParams))) {
            $GraphURL = "https://graph.microsoft.com/$($APIVersion)/$($Resource)"
        }
        else{
            $GraphURL = "https://graph.microsoft.com/$($APIVersion)/$($Resource)/$($QueryParams)"
        }
        if([string]::IsNullOrEmpty($Body)){
            $GraphResponse = Invoke-RestMethod -Uri $GraphURL -Headers $GraphHeader -Method $Method
        }
        else {
            $GraphResponse = Invoke-RestMethod -Uri $GraphURL -Headers $GraphHeader -Method $Method -Body $Body
        }

    }
    catch {
        $ex = $_.Exception
        $errorResponse = $ex.Response.GetResponseStream()
        $reader = New-Object System.IO.StreamReader($errorResponse)
        $reader.BaseStream.Position = 0
        $reader.DiscardBufferedData()
        $responseBody = $reader.ReadToEnd();
        Write-PSFMessage -Level Host -Message "Response content:`n$responseBody" -f Red
        Throw "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
        Write-PSFMessage -Level Host -Message
        break

        }

    # Return the data
    if($NULL -eq $GraphResponse.Value){
        $Items = $GraphResponse
    }else{
        $Items = $GraphResponse.Value
    }
    $NextLink = $GraphResponse.'@odata.nextLink'
    # Need to loop the requests because only 100 results are returned each time
    While ($NULL -ne $NextLink)
    {
        $GraphResponse = Invoke-RestMethod -Uri $NextLink -Headers $GraphtHeader -Method Get
        $NextLink = $GraphResponse.'@odata.nextLink'
        $Items += $GraphResponse.Value
    }
    Return $Items
}


<#
.SYNOPSIS
Send Query to the Office 365 Service Communication API
 
.DESCRIPTION
Send Query to the Office 365 Service Communication API with Azure AD Application
 
.PARAMETER TenantName
You need to specify the Tenant Name, Tenant ID or Registered Domain name on your Azure or Office 365 Tenant
 
.PARAMETER Operation
Type of operation to send to the API
For more info see https://docs.microsoft.com/en-us/office/office-365-management-api/office-365-management-activity-api-reference
 
.PARAMETER ClientID
This is the Client ID (Application ID) of the registered Azure AD Application.
The Application need to have the right permission in your tenant.
#TODO = Document the minimal app permission
 
.PARAMETER ClientSecret
If you are leveraging an Azure AD Application with Client Secret authentication, you need to provide the Secret here
 
.PARAMETER CertificatePath
If you are leveraging an Azure AD Application with Certificate authentication, you need to provide the Certificate Path here
 
.PARAMETER CertificatePassword
If you are leveraging an Azure AD Application with Certificate authentication, you need to provide the Certificate Password here to access the private key
 
.PARAMETER APIVersion
Optional, default is V1.0
Specify the API version to which send the request.
V1.0 or Beta are the current accepted Value
 
.EXAMPLE
Get current Status of all O365 Services
Invoke-O365ServiceCommunications -TenantName contoso.com -Operation CurrentStatus -ClientID $ClientID -ClientSecret $ClientSecret
 
.NOTES
Only Support App connection (As of : 2019-05)
https://docs.microsoft.com/en-us/office/office-365-management-api/office-365-service-communications-api-reference
https://docs.microsoft.com/en-us/office/office-365-management-api/office-365-management-activity-api-reference
https://docs.microsoft.com/en-us/office/office-365-management-api/get-started-with-office-365-management-apis
 
#>


Function Invoke-O365ServiceCommunication
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")]
    [CmdletBinding(DefaultParameterSetName='ClientSecret')]
    Param(
        [Parameter(ParameterSetName='ClientSecret', Mandatory=$True)]
        [Parameter(ParameterSetName='ClientCert', Mandatory=$True)]
        [String]
        $TenantName,
        [Parameter(ParameterSetName='ClientSecret', Mandatory=$True)]
        [Parameter(ParameterSetName='ClientCert', Mandatory=$True)]
        [String]
        $Operation,
        [Parameter(ParameterSetName='ClientCert', Mandatory=$True)]
        [Parameter(ParameterSetName='ClientSecret', Mandatory=$True)]
        [String]
        $ClientID,
        [Parameter(ParameterSetName='ClientSecret', Mandatory=$True)]
        [String]
        $ClientSecret,
        [Parameter(ParameterSetName='ClientCert', Mandatory=$True)]
        [String]
        $CertificatePath,
        [Parameter(ParameterSetName='ClientCert', Mandatory=$True)]
        [String]
        $CertificatePassword,
        [Parameter(ParameterSetName='ClientSecret', Mandatory=$False)]
        [Parameter(ParameterSetName='ClientCert', Mandatory=$False)]
        [String]
        [ValidateSet(
            'V1.0',
            'beta'
        )]
        $APIVersion = "v1.0"

    )

    $ResourceURI = "https://manage.office.com"
    switch ( $PsCmdlet.ParameterSetName )
    {
        "ClientSecret"
        {
            $ManagementHeader = Get-OAuthHeaderAppClientSecretNoDLL -TenantName $TenantName -clientId $ClientID -ClientSecret $ClientSecret -resourceURI $ResourceURI
        }
        "ClientCert"
        {
            $ManagementHeader = Get-OAuthHeaderAppCert -ClientID $ClientID -CertificatePath $CertificatePath -CertificatePassword $CertificatePassword -TenantName $TenantName -resourceURI $ResourceURI
        }
    }
    $TenantGUID = (Get-TenantLoginEndPoint $TenantName).token_endpoint.split("/")[-3]
    $uri = "https://manage.office.com/api/$($APIVersion)/$TenantGUID/ServiceComms/$($operation)"
    $Query = (Invoke-RestMethod -Uri $uri -Headers $ManagementHeader -Method Get -Verbose).value
    Return $Query
}


<#
This is an example configuration file
 
By default, it is enough to have a single one of them,
however if you have enough configuration settings to justify having multiple copies of it,
feel totally free to split them into multiple files.
#>


<#
# Example Configuration
Set-PSFConfig -Module 'MSApiConnect' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'"
#>


Set-PSFConfig -Module 'MSApiConnect' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging."
Set-PSFConfig -Module 'MSApiConnect' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments."

<#
Stored scriptblocks are available in [PsfValidateScript()] attributes.
This makes it easier to centrally provide the same scriptblock multiple times,
without having to maintain it in separate locations.
 
It also prevents lengthy validation scriptblocks from making your parameter block
hard to read.
 
Set-PSFScriptblock -Name 'MSApiConnect.ScriptBlockName' -Scriptblock {
     
}
#>


<#
# Example:
Register-PSFTeppScriptblock -Name "MSApiConnect.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' }
#>


<#
# Example:
Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name MSApiConnect.alcohol
#>


New-PSFLicense -Product 'MSApiConnect' -Manufacturer 'Simon Poirier' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2019-08-02") -Text @"
Copyright (c) 2019 Simon Poirier
 
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"@

#endregion Load compiled code