PSDev.psm1

#Region '.\prefix.ps1' -1

# The content of this file will be prepended to the top of the psm1 module file. This is useful for custom module setup is needed on import.
#EndRegion '.\prefix.ps1' 2
#Region '.\Private\Assert-FolderExist.ps1' -1

function Assert-FolderExist
{
    <#
    .SYNOPSIS
        Verify and create folder
    .DESCRIPTION
        Verifies that a folder path exists, if not it will create it
    .PARAMETER Path
        Defines the path to be validated
    .EXAMPLE
        'C:\Temp' | Assert-FolderExist
 
        This will verify that the path exists and if it does not the folder will be created
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [string]
        $Path
    )

    process
    {
        $exists = Test-Path -Path $Path -PathType Container
        if (!$exists)
        {
            $null = New-Item -Path $Path -ItemType Directory
        }
    }
}
#EndRegion '.\Private\Assert-FolderExist.ps1' 31
#Region '.\Private\Invoke-GarbageCollect.ps1' -1

function Invoke-GarbageCollect
{
    <#
    .SYNOPSIS
        Calls system.gc collect method. Purpose is mainly for readability.
    .DESCRIPTION
        Calls system.gc collect method. Purpose is mainly for readability.
    .EXAMPLE
        Invoke-GarbageCollect
    #>

    [system.gc]::Collect()
}
#EndRegion '.\Private\Invoke-GarbageCollect.ps1' 13
#Region '.\Private\pslog.ps1' -1

function pslog
{
    <#
    .SYNOPSIS
        This is simple logging function that automatically log to file. Logging to console is maintained.
    .DESCRIPTION
        This is simple logging function that automatically log to file. Logging to console is maintained.
    .PARAMETER Severity
        Defines the type of log, valid vales are, Success,Info,Warning,Error,Verbose,Debug
    .PARAMETER Message
        Defines the message for the log entry
    .PARAMETER Source
        Defines a source, this is useful to separate log entries in categories for different stages of a process or for each function, defaults to default
    .PARAMETER Throw
        Specifies that when using severity error pslog will throw. This is useful in catch statements so that the terminating error is propagated upwards in the stack.
    .PARAMETER LogDirectoryOverride
        Defines a hardcoded log directory to write the log file to. This defaults to %appdatalocal%\<modulename\logs.
    .PARAMETER DoNotLogToConsole
        Specifies that logs should only be written to the log file and not to the console.
    .EXAMPLE
        pslog Verbose 'Successfully wrote to logfile'
        Explanation of the function or its result. You can include multiple examples with additional .EXAMPLE lines
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Sole purpose of function is logging, including console')]
    [cmdletbinding()]
    param(
        [parameter(Position = 0)]
        [ValidateSet('Success', 'Info', 'Warning', 'Error', 'Verbose', 'Debug')]
        [Alias('Type')]
        [string]
        $Severity,

        [parameter(Mandatory, Position = 1)]
        [string]
        $Message,

        [parameter(position = 2)]
        [string]
        $source = 'default',

        [parameter(Position = 3)]
        [switch]
        $Throw,

        [parameter(Position = 4)]
        [string]
        $LogDirectoryOverride,

        [parameter(Position = 5)]
        [switch]
        $DoNotLogToConsole
    )

    begin
    {
        if (-not $LogDirectoryOverride)
        {
            $localappdatapath = [Environment]::GetFolderPath('localapplicationdata') # ie C:\Users\<username>\AppData\Local
            $modulename = $MyInvocation.MyCommand.Module
            $logdir = "$localappdatapath\$modulename\logs"
        }
        else
        {
            $logdir = $LogDirectoryOverride
        }
        $logdir | Assert-FolderExist -Verbose:$VerbosePreference
        $timestamp = (Get-Date)
        $logfilename = ('{0}.log' -f $timestamp.ToString('yyy-MM-dd'))
        $timestampstring = $timestamp.ToString('yyyy-MM-ddThh:mm:ss.ffffzzz')
    }

    process
    {
        switch ($Severity)
        {
            'Success'
            {
                "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                if (-not $DoNotLogToConsole)
                {
                    Write-Host -Object "SUCCESS: $timestampstring`t$source`t$message" -ForegroundColor Green
                }
            }
            'Info'
            {
                "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                if (-not $DoNotLogToConsole)
                {
                    Write-Information -MessageData "$timestampstring`t$source`t$message"
                }
            }
            'Warning'
            {
                "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                if (-not $DoNotLogToConsole)
                {
                    Write-Warning -Message "$timestampstring`t$source`t$message"
                }
            }
            'Error'
            {
                "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                if (-not $DoNotLogToConsole)
                {
                    Write-Error -Message "$timestampstring`t$source`t$message"
                }
                if ($throw)
                {
                    throw
                }
            }
            'Verbose'
            {
                if ($VerbosePreference -ne 'SilentlyContinue')
                {
                    "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                }
                if (-not $DoNotLogToConsole)
                {
                    Write-Verbose -Message "$timestampstring`t$source`t$message"
                }
            }
            'Debug'
            {
                if ($DebugPreference -ne 'SilentlyContinue')
                {
                    "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                }
                if (-not $DoNotLogToConsole)
                {
                    Write-Debug -Message "$timestampstring`t$source`t$message"
                }
            }
        }
    }
}
#EndRegion '.\Private\pslog.ps1' 137
#Region '.\Private\Write-PSProgress.ps1' -1

function Write-PSProgress
{
    <#
    .SYNOPSIS
        Wrapper for PSProgress
    .DESCRIPTION
        This function will automatically calculate items/sec, eta, time remaining
        as well as set the update frequency in case the there are a lot of items processing fast.
    .PARAMETER Activity
        Defines the activity name for the progressbar
    .PARAMETER Id
        Defines a unique ID for this progressbar, this is used when nesting progressbars
    .PARAMETER Target
        Defines a arbitrary text for the currently processed item
    .PARAMETER ParentId
        Defines the ID of a parent progress bar
    .PARAMETER Completed
        Explicitly tells powershell to set the progress bar as completed removing
        it from view. In some cases the progress bar will linger if this is not done.
    .PARAMETER Counter
        The currently processed items counter
    .PARAMETER Total
        The total number of items to process
    .PARAMETER StartTime
        Sets the start datetime for the progressbar, this is required to calculate items/sec, eta and time remaining
    .PARAMETER DisableDynamicUpdateFrquency
        Disables the dynamic update frequency function and every item will update the status of the progressbar
    .PARAMETER NoTimeStats
        Disables calculation of items/sec, eta and time remaining
    .EXAMPLE
        1..10000 | foreach-object -begin {$StartTime = Get-Date} -process {
            Write-PSProgress -Activity 'Looping' -Target $PSItem -Counter $PSItem -Total 10000 -StartTime $StartTime
        }
        Explanation of the function or its result. You can include multiple examples with additional .EXAMPLE lines
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Standard')]
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Completed')]
        [string]
        $Activity,

        [Parameter(Position = 1, ParameterSetName = 'Standard')]
        [Parameter(Position = 1, ParameterSetName = 'Completed')]
        [ValidateRange(0, 2147483647)]
        [int]
        $Id,

        [Parameter(Position = 2, ParameterSetName = 'Standard')]
        [string]
        $Target,

        [Parameter(Position = 3, ParameterSetName = 'Standard')]
        [Parameter(Position = 3, ParameterSetName = 'Completed')]
        [ValidateRange(-1, 2147483647)]
        [int]
        $ParentId,

        [Parameter(Position = 4, ParameterSetname = 'Completed')]
        [switch]
        $Completed,

        [Parameter(Mandatory = $true, Position = 5, ParameterSetName = 'Standard')]
        [long]
        $Counter,

        [Parameter(Mandatory = $true, Position = 6, ParameterSetName = 'Standard')]
        [long]
        $Total,

        [Parameter(Position = 7, ParameterSetName = 'Standard')]
        [datetime]
        $StartTime,

        [Parameter(Position = 8, ParameterSetName = 'Standard')]
        [switch]
        $DisableDynamicUpdateFrquency,

        [Parameter(Position = 9, ParameterSetName = 'Standard')]
        [switch]
        $NoTimeStats
    )

    # Define current timestamp
    $TimeStamp = (Get-Date)

    # Define a dynamic variable name for the global starttime variable
    $StartTimeVariableName = ('ProgressStartTime_{0}' -f $Activity.Replace(' ', ''))

    # Manage global start time variable
    if ($PSBoundParameters.ContainsKey('Completed') -and (Get-Variable -Name $StartTimeVariableName -Scope Global -ErrorAction SilentlyContinue))
    {
        # Remove the global starttime variable if the Completed switch parameter is users
        try
        {
            Remove-Variable -Name $StartTimeVariableName -ErrorAction Stop -Scope Global
        }
        catch
        {
            throw $_
        }
    }
    elseif (-not (Get-Variable -Name $StartTimeVariableName -Scope Global -ErrorAction SilentlyContinue))
    {
        # Global variable do not exist, create global variable
        if ($null -eq $StartTime)
        {
            # No start time defined with parameter, use current timestamp as starttime
            Set-Variable -Name $StartTimeVariableName -Value $TimeStamp -Scope Global
            $StartTime = $TimeStamp
        }
        else
        {
            # Start time defined with parameter, use that value as starttime
            Set-Variable -Name $StartTimeVariableName -Value $StartTime -Scope Global
        }
    }
    else
    {
        # Global start time variable is defined, collect and use it
        $StartTime = Get-Variable -Name $StartTimeVariableName -Scope Global -ErrorAction Stop -ValueOnly
    }

    # Define frequency threshold
    $Frequency = [Math]::Ceiling($Total / 100)
    switch ($PSCmdlet.ParameterSetName)
    {
        'Standard'
        {
            # Only update progress is any of the following is true
            # - DynamicUpdateFrequency is disabled
            # - Counter matches a mod of defined frequecy
            # - Counter is 0
            # - Counter is equal to Total (completed)
            if (($DisableDynamicUpdateFrquency) -or ($Counter % $Frequency -eq 0) -or ($Counter -eq 1) -or ($Counter -eq $Total))
            {

                # Calculations for both timestats and without
                $Percent = [Math]::Round(($Counter / $Total * 100), 0)

                # Define count progress string status
                $CountProgress = ('{0}/{1}' -f $Counter, $Total)

                # If percent would turn out to be more than 100 due to incorrect total assignment revert back to 100% to avoid that write-progress throws
                if ($Percent -gt 100)
                {
                    $Percent = 100
                }

                # Define write-progress splat hash
                $WriteProgressSplat = @{
                    Activity         = $Activity
                    PercentComplete  = $Percent
                    CurrentOperation = $Target
                }

                # Add ID if specified
                if ($Id)
                {
                    $WriteProgressSplat.Id = $Id
                }

                # Add ParentID if specified
                if ($ParentId)
                {
                    $WriteProgressSplat.ParentId = $ParentId
                }

                # Calculations for either timestats and without
                if ($NoTimeStats)
                {
                    $WriteProgressSplat.Status = ('{0} - {1}%' -f $CountProgress, $Percent)
                }
                else
                {
                    # Total seconds elapsed since start
                    $TotalSeconds = ($TimeStamp - $StartTime).TotalSeconds

                    # Calculate items per sec processed (IpS)
                    $ItemsPerSecond = ([Math]::Round(($Counter / $TotalSeconds), 2))

                    # Calculate seconds spent per processed item (for ETA)
                    $SecondsPerItem = if ($Counter -eq 0)
                    {
                        0
                    }
                    else
                    {
 ($TotalSeconds / $Counter)
                    }

                    # Calculate seconds remainging
                    $SecondsRemaing = ($Total - $Counter) * $SecondsPerItem
                    $WriteProgressSplat.SecondsRemaining = $SecondsRemaing

                    # Calculate ETA
                    $ETA = $(($Timestamp).AddSeconds($SecondsRemaing).ToShortTimeString())

                    # Add findings to write-progress splat hash
                    $WriteProgressSplat.Status = ('{0} - {1}% - ETA: {2} - IpS {3}' -f $CountProgress, $Percent, $ETA, $ItemsPerSecond)
                }

                # Call writeprogress
                Write-Progress @WriteProgressSplat
            }
        }
        'Completed'
        {
            Write-Progress -Activity $Activity -Id $Id -Completed
        }
    }
}
#EndRegion '.\Private\Write-PSProgress.ps1' 214
#Region '.\Public\Add-NumberFormater.ps1' -1

function Add-NumberFormater {
    <#
    .DESCRIPTION
        Adding formater capabilities by overwriting the ToString method of the input double value
    .PARAMETER InputObject
        Defines the input value to process
    .PARAMETER Type
        Defines what type of value it is and what units to use. Available values is Standard and DataSize
    .EXAMPLE
        Add-NumberFormater -InputObject 2138476234 -Type DataSize
        Processes the number 2138476234 and returns the value with the replaced ToString() method. This case would return "1,99 GB"
    #>


    [CmdletBinding()] # Enabled to support verbose
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'Parameter use is not correctly identified by PSScriptAnalyzer')]
    param(
        [Parameter(Mandatory, ValueFromPipeline)][Alias('Double', 'Number')][double[]]$InputObject,
        [ValidateSet('DataSize', 'Standard')][string]$Type = 'Standard'
    )

    begin {
        $Configuration = @{
            DataSize = @{
                Base  = 1024
                Units = @('', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')
            }
            Standard = @{
                Base  = 1000
                Units = @('', 'K', 'MN', 'MD', 'BN', 'BD', 'TN', 'TD')
            }
        }
    }

    process {
        $InputObject | foreach-object {
            $CurrentNumber = $_
            $TempCopyOfCurrentNumber = $CurrentNumber

            if ($TempCopyOfCurrentNumber -lt $Configuration.($Type).Base) {
                $DisplayString = "'{0:N}'" -f [double]($TempCopyOfCurrentNumber)
            } else {
                $i = 0
                while ($TempCopyOfCurrentNumber -ge $Configuration.($Type).Base -and $i -lt $Configuration.($Type).Units.Length - 1 ) {
                    $TempCopyOfCurrentNumber /= $Configuration.($Type).Base
                    $i++
                }
                $DisplayString = "'{0:N2} {1}'" -f [double]($TempCopyOfCurrentNumber), ($Configuration.($Type).Units[$i])
            }

            $NewObject = $CurrentNumber | Add-Member -MemberType ScriptMethod -Name ToString -Value ([Scriptblock]::Create($DisplayString)) -Force -PassThru
            return $NewObject
        }
    }
    end { }
}
#EndRegion '.\Public\Add-NumberFormater.ps1' 56
#Region '.\Public\Compare-ObjectProperties.ps1' -1

function Compare-ObjectProperties
{
    <#
    .DESCRIPTION
        Compare two objects and compare all property values
    .PARAMETER Object1
        Define reference object to compare
    .PARAMETER Object2
        Defines compare object to compare
    .PARAMETER HeaderProperty
        Define the property to be used as identifyer for the object
    .EXAMPLE
        Compare-ObjectProperties -Object1 $temp1 -Object2 $temp2 -HeaderProperty 'name'
 
        Compare properties of the two objects
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Makes no sense when no filtering can be applied')]
    param (
        $Object1,
        $Object2,
        $HeaderProperty
    )

    foreach ($Property in $Object1.PSObject.Properties.Name)
    {
        $Diff = if ($Object1.$Property -ne $Object2.$Property)
        {
            '<--'
        }
        [pscustomobject]@{
            Name                     = $Property
            $Object1.$HeaderProperty = $Object1.$Property
            $Object2.$HeaderProperty = $Object2.$Property
            Diff                     = $Diff
        } | Select-Object -Property 'Name', @{Name = ($Object1.$HeaderProperty); exp = {
                $PSItem.($Object1.$HeaderProperty).ToString()
            }
        }, @{Name = ($Object2.$HeaderProperty); exp = { $PSItem.($Object2.$HeaderProperty).ToString() } }, Diff
    }
}
#EndRegion '.\Public\Compare-ObjectProperties.ps1' 41
#Region '.\Public\Convert-Metric.ps1' -1

function Convert-Metric
{
    <#
        .DESCRIPTION
            Function that converts speed metrics
        .PARAMETER Bps
            Defines bytes per second value
        .PARAMETER Bpm
            Defines bytes per minute value
        .PARAMETER Bph
            Defines bytes per hour value
        .PARAMETER Bit
            Defines bits per second value
        .PARAMETER Kbps
            Defines kilobytes per second value
        .PARAMETER Kbpm
            Defines kilobytes per minute value
        .PARAMETER Kbph
            Defines kilobytes per hour value
        .PARAMETER Kbit
            Defines kilobits per second value
        .PARAMETER Mbps
            Defines megabytes per second value
        .PARAMETER Mbpm
            Defines megabytes per minute value
        .PARAMETER Mbph
            Defines megabytes per hour value
        .PARAMETER Mbit
            Defines megabits per second value
        .PARAMETER Gbps
            Defines gigabytes per second value
        .PARAMETER Gbpm
            Defines gigabytes per minute value
        .PARAMETER Gbph
            Defines gigabytes per hour value
        .PARAMETER Gbit
            Defines gigabits per second value
        .PARAMETER Shortunits
            Specifies that the resulting object specifies the metrics with short units i.e. "Mbps" instead of "MB per sec"
        .PARAMETER RoundToNearestInteger
            Specifies that all values are rounded to the nearest integer
        .PARAMETER Round
            Defines that the values should be rounded
        .PARAMETER RoundMethod
            Defines what rounding method should be used, valid values are FindScale and MathRound
        .EXAMPLE
            Convert-Metric -Gbph 40 -ShortUnits -RoundToNearestInteger
 
            Name Value
            ---- -----
            Bps 11930465
            Bpm 715827883
            Bph 42949672960
            Bit 95443718
            KBps 11651
            KBpm 699051
            KBph 41943040
            KBit 93207
            MBps 11
            MBpm 683
            MBph 40960
            MBit 91
            GBps 0
            GBpm 1
            GBph 40
            GBit 0
 
            This commands converts "40 gigabyte per hour" to all the other metrics in the table and rounds the value.
    #>

    [CmdletBinding()]
    [OutputType([System.Collections.Specialized.OrderedDictionary])]
    param (

        [Parameter(Mandatory, ParameterSetName = 'Bps')][double]$Bps,
        [Parameter(Mandatory, ParameterSetName = 'Bpm')][double]$Bpm,
        [Parameter(Mandatory, ParameterSetName = 'Bph')][double]$Bph,
        [Parameter(Mandatory, ParameterSetName = 'Bit')][double]$Bit,

        [Parameter(Mandatory, ParameterSetName = 'Kbps')][double]$Kbps,
        [Parameter(Mandatory, ParameterSetName = 'Kbpm')][double]$Kbpm,
        [Parameter(Mandatory, ParameterSetName = 'Kbph')][double]$Kbph,
        [Parameter(Mandatory, ParameterSetName = 'Kbit')][double]$Kbit,

        [Parameter(Mandatory, ParameterSetName = 'Mbps')][double]$Mbps,
        [Parameter(Mandatory, ParameterSetName = 'Mbpm')][double]$Mbpm,
        [Parameter(Mandatory, ParameterSetName = 'Mbph')][double]$Mbph,
        [Parameter(Mandatory, ParameterSetName = 'Mbit')][double]$Mbit,

        [Parameter(Mandatory, ParameterSetName = 'Gbps')][double]$Gbps,
        [Parameter(Mandatory, ParameterSetName = 'Gbpm')][double]$Gbpm,
        [Parameter(Mandatory, ParameterSetName = 'Gbph')][double]$Gbph,
        [Parameter(Mandatory, ParameterSetName = 'Gbit')][double]$Gbit,
        [switch]$ShortUnits,
        [switch]$Round,
        [ValidateSet('FindScale', 'MathRound')][string]$RoundMethod = 'FindScale'
    )

    # Convert input value to one common unit
    switch ($PsCmdlet.ParameterSetName)
    {

        'Bps'
        {
            $Bps = $Bps
        }
        'Bpm'
        {
            $Bps = $Bpm / 60
        }
        'Bph'
        {
            $Bps = $Bph / 60 / 60
        }
        'Bit'
        {
            $Bps = $Bit / 8
        }

        'Kbps'
        {
            $Bps = $Kbps * 1KB
        }
        'Kbpm'
        {
            $Bps = $Kbpm * 1KB / 60
        }
        'Kbph'
        {
            $Bps = $Kbph * 1KB / 60 / 60
        }
        'Kbit'
        {
            $Bps = $Kbit * 1KB / 8
        }

        'Mbps'
        {
            $Bps = $Mbps * 1MB
        }
        'Mbpm'
        {
            $Bps = $Mbpm * 1MB / 60
        }
        'Mbph'
        {
            $Bps = $Mbph * 1MB / 60 / 60
        }
        'Mbit'
        {
            $Bps = $Mbit * 1MB / 8
        }

        'Gbps'
        {
            $Bps = $Gbps * 1GB
        }
        'Gbpm'
        {
            $Bps = $Gbpm * 1GB / 60
        }
        'Gbph'
        {
            $Bps = $Gbph * 1GB / 60 / 60
        }
        'Gbit'
        {
            $Bps = $Gbit * 1GB / 8
        }

    }

    # Convert the common unit to all other units
    $Bps = $Bps
    $Bpm = $Bps * 60
    $Bph = $Bps * 60 * 60
    $Bit = $Bps * 8

    $Kbps = $Bps / 1KB
    $Kbpm = $Bps / 1KB * 60
    $Kbph = $Bps / 1KB * 60 * 60
    $Kbit = $Bps / 1KB * 8

    $Mbps = $Bps / 1MB
    $Mbpm = $Bps / 1MB * 60
    $Mbph = $Bps / 1MB * 60 * 60
    $Mbit = $Bps / 1MB * 8

    $Gbps = $Bps / 1GB
    $Gbpm = $Bps / 1GB * 60
    $Gbph = $Bps / 1GB * 60 * 60
    $Gbit = $Bps / 1GB * 8

    if ($Round -and $RoundMethod -eq 'FindScale')
    {
        if (-not (Get-Command -Name 'Find-Scale' -ErrorAction SilentlyContinue))
        {
            $RoundMethod = 'MathRound'
            Write-Warning -Message 'FindScale method is not available, make sure dependency is available in scope'
            Write-Warning -Message 'Falling back to RoundMathod "MathRound"'
        }
    }

    if ($Round -and $RoundMethod -eq 'FindScale')
    {
        $Bpm = Find-Scale -Value $Bpm
        $Bps = Find-Scale -Value $Bps
        $Bph = Find-Scale -Value $Bph
        $Bit = Find-Scale -Value $Bit
        $Kbps = Find-Scale -Value $Kbps
        $Kbpm = Find-Scale -Value $Kbpm
        $Kbph = Find-Scale -Value $Kbph
        $Kbit = Find-Scale -Value $Kbit
        $Mbps = Find-Scale -Value $Mbps
        $Mbpm = Find-Scale -Value $Mbpm
        $Mbph = Find-Scale -Value $Mbph
        $Mbit = Find-Scale -Value $Mbit
        $Gbps = Find-Scale -Value $Gbps
        $Gbpm = Find-Scale -Value $Gbpm
        $Gbph = Find-Scale -Value $Gbph
        $Gbit = Find-Scale -Value $Gbit
    }
    elseif ($Round -and $RoundMethod -eq 'MathRound')
    {
        $Bpm = [Math]::Round($Bpm )
        $Bps = [Math]::Round($Bps )
        $Bph = [Math]::Round($Bph )
        $Bit = [Math]::Round($Bit )
        $Kbps = [Math]::Round($Kbps)
        $Kbpm = [Math]::Round($Kbpm)
        $Kbph = [Math]::Round($Kbph)
        $Kbit = [Math]::Round($Kbit)
        $Mbps = [Math]::Round($Mbps)
        $Mbpm = [Math]::Round($Mbpm)
        $Mbph = [Math]::Round($Mbph)
        $Mbit = [Math]::Round($Mbit)
        $Gbps = [Math]::Round($Gbps)
        $Gbpm = [Math]::Round($Gbpm)
        $Gbph = [Math]::Round($Gbph)
        $Gbit = [Math]::Round($Gbit)
    }
    else
    {

    }

    # Return object with results
    if ($ShortUnits)
    {
        $HashResult = [ordered]@{
            'Bps'  = $Bps
            'Bpm'  = $Bpm
            'Bph'  = $Bph
            'Bit'  = $Bit
            'KBps' = $Kbps
            'KBpm' = $Kbpm
            'KBph' = $Kbph
            'KBit' = $Kbit
            'MBps' = $Mbps
            'MBpm' = $Mbpm
            'MBph' = $Mbph
            'MBit' = $Mbit
            'GBps' = $Gbps
            'GBpm' = $Gbpm
            'GBph' = $Gbph
            'GBit' = $Gbit
        }
    }
    else
    {
        $HashResult = [ordered]@{
            'B per sec'  = $Bps
            'B per min'  = $Bpm
            'B per hou'  = $Bph
            'Bit'        = $Bit
            'KB per sec' = $Kbps
            'KB per min' = $Kbpm
            'KB per hou' = $Kbph
            'KiloBit'    = $Kbit
            'MB per sec' = $Mbps
            'MB per min' = $Mbpm
            'MB per hou' = $Mbph
            'MegaBit'    = $Mbit
            'GB per sec' = $Gbps
            'GB per min' = $Gbpm
            'GB per hou' = $Gbph
            'GigaBit'    = $Gbit
        }
    }
    $HashResult
}
#EndRegion '.\Public\Convert-Metric.ps1' 291
#Region '.\Public\Convert-Object.ps1' -1

function Convert-Object {
    <#
    .DESCRIPTION
        Function that converts a specific input value to a number of output formats. This
        is a function that allows shortcuts of already existing powershell/.net features.
        All conversions are made with an intermediate conversion to byte[].
    .PARAMETER FromString
        Defines the input as a standard string, ie "Hello World"
    .PARAMETER FromBase64String
        Defines the input as a base64 encoded string, ie "MjUzZmY1NWUtNzhjNy00MjczLWFmYmUtNjgzZThiZjZiMWE1"
    .PARAMETER FromGUID
        Defines the input as a GUID value represented in string format, ie "253ff55e-78c7-4273-afbe-683e8bf6b1a5"
    .PARAMETER FromHexString
        Defines the input as a Hex value represented in string format, ie "48656c6c6f20576f726c64"
    .PARAMETER FromIntArray
        Defines the input as a int array, ie 1,2,3
    .PARAMETER FromInt
        Defines the input as a int value, ie 12345
    .PARAMETER FromCharArray
        Defines the input as a char array, ie 'a','b','c'
    .PARAMETER FromByteArray
        Defines the input as a byte array, ie 72,101,108,108,111,32,87,111,114,108,100
    .PARAMETER FromScriptBlock
        Defines the input as a scriptblock, ie {Write-Host 'Test'}
    .PARAMETER FromSecureString
        Deinfes the input as a securestring, ie 01000000d08c9ddf0115d1118c7a00c04fc2....
    .PARAMETER FromSecureStringObject
        Defines the input as a securestringobject, ie (Get-Credential)
    .PARAMETER FromBinaryStringArray
        Defines the input as a binarystring array, ie '01001000','01100101','01101100','01101100','01101111'
    .PARAMETER FromIPAddressString
        Defines the input as a ip address, ie 72.101.108.108
    .PARAMETER FromByteCollection
        Defines the input as an byte collection
    .PARAMETER Properties
        Defines a string array with properties to return. Defaults to the reserved word 'All'
    .EXAMPLE
        Code
        Description
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")]
    [CmdletBinding(DefaultParameterSetName = 'FromString')]
    param()
    DynamicParam {
        $Conversions = @(
            'String',
            'Base64String',
            'ByteArray',
            'CharArray',
            'GUID',
            'HexStringArray',
            'HexString',
            'SecureString',
            'SecureStringObject',
            'BinaryStringArray',
            'BigInteger',
            'Int64',
            'IPAddressString',
            'ScriptBlock',
            'ByteCollection'
        )

        # Property parameter
        # Define base parameter attributes
        $ParameterName = 'Property'
        $ParameterDataType = [string]
        $ParameterAlias = @('Properties')
        $ParameterValidateSet = $Conversions

        # Create simple parameter attributes
        $ParameterAttribute = New-Object -TypeName System.Management.Automation.ParameterAttribute
        $ParameterAttribute.ParameterSetName = '__AllParameterSets'

        # Create validateset attribute
        $ValidateSetAttribute = New-Object -TypeName System.Management.Automation.ValidateSetAttribute -ArgumentList $ParameterValidateSet

        # Create alias attribute
        $AliasAttribute = New-Object -TypeName System.Management.Automation.AliasAttribute -ArgumentList $ParameterAlias

        $AttributeCollection = New-Object -TypeName System.Collections.ObjectModel.Collection[System.Attribute]
        $AttributeCollection.Add($ParameterAttribute)
        $AttributeCollection.Add($ValidateSetAttribute)
        $AttributeCollection.Add($AliasAttribute)

        # Define Dynamic parameter based on attribute collection
        $DynamicParameter = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter -ArgumentList ($ParameterName, $ParameterDataType, $AttributeCollection)

        # If paramDictionary already exists, add dynamic parameter to dictionary, otherwise create a new dictionary
        if ($paramDictionary -and $paramDictionary.Keys -notcontains $ParameterName) {
            $paramDictionary.Add($ParameterName, $DynamicParameter)
        } else {
            $paramDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary
            $paramDictionary.Add($ParameterName, $DynamicParameter)
        }

        # Fromparameters
        foreach ($source in $Conversions) {
            # Define base parameter attributes
            $ParameterName = ('From{0}' -f $Source)
            $ParameterDataType = [Object]

            # Create simple parameter attributes
            $ParameterAttribute = New-Object -TypeName System.Management.Automation.ParameterAttribute
            $ParameterAttribute.ParameterSetName = $ParameterName
            $ParameterAttribute.Mandatory = $true

            # Sätt samman attribut-komponenter till collection
            $AttributeCollection = New-Object -TypeName System.Collections.ObjectModel.Collection[System.Attribute]
            $AttributeCollection.Add($ParameterAttribute)

            # Define Dynamic parameter based on attribute collection
            $DynamicParameter = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter -ArgumentList ($ParameterName, $ParameterDataType, $AttributeCollection)

            # If paramDictionary already exists, add dynamic parameter to dictionary, otherwise create a new dictionary
            if ($paramDictionary -and $paramDictionary.Keys -notcontains $ParameterName) {
                $paramDictionary.Add($ParameterName, $DynamicParameter)
            } else {
                $paramDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary
                $paramDictionary.Add($ParameterName, $DynamicParameter)
            }
        }
        return $paramDictionary
    }

    PROCESS {
        Remove-Variable -Name 'Master' -ErrorAction SilentlyContinue

        #region Source

        $FromSelection = (($PSBoundParameters).keys.where( {$_ -ne 'Property'})[0])
        $FromValue = $PSBoundParameters[$FromSelection]

        $Master = switch ($FromSelection) {
            'FromString' {[byte[]](([Text.Encoding]::UTF8).GetBytes($FromValue))}
            'FromBase64String' {Convert-Object -FromCharArray ([convert]::FromBase64String($FromValue)) -Property ByteArray}
            'FromByteArray' {$FromValue}
            'FromCharArray' {[byte[]]$FromValue}
            'FromGUID' {$FromValue.ToByteArray()}
            'FromHexStringArray' {Convert-Object -FromHexString ($FromValue -join '') -Property ByteArray}
            'FromHexString' {
                $Bytes = [byte[]]::new($FromValue.Length / 2)
                For ($i = 0; $i -lt $FromValue.Length; $i += 2) {
                    $Bytes[$i / 2] = [convert]::ToByte($FromValue.Substring($i, 2), 16)
                }
                $Bytes
            }
            'FromSecureString' {Convert-Object -FromSecureStringObject (ConvertTo-SecureString -String $FromValue) -Property ByteArray}
            'FromSecureStringObject' {
                $marshal = [Runtime.InteropServices.Marshal]
                $Pointer = $marshal::SecureStringToBSTR($FromValue)
                Convert-Object -FromString ($marshal::PtrToStringBSTR($Pointer)) -Property ByteArray
                $marshal::ZeroFreeBSTR($Pointer)
            }
            'FromBinaryStringArray' {[byte[]]($FromValue | foreach-object {[convert]::ToByte($_, 2)})}
            'FromBigInteger' {([System.Numerics.BigInteger]$FromValue).ToByteArray()}
            'FromInt64' {[System.BitConverter]::GetBytes($FromValue)}
            'FromIPAddressString' {Convert-Object -FromCharArray ($FromValue.Split('.') | ForEach-Object {[convert]::ToInt64($_)}) -Properties ByteArray}
            'FromScriptblock' {Convert-Object -FromString $FromValue.ToString() -Property ByteArray}
            'FromByteCollection' {$FromValue.Bytes}
        }

        #region Target
        switch ($PSBoundParameters['Property']) {
            'String' {[Text.Encoding]::UTF8.GetString($Master)}
            'Base64String' {[convert]::ToBase64String($Master)}
            'ByteArray' {$Master}
            'CharArray' {[char[]]$Master}
            'GUID' {try {[guid]::new((Convert-Object -FromByteArray $Master -Property String))} catch {'N/A'}}
            'HexStringArray' {ForEach ($byte in $Master) {("{0:x2}" -f $byte)}}
            'HexString' {(Convert-Object -FromByteArray $Master -Property HexStringArray) -join ''}
            'SecureString' {(ConvertTo-SecureString -String (Convert-Object -FromByteArray $Master -Property String) -AsPlainText -Force | ConvertFrom-SecureString)}
            'SecureStringObject' {(ConvertTo-SecureString -String (Convert-Object -FromByteArray $Master -Properties String) -AsPlainText -Force)}
            'BinaryStringArray' {$Master | foreach-object {[convert]::ToString($_, 2).PadLeft(8, '0')}}
            'BigInteger' {[bigint]::New(($Master += [byte]0))}
            'Int64' {if ($Master.Length -eq 8) {[BitConverter]::ToInt64($Master, 0)} else {'N/A'}}
            'IPAddressString' {$Master -join '.'}
            'ScriptBlock' {try {[scriptblock]::Create((Convert-Object -FromByteArray $Master -Properties String))} catch {[scriptblock]::Create('N/A')}}
            'ByteCollection' {[Microsoft.PowerShell.Commands.ByteCollection]::New($Master)}
            default {
                $Hash = [ordered]@{}
                foreach ($Target in $Conversions) {
                    $Hash.$Target = Convert-Object -FromByteArray $Master -Property $Target
                }
                [pscustomobject]$Hash
            }
        }
    }
}
#EndRegion '.\Public\Convert-Object.ps1' 190
#Region '.\Public\Debug-String.ps1' -1

<#
 
  Prerequisites: PowerShell v5.1 and above (verified; may also work in earlier versions)
  License: MIT
  Author: Michael Klement (email redacted to spare author of spam-hunter-bots.)
 
#>


function Debug-String
{

    <#
        .SYNOPSIS
        Outputs a string in diagnostic form or as source code.
 
        .DESCRIPTION
 
        Author: Michael Klement
 
        Prints a string with control or hidden characters visualized, and optionally
        all non-ASCII-range Unicode characters represented as escape sequences.
 
        With -AsSourceCode, the result is printed in single-line form as a
        double-quoted PowerShell string literal that is reusable as source code,
 
        Common control characters are visualized using PowerShell's own escaping
        notation by default, such as
        "`t" for a tab, "`r" for a CR, but a LF is visualized as itself, as an
        actual newline, unless you specify -SingleLine.
 
        As an alternative, if you want ASCII-range control characters visualized in caret notation
        (see https://en.wikipedia.org/wiki/Caret_notation), similar to cat -A on Linux,
        use -CaretNotation. E.g., ^M then represents a CR; but note that a LF is
        always represented as "$" followed by an actual newline.
 
        Any other control characters as well as otherwise hidden characters or
        format / punctuation characters in the non-ASCII range are represented in
        `u{hex-code-point} notation.
 
        To print space characters as themselves, use -NoSpacesAsDots.
 
        $null inputs are accepted, but a warning is issued.
 
        .PARAMETER InputObject
        Defines the string to analyze
 
        .PARAMETER CaretNotation
        Causes LF to be visualized as "$" and all other ASCII-range control characters
        in caret notation, similar to `cat -A` on Linux.
 
        .PARAMETER Delimiters
        You may optionally specify delimiters that the visualization of each input string is enclosed
        in as a a whole its boundaries. You may specify a single string or a 2-element array.
 
        .PARAMETER NoSpacesAsDots
        By default, space chars. are visualized as "·", the MIDDLE DOT char. (U+00B7)
 
        Use this switch to represent spaces as themselves.
 
        .PARAMETER NoEmphasis
        By default, those characters (other than spaces) that aren't output as themselves,
        i.e. control characters and, if requested with -UnicodeEscapes, non-ASCII-range characters,
        are highlighted by color inversion, using ANSI (VT) escape sequences.
 
        Use this switch to turn off this highlighting.
 
        Note that if $PSStyle.OutputRendering = 'PlainText' is in effect, the highlighting
        isn't *shown* even *without* -NoEmphasis, but the escape sequences are still part
        of the output string. Only -NoEmphasis prevents inclusion of these escape sequences.
 
        .PARAMETER AsSourceCode
        Outputs each input string as a double-quoted PowerShell string
        that is reusable in source code, with embedded double quotes, backticks,
        and "$" signs backtick-escaped.
 
        Use -SingleLine to get a single-line representation.
        Control characters that have no native PS escape sequence are represented
        using `u{<hex-code-point} notation, which will only work in PowerShell *Core*
        (v6+) source code.
 
        .PARAMETER SingleLine
        Requests a single-line representation, where LF characters are represented
        as `n instead of actual line breaks.
 
        .PARAMETER UnicodeEscapes
        Requests that all non-ASCII-range characters - such as accented letters - in
        the input string be represented as Unicode escape sequences in the form
        `u{hex-code-point}.
 
        Whe cominbed with -AsSourceCode, the result is a PowerShell string literal
        composed of ASCII-range characters only, but note that only PowerShell *Core*
        (v6+) understands such Unicode escapes.
 
        By default, only control characters that don't have a native PS escape
        sequence / cannot be represented with caret notation are represented this way.
 
        .EXAMPLE
        PS> "a`ab`t c`0d`r`n" | Debug-String -Delimiters [, ]
        [a`0b`t·c`0d`r`
        ]
 
        .EXAMPLE
        PS> "a`ab`t c`0d`r`n" | Debug-String -CaretNotation
        a^Gb^I c^@d^M$
 
        .EXAMPLE
        PS> "a-ü`u{2028}" | Debug-String -UnicodeEscapes # The dash is an em-dash (U+2014)
        a·`u{2014}·`u{fc}
 
        .EXAMPLE
        PS> "a`ab`t c`0d`r`n" | Debug-String -AsSourceCode -SingleLine # roundtrip
        "a`ab`t c`0d`r`n"
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'NoSpacesAsDots', Justification = 'False positive')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'NoEmphasis', Justification = 'False positive')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'SingleLine', Justification = 'False positive')]
    [CmdletBinding(DefaultParameterSetName = 'Standard', PositionalBinding = $false)]
    param(
        [Parameter(ValueFromPipeline, Mandatory, ParameterSetName = 'Standard', Position = 0)]
        [Parameter(ValueFromPipeline, Mandatory, ParameterSetName = 'Caret', Position = 0)]
        [Parameter(ValueFromPipeline, Mandatory, ParameterSetName = 'AsSourceCode', Position = 0)]
        [AllowNull()]
        [object[]] $InputObject,

        [Parameter(ParameterSetName = 'Standard')]
        [Parameter(ParameterSetName = 'Caret')]
        [string[]] $Delimiters, # for enclosing the visualized strings as a whole - probably rarely used.

        [Parameter(ParameterSetName = 'Caret')]
        [switch] $CaretNotation,

        [Parameter(ParameterSetName = 'Standard')]
        [Parameter(ParameterSetName = 'Caret')]
        [switch] $NoSpacesAsDots,
        [Parameter(ParameterSetName = 'Caret')]
        [Parameter(ParameterSetName = 'Standard')]
        [switch] $NoEmphasis,

        [Parameter(ParameterSetName = 'AsSourceCode')]
        [switch] $AsSourceCode,

        [Parameter(ParameterSetName = 'Standard')]
        [Parameter(ParameterSetName = 'AsSourceCode')]
        [switch] $SingleLine,

        [Parameter(ParameterSetName = 'Standard')]
        [Parameter(ParameterSetName = 'Caret')]
        [Parameter(ParameterSetName = 'AsSourceCode')]
        [switch] $UnicodeEscapes

    )

    begin
    {
        $esc = [char] 0x1b
        if ($UnicodeEscapes)
        {
            $re = [regex] '(?s).' # We must look at *all* characters.
        }
        else
        {
            # Only control / separator / punctuation chars.
            # * \p{C} matches any Unicode control / format/ invisible characters, both inside and outside
            # the ASCII range; note that tabs (`t) are control character too, but not spaces; it comprises
            # the following Unicode categories: Control, Format, Private_Use, Surrogate, Unassigned
            # * \p{P} comprises punctuation characters.
            # * \p{Z} comprises separator chars., including spaces, but not other ASCII whitespace, which is in the Control category.
            # Note: For -AsSourceCode we include ` (backticks) too.
            $re = if ($AsSourceCode)
            {
                [regex] '[`\p{C}\p{P}\p{Z}]'
            }
            else
            {
                [regex] '[\p{C}\p{P}\p{Z}]'
            }
        }
        $openingDelim = $closingDelim = ''
        if ($Delimiters)
        {
            $openingDelim = $Delimiters[0]
            $closingDelim = $Delimiters[1]
            if (-not $closingDelim)
            {
                $closingDelim = $openingDelim
            }
        }
    }
    process
    {
        if ($null -eq $InputObject)
        {
            Write-Warning 'Ignoring $null input.'; return
        }
        foreach ($str in $InputObject)
        {
            if ($null -eq $str)
            {
                Write-Warning 'Ignoring $null input.'; continue
            }
            if ($str -isnot [string])
            {
                $str = -join ($str | Out-String -Stream)
            }
            $strViz = $re.Replace($str, {
                    param($match)
                    $char = [char] $match.Value[0]
                    $codePoint = [uint16] $char
                    $sbToUnicodeEscape = { '`u{' + '{0:x}' -f [int] $Args[0] + '}' }
                    # wv -v ('in [{0}]' -f [char] $match.Value)
                    $vizChar =
                    if ($CaretNotation)
                    {
                        if ($codePoint -eq 0xA)
                        {
                            # LF -> $<newline>
                            '$' + $char
                        }
                        elseif ($codePoint -eq 0x20)
                        {
                            # space char.
                            if ($NoSpacesAsDots)
                            {
                                ' '
                            }
                            else
                            {
                                '·'
                            }
                        }
                        elseif ($codePoint -ge 0 -and $codePoint -le 31 -or $codePoint -eq 127)
                        {
                            # If it's a control character in the ASCII range,
                            # use caret notation too (C0 range).
                            # See https://en.wikipedia.org/wiki/Caret_notation
                            '^' + [char] ((64 + $codePoint) -band 0x7f)
                        }
                        elseif ($codePoint -ge 128)
                        {
                            # Non-ASCII (control) character -> `u{<hex-code-point>}
                            & $sbToUnicodeEscape $codePoint
                        }
                        else
                        {
                            $char
                        }
                    }
                    else
                    {
                        # -not $CaretNotation
                        # Translate control chars. that have native PS escape sequences
                        # into these escape sequences.
                        switch ($codePoint)
                        {
                            0
                            {
                                '`0'; break
                            }
                            7
                            {
                                '`a'; break
                            }
                            8
                            {
                                '`b'; break
                            }
                            9
                            {
                                '`t'; break
                            }
                            11
                            {
                                '`v'; break
                            }
                            12
                            {
                                '`f'; break
                            }
                            10
                            {
                                if ($SingleLine)
                                {
                                    '`n'
                                }
                                else
                                {
                                    "`n"
                                }; break
                            }
                            13
                            {
                                '`r'; break
                            }
                            27
                            {
                                '`e'; break
                            }
                            32
                            {
                                if ($AsSourceCode -or $NoSpacesAsDots)
                                {
                                    ' '
                                }
                                else
                                {
                                    '·'
                                }; break
                            } # Spaces are visualized as middle dots by default.
                            default
                            {
                                # Note: 0x7f (DELETE) is technically still in the ASCII range, but it is a control char. that should be visualized as such
                                # (and has no dedicated escape sequence).
                                if ($codePoint -ge 0x7f)
                                {
                                    & $sbToUnicodeEscape $codePoint
                                }
                                elseif ($AsSourceCode -and $codePoint -eq 0x60)
                                {
                                    # ` (backtick)
                                    '``'
                                }
                                else
                                {
                                    $char
                                }
                            }
                        } # switch
                    }
                    # Return the visualized character.
                    if (-not ($NoEmphasis -or $AsSourceCode) -and $char -ne ' ' -and $vizChar -cne $char)
                    {
                        # Highlight a visualized character that isn't visualized as itself (apart from spaces)
                        # by inverting its colors, using VT / ANSI escape sequences
                        "$esc[7m$vizChar$esc[m"
                    }
                    else
                    {
                        $vizChar
                    }
                }) # .Replace

            # Output
            if ($AsSourceCode)
            {
                '"{0}"' -f ($strViz -replace '"', '`"' -replace '\$', '`$')
            }
            else
            {
                if ($CaretNotation)
                {
                    # If a string *ended* in a newline, our visualization now has
                    # a trailing LF, which we remove.
                    $strViz = $strViz -replace '(?s)^(.*\$)\n$', '$1'
                }
                $openingDelim + $strViz + $closingDelim
            }
        }
    } # process

} # function
#EndRegion '.\Public\Debug-String.ps1' 362
#Region '.\Public\Get-Color.ps1' -1

function Get-Color
{
    <#
    .DESCRIPTION
        Functions showing all consolecolors
    .EXAMPLE
        Get-Color
        Shows all the available consolecolors
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Interactive command')]
    [CmdletBinding()]
    param()

    [enum]::GetNames([consolecolor]) | ForEach-Object {
        Write-Host (' {0}' -f $_).PadRight(15) -BackgroundColor $_ -NoNewline
        Write-Host ' ' -NoNewline
        Write-Host ('{0} ' -f $_).PadLeft(15) -BackgroundColor $_ -ForegroundColor Black
    }
}
#EndRegion '.\Public\Get-Color.ps1' 20
#Region '.\Public\Get-DotNetVersion.ps1' -1

function Get-DotNetVersion
{
    <#
        .DESCRIPTION
            Script retreivs the .net framework version from the registry
        .PARAMETER Release
            Defines the release version
        .EXAMPLE
            Get-DotNetVersion
 
            Script retreivs the .net framework version from the registry
    #>

    [CmdletBinding()]
    param(
        [int]$Release = ''
    )

    if ($Release -eq '')
    {
        $Release = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full' | Select-Object -ExpandProperty Release
    }

    $RegKey = [hashtable][ordered]@{
        VersionNumber = $Release
        Version       = ''
        OS            = ''
    }

    switch ($RegKey.VersionNumber)
    {
        378389
        {
            $RegKey.Version = '.NET Framework 4.5.0'
            $RegKey.OS = 'All'
        }
        378675
        {
            $RegKey.Version = '.NET Framework 4.5.1'
            $RegKey.OS = 'Windows 8.1'
        }
        378758
        {
            $RegKey.Version = '.NET Framework 4.5.1'
            $RegKey.OS = 'All other than Windows 8.1'
        }
        379893
        {
            $RegKey.Version = '.NET Framework 4.5.2'
            $RegKey.OS = 'All'
        }
        393295
        {
            $RegKey.Version = '.NET Framework 4.6.0'
            $RegKey.OS = 'Windows 10'
        }
        393297
        {
            $RegKey.Version = '.NET Framework 4.6.0'
            $RegKey.OS = 'All other than Windows 10'
        }
        394254
        {
            $RegKey.Version = '.NET Framework 4.6.1'
            $RegKey.OS = 'Windows 10 November Update'
        }
        394271
        {
            $RegKey.Version = '.NET Framework 4.6.1'
            $RegKey.OS = 'All other than Windows 10 November Update'
        }
        394802
        {
            $RegKey.Version = '.NET Framework 4.6.2'
            $RegKey.OS = 'Windows 10 Anniversary Update and Windows Server 2016'
        }
        394806
        {
            $RegKey.Version = '.NET Framework 4.6.2'
            $RegKey.OS = 'All except Windows 10 Anniversary Update and Windows Server 2016'
        }
        460798
        {
            $RegKey.Version = '.NET Framework 4.7.0'
            $RegKey.OS = 'Windows 10 Creators Update'
        }
        460805
        {
            $RegKey.Version = '.NET Framework 4.7.0'
            $RegKey.OS = 'All except Windows 10 Creators Update'
        }
        461308
        {
            $RegKey.Version = '.NET Framework 4.7.1'
            $RegKey.OS = 'Windows 10 Creators Update and Windows Server, version 1709'
        }
        461310
        {
            $RegKey.Version = '.NET Framework 4.7.1'
            $RegKey.OS = 'All except Windows 10 Creators Update and Windows Server, version 1709'
        }
        461808
        {
            $RegKey.Version = '.NET Framework 4.7.2'
            $RegKey.OS = 'Windows 10 April 2018 Update and Windows Server, version 1803'
        }
        461814
        {
            $RegKey.Version = '.NET Framework 4.7.2'
            $RegKey.OS = 'All except [Windows 10 April 2018 Update] and [Windows Server, version 1803]'
        }
        528449
        {
            $RegKey.Version = '.NET Framework 4.8.0'
            $RegKey.OS = 'Windows 11 and Windows Server 2022'
        }
        528040
        {
            $RegKey.Version = '.NET Framework 4.8'
            $RegKey.OS = 'Windows 10 May 2019 Update and Windows 10 November 2019 Update'
        }
        528372
        {
            $RegKey.Version = '.NET Framework 4.8'
            $RegKey.OS = 'Windows 10 May 2020 Update and Windows 10 October 2020 Update and Windows 10 May 2021 Update'
        }
        528049
        {
            $RegKey.Version = '.NET Framework 4.8'
            $RegKey.OS = 'All except [Windows 11],[Windows Server 2022],[Windows 10 May 2020 Update],[Windows 10 October 2020 Update],[Windows 10 May 2021 Update],[Windows 10 May 2019 Update],[Windows 10 November 2019 Update]'
        }
        533325
        {
            $RegKey.Version = '.NET Framework 4.8.1'
            $RegKey.OS = 'All'
        }
        default
        {
            $RegKey.Version = '<Unknown>'
        }
    }

    Write-Output (New-Object -TypeName PSObject -Property $RegKey)
}
#EndRegion '.\Public\Get-DotNetVersion.ps1' 144
#Region '.\Public\Get-Office365IPURL.ps1' -1

function Get-Office365IPURL
{
    <#
      .DESCRIPTION
      Retreive a list of ip and urls required for communication to and from Office 365.
 
      .PARAMETER Services
      Defines which services to retreive IP and URLs for. Valid values are Skype,Exchange,Sharepoint.
      Note that Teams is included in the Skype ruleset and OneDrive is included in the Sharepoint ruleset.
 
      .PARAMETER OnlyRequired
      Defines that only rules that are required are returned. This will exclude optional optimize rules.
 
      .PARAMETER Types
      Defines what type of rules to return. Valid values are URL,IP4,IP6
 
      .PARAMETER OutputFormat
      Defines the output format, defaults to an array of objects. Valid values are Object and JSON as of now. If a specific format is
      needed for a firewall please raise a issue with the instructions for the format and it is possible to create preset for it.
 
      .PARAMETER Office365IPURL
      Defines the URL to the Office 365 IP URL Endpoint. Defaults to 'https://endpoints.office.com/endpoints/worldwide?clientrequestid=b10c5ed1-bad1-445f-b386-b919946339a7'.
      Provided as parameter to allow queries to other environments than worldwide as well as keep agility if Microsoft would change URL.
 
      .EXAMPLE
      Get-Office365IPURL -Services Exchange,Skype -OnlyRequired -Types IP4,URL -Outputformat JSON
 
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'False positive')]
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateSet('Skype', 'Exchange', 'Sharepoint')]
        [string[]]
        $Services = @('Skype', 'Exchange', 'Sharepoint'),

        [Parameter()]
        [switch]
        $OnlyRequired,

        [Parameter()]
        [ValidateSet('URL', 'IP4', 'IP6')]
        [string[]]
        $Types = @('URL', 'IP4', 'IP6'),

        [Parameter()]
        [ValidateSet('Object', 'JSON')]
        [string]
        $OutputFormat = 'Object',

        [Parameter()]
        [string]
        $Office365IPURL = 'https://endpoints.office.com/endpoints/worldwide?clientrequestid=b10c5ed1-bad1-445f-b386-b919946339a7'
    )

    $ErrorActionPreference = 'Stop'

    # Get latest IP URL info
    $Office365Endpoints = Invoke-RestMethod -Uri $Office365IPURL -Method Get

    # Import net module
    Import-Module indented.net.ip

    # Loop through rules
    $Result = $Office365Endpoints | Where-Object { $Services -contains $_.ServiceArea } | ForEach-Object {
        $CurrentRule = $PSItem

        $ObjectHash = [ordered]@{
            Group    = ''
            Service  = $CurrentRule.ServiceArea
            Type     = ''
            Protocol = ''
            Port     = $null
            Endpoint = ''
            Required = $CurrentRule.Required
        }

        $CurrentRule.URLs | Where-Object { $_ -ne '' -and $_ -ne $null } | ForEach-Object {
            $ObjectHash.Type = 'URL'
            $ObjectHash.Endpoint = $PSItem

            $CurrentRule.TCPPorts -split (',') | Where-Object { $_ -ne '' } | ForEach-Object {
                $ObjectHash.Protocol = 'TCP'
                $ObjectHash.Port = $PSItem.Trim()
                $ObjectHash.Group = $CurrentRule.ServiceArea + '_' + 'TCP' + '_' + "$($PSItem.Trim())" + '_' + 'URL'
                [pscustomobject]$ObjectHash
            }
            $CurrentRule.UDPPorts -split (',') | Where-Object { $_ -ne '' } | ForEach-Object {
                $ObjectHash.Protocol = 'UDP'
                $ObjectHash.Port = $PSItem.Trim()
                $ObjectHash.Group = $CurrentRule.ServiceArea + '_' + 'UDP' + '_' + "$($PSItem.Trim())" + '_' + 'URL'
                [pscustomobject]$ObjectHash
            }
        }
        # Process IPs
        $CurrentRule.ips | Where-Object { $_ -ne '' -and $_ -ne $null } | ForEach-Object {
            if ($PSItem -like '*:*')
            {
                $ObjectHash.Type = 'IP6'
            }
            else
            {
                $ObjectHash.Type = 'IP4'
            }
            $ObjectHash.Endpoint = $PSItem

            $CurrentRule.TCPPorts -split (',') | Where-Object { $_ -ne '' } | ForEach-Object {
                $ObjectHash.Protocol = 'TCP'
                $ObjectHash.Port = $PSItem.Trim()
                $ObjectHash.Group = $CurrentRule.ServiceArea + '_' + 'TCP' + '_' + "$($PSItem.Trim())" + '_' + 'IP'
                [pscustomobject]$ObjectHash
            }
            $CurrentRule.UDPPorts -split (',') | Where-Object { $_ -ne '' } | ForEach-Object {
                $ObjectHash.Protocol = 'UDP'
                $ObjectHash.Port = $PSItem.Trim()
                $ObjectHash.Group = $CurrentRule.ServiceArea + '_' + 'UDP' + '_' + "$($PSItem.Trim())" + '_' + 'IP'
                [pscustomobject]$ObjectHash
            }
        }
    } | Where-Object { $Types -contains $PSItem.Type }

    switch ($OutputFormat)
    {
        'Object'
        {
            if ($OnlyRequired)
            {
                $Result | Where-Object { $_.required -eq $true } | Sort-Object -Property Group
            }
            else
            {
                $Result | Sort-Object -Property Group
            }
        }
        'JSON'
        {
            $JSONHash = [ordered]@{}
            $Result | Group-Object -Property Protocol | ForEach-Object {
                $CurrentProtocolGroup = $PSItem

                # Create protocol node if it does not exist
                if (-not $JSONHash.Contains($CurrentProtocolGroup.Name))
                {
                    $JSONHash.Add($CurrentProtocolGroup.Name, [ordered]@{})
                }
                $CurrentProtocolGroup.Group | Group-Object -Property Port | ForEach-Object {
                    $CurrentPortGroup = $PSItem

                    # Create port node if it does not exists
                    if (-not $JSONHash.$($CurrentProtocolGroup.Name).Contains($CurrentPortGroup.Name))
                    {
                        $JSONHash.$($CurrentProtocolGroup.Name).Add($CurrentPortGroup.Name, [ordered]@{})
                    }

                    $CurrentPortGroup.Group | Group-Object -Property Type | ForEach-Object {
                        $CurrentTypeGroup = $PSItem
                        $EndpointArray = [string[]]($CurrentTypeGroup.Group.Endpoint)
                        $JSONHash.$($CurrentProtocolGroup.Name).$($CurrentPortGroup.Name).Add($CurrentTypeGroup.Name, $EndpointArray)
                    }
                }
            }
            $JSONHash | ConvertTo-Json -Depth 10
        }
    }
}
#EndRegion '.\Public\Get-Office365IPURL.ps1' 166
#Region '.\Public\Get-PublicIP.ps1' -1

function Get-PublicIP {
    <#
    .DESCRIPTION
        Get the current public facing IP address
    .PARAMETER Name
        Description
    .EXAMPLE
        Get-PublicIP
        Description of example
    #>


    [CmdletBinding()]
    param(
    )
    PROCESS {
        Invoke-RestMethod -Uri 'http://ipinfo.io/json'
    }
}
#EndRegion '.\Public\Get-PublicIP.ps1' 19
#Region '.\Public\Get-Selector.ps1' -1

function Get-Selector
{
    <#
    .DESCRIPTION
        Increments a string of characters
    .PARAMETER PreviousSelector
        Defines the string to be incremented
    .EXAMPLE
        Get-Selector -PreviousSelector AA
 
        Would return AB
    .EXAMPLE
        Get-Selector -PreviousSelector ZZ
 
        Would return AAA
    #>


    param (
        [string]$PreviousSelector
    )

    $Length = $PreviousSelector.Length

    $SelectorString = $PreviousSelector.PadLeft($Length, '0')
    $SelectorCharArray = $SelectorString.ToCharArray()

    function Increment
    {
        param(
            [char[]]$CharArray,
            [int]$position
        )

        if ($position -lt 0)
        {
            throw 'Selector wrapped around, exceeding string length capability. Increase string length to accomodate more values.'
        }

        if ($CharArray[$position] -eq 90)
        {
            $CharArray[$position] = 65
            $CharArray = Increment -CharArray $CharArray -position ($position - 1)
        }
        else
        {
            if ($CharArray[$position] -eq '0')
            {
                $CharArray[$position] = 65
            }
            else
            {
                $CharArray[$position] = [char](($CharArray[$position] -as [int]) + 1)
            }
        }
        return $CharArray
    }

    try
    {
        $Result = Increment -CharArray $SelectorCharArray -position ($Length - 1)
    }
    catch
    {
        if ($_.Exception.message -like 'Selector wrapped around*')
        {
            $SelectorString = $PreviousSelector.PadLeft($Length + 1, '0')
            $SelectorCharArray = $SelectorString.ToCharArray()
            $Result = Increment -CharArray $SelectorCharArray -position ($Length)
        }
        else
        {
            throw $_
        }
    }
    return [string](($Result -join '').Trim('0'))
}
#EndRegion '.\Public\Get-Selector.ps1' 77
#Region '.\Public\Get-StringHash.ps1' -1

function Get-StringHash
{
    <#
        .DESCRIPTION
            Generates a hash of an string object
        .PARAMETER Strings
            Defines the array of strings to generate hashes of
        .PARAMETER Algorithm
            Defines which hashing algorithm to use, valid values are MD5, SHA256, SHA384 and SHA512. Defaults to SHA512
        .PARAMETER Salt
            Defines a specific salt to use, this is useful when recalculating a string hash with a known salt for comparison. A new random salt
            is generated by default for every string that is processed.
        .PARAMETER Iterations
            Defines the number of rehashing operations that is performed.
        .PARAMETER RandomSalt
            Defines that a random salt should be used
        .EXAMPLE
            Get-StringHash -Strings 'ThisIsAComplicatedPassword123#' -Algorithm SHA512
            Hashes the string specified
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'False positive')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)][String[]]$Strings,
        [ValidateSet('MD5', 'SHA256', 'SHA384', 'SHA512', 'SHA1')][string]$Algorithm = 'SHA256',
        [string]$Salt = '',
        [switch]$RandomSalt,
        [int]$Iterations = 10
    )
    BEGIN
    {
        if ($Iterations -eq 0)
        {
            $Iterations = 1
        }
    }
    PROCESS
    {
        $Strings | ForEach-Object {
            # if no salt is specified, generate a new salt to use.
            if ($RandomSalt)
            {
                $Salt = [guid]::NewGuid().Guid
            }
            $String = $_
            $StringBytes = [Text.Encoding]::UTF8.GetBytes($String)
            if ($Salt -ne '')
            {
                $SaltBytes = [Text.Encoding]::UTF8.GetBytes($salt)
            }
            $Hasher = [Security.Cryptography.HashAlgorithm]::Create($Algorithm)
            $StringBuilder = New-Object -TypeName System.Text.StringBuilder

            $Measure = Measure-Command -Expression {
                # Compute first hash
                if ($Salt -ne '')
                {
                    $HashBytes = $Hasher.ComputeHash($StringBytes + $SaltBytes)
                }
                else
                {
                    $HashBytes = $Hasher.ComputeHash($StringBytes)
                }

                # Iterate rehashing
                if ($Iterations -ge 2)
                {
                    2..$Iterations | ForEach-Object {
                        if ($Salt -ne '')
                        {
                            $HashBytes = $Hasher.ComputeHash($HashBytes + $StringBytes + $SaltBytes)
                        }
                        else
                        {
                            $HashBytes = $Hasher.ComputeHash($HashBytes + $StringBytes)
                        }
                    }
                }
            }

            # Convert final hash to a string
            $HashBytes | ForEach-Object {
                $null = $StringBuilder.Append($_.ToString('x2'))
            }

            # Return object
            [pscustomobject]@{
                Hash           = $StringBuilder.ToString()
                OriginalString = $String
                Algorithm      = $algorithm
                Iterations     = $Iterations
                Salt           = $salt
                Compute        = [math]::Round($Measure.TotalMilliseconds)
            }

        }
    }
}
#EndRegion '.\Public\Get-StringHash.ps1' 99
#Region '.\Public\Group-ObjectEvenly.ps1' -1

function Group-ObjectEvenly
{
    <#
        .DESCRIPTION
            Function that splits a object array into groups of a specific number
        .PARAMETER InputObject
            Defines the object array to split
        .PARAMETER SizeOfGroup
            Defines the size of each group of objects
        .PARAMETER NbrOfGroups
            Defines the number of groups should be created, the objects will be evenly distributed within the groups
        .EXAMPLE
            Get-Process | Group-ObjectByAmount -Amount 5
 
            This example collects all processes running and groups them in groups of five processes per object.
    #>

    [CmdletBinding()]
    [OutputType([System.Collections.ArrayList])]
    param(
        [Parameter(Mandatory, ValueFromPipeline)][object[]]$InputObject,
        [Parameter(Mandatory, ParameterSetName = 'SizeOfGroup')][int]$SizeOfGroup,
        [Parameter(Mandatory, ParameterSetName = 'NbrOfGroups')][int]$NbrOfGroups
    )
    begin
    {
        $AllObjects = [collections.arraylist]::new()
        $Groups = [collections.arraylist]::new()
    }
    process
    {
        $InputObject | ForEach-Object {
            $null = $AllObjects.Add($_)
        }
    }
    end
    {
        switch ($PSCmdlet.ParameterSetName)
        {
            'SizeOfGroup'
            {
                $ID = 1
                while ($AllObjects.Count -ne 0)
                {
                    $Group = [pscustomobject]@{
                        ID    = $ID
                        Group = $AllObjects | Select-Object -First $SizeOfGroup
                    }
                    $null = $Groups.Add($Group)
                    $AllObjects = $AllObjects | Select-Object -Skip $SizeOfGroup
                    $ID++
                }
                $Groups
            }
            'NbrOfGroups'
            {
                $ID = 1
                while ($AllObjects.Count -ne 0)
                {
                    $SizeOfGroup = [Math]::Max(([Math]::Round(($AllObjects.count / $NbrOfGroups))), 1)
                    $Group = [pscustomobject]@{
                        ID    = $ID
                        Group = $AllObjects | Select-Object -First $SizeOfGroup
                    }
                    $null = $Groups.Add($Group)
                    $AllObjects = $AllObjects | Select-Object -Skip $SizeOfGroup
                    $ID++
                    $NbrOfGroups--
                }
                $Groups
            }
        }
    }
}
#EndRegion '.\Public\Group-ObjectEvenly.ps1' 74
#Region '.\Public\New-EXOUnattendedAzureApp.ps1' -1

<#PSLicenseInfo
Copyright © 2024 Hannes Palmquist
 
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.
 
PSLicenseInfo#>

<#PSScriptInfo
{
    "VERSION": "1.0.0.0",
    "GUID": "7fef8b41-e91d-4cb0-b656-1201c3820eb8",
    "FILENAME": "New-EXOUnattendedAzureApp.ps1",
    "AUTHOR": "Hannes Palmquist",
    "AUTHOREMAIL": "hannes.palmquist@outlook.com",
    "CREATEDDATE": "2024-10-04",
    "COMPANYNAME": "N/A",
    "COPYRIGHT": "© 2024, Hannes Palmquist, All Rights Reserved"
}
PSScriptInfo#>


function New-EXOUnattendedAzureApp
{
    <#
        .DESCRIPTION
            Provisions a new azure app and configures it for Exchange Online management
        .PARAMETER Organization
            Defines the organization that the azure app should be created in.
        .PARAMETER OutputDirectory
            Defines the output directory of the certificate
        .PARAMETER AppName
            Defines the name of the new azure app. Default to ExchangeScriptAccess
        .PARAMETER PassThru
            If specified an object is returned with the result data instead of written to host.
        .EXAMPLE
            New-EXOUnattendedAzureApp -Organization contoso.onmicrosoft.com -OutputDirectory C:\output -AppName 'ExchangeManagement'
 
            Creates new azure app named ExchangeManagement.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Interactive script')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Do not agree that a new item changes state')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)][string]$Organization,
        [Parameter(Mandatory)][string]$OutputDirectory,
        [Parameter()][string]$AppName = 'ExchangeScriptAccess',
        [Parameter()][switch]$PassThru
    )

    $ErrorActionPreference = 'Stop'

    $ImportModules = @('PKI', 'Microsoft.Graph.Authentication', 'Microsoft.Graph.Applications', 'Microsoft.Graph.DeviceManagement.Enrollment', 'Microsoft.Graph.Identity.Governance')
    foreach ($Module in $ImportModules)
    {
        try
        {
            Write-Verbose "Importing module $Module"
            $SavedVerbosePreference = $VerbosePreference
            $VerbosePreference = 'SilentlyContinue'
            switch ($Module)
            {
                'PKI'
                {
                    Import-Module $Module -Force -Verbose:$false -ErrorAction Stop -Cmdlet 'New-SelfSignedCertificate', 'Export-PfxCertificate', 'Export-Certificate'
                }
                'Microsoft.Graph.Identity.Governance'
                {
                    Import-Module $Module -Force -Verbose:$false -ErrorAction Stop -MinimumVersion 2.15.0 -Cmdlet 'New-MgRoleManagementDirectoryRoleAssignment'
                }
                'Microsoft.Graph.Authentication'
                {
                    Import-Module $Module -Force -Verbose:$false -ErrorAction Stop -MinimumVersion 2.15.0 -Cmdlet 'Connect-Graph'
                }
                'Microsoft.Graph.Applications'
                {
                    Import-Module $Module -Force -Verbose:$false -ErrorAction Stop -MinimumVersion 2.15.0 -Cmdlet 'New-MgApplication', 'New-MgServicePrincipal'
                }
                default
                {
                    Import-Module $Module -Force -Verbose:$false -ErrorAction Stop
                }
            }
            $VerbosePreference = $SavedVerbosePreference
            Write-Verbose "Imported module $Module"
        }
        catch
        {
            Write-Error "Failed to import module $Module with error: $_" -ErrorAction Stop
        }
    }


    $ResultObjectHash = [ordered]@{
        Organization        = $Organization
        OutputDirectory     = $OutputDirectory
        AppName             = $AppName
        Certificate         = $null
        CertificatePassword = New-Password -Length 20 -ReturnSecureStringObject
        CertificatePFXPath  = (Join-Path -Path $OutputDirectory -ChildPath "$Organization.pfx")
        CertificateCERPath  = (Join-Path -Path $OutputDirectory -ChildPath "$Organization.cer")
    }

    $null = Connect-Graph -Scopes 'Application.ReadWrite.All', 'RoleManagement.ReadWrite.Directory' -TenantId $Organization

    $ResultObjectHash.Certificate = New-SelfSignedCertificate -DnsName $ResultObjectHash.Organization -CertStoreLocation 'cert:\CurrentUser\My' -NotAfter (Get-Date).AddYears(1) -KeySpec KeyExchange -FriendlyName $AppName
    Write-Verbose "Created self-signed certificate with thumbprint: $($ResultObjectHash.Certificate.Thumbprint)"

    $null = $ResultObjectHash.Certificate | Export-PfxCertificate -FilePath $ResultObjectHash.CertificatePFXPath -Password $ResultObjectHash.CertificatePassword
    Write-Verbose "Exported certificate with private key (pfx) to: $($ResultObjectHash.CertificatePFXPath)"

    $null = $ResultObjectHash.Certificate | Export-Certificate -FilePath $ResultObjectHash.CertificateCERPath
    Write-Verbose "Exported certificate with public key (cer) to: $($ResultObjectHash.CertificateCERPath)"

    $Web = [Microsoft.Graph.PowerShell.Models.MicrosoftGraphWebApplication]::New()
    $Web.RedirectUris = 'https://localhost'
    Write-Verbose 'Initialized MicrosoftGraphWebApplication'

    $ResourceAccess = [Microsoft.Graph.PowerShell.Models.MicrosoftGraphResourceAccess]::New()
    $ResourceAccess.Id = 'dc50a0fb-09a3-484d-be87-e023b12c6440'
    $ResourceAccess.Type = 'Role'
    Write-Verbose 'Initialized MicrosoftGraphResourceAccess'

    $RequiredResourceAccess = [Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess]::New()
    $RequiredResourceAccess.ResourceAccess = $ResourceAccess
    $RequiredResourceAccess.ResourceAppId = '00000002-0000-0ff1-ce00-000000000000'
    Write-Verbose 'Initialized MicrosoftGraphRequiredResourceAccess'

    $KeyCred = [Microsoft.Graph.PowerShell.Models.MicrosoftGraphKeyCredential]::New()
    $keycred.DisplayName = $ResultObjectHash.Organization
    $KeyCred.Key = $ResultObjectHash.Certificate.RawData
    $KeyCred.KeyId = $ResultObjectHash.Certificate.SerialNumber
    $KeyCred.StartDateTime = $ResultObjectHash.Certificate.NotBefore
    $KeyCred.EndDateTime = $ResultObjectHash.Certificate.NotAfter
    $KeyCred.Usage = 'Verify'
    $KeyCred.Type = 'AsymmetricX509Cert'
    Write-Verbose 'Initialized MicrosoftGraphKeyCredential'

    $ResultObjectHash.AzureAppRegistration = New-MgApplication  `
        -DisplayName $ResultObjectHash.AppName `
        -Description 'Used by automations that need access to exchange online' `
        -SignInAudience AzureADMyOrg `
        -Web $Web `
        -RequiredResourceAccess $RequiredResourceAccess `
        -KeyCredentials $KeyCred
    Write-Verbose "Created Azure App: $($ResultObjectHash.AppName)"

    $ResultObjectHash.ServicePrincipal = New-MgServicePrincipal -AppId $ResultObjectHash.AzureAppRegistration.AppId

    Write-Host 'Waiting 60 seconds for Azure app to be provisioned...'
    Start-Sleep -Seconds 60
    Write-Host ''
    Write-Host ' If a webbrowser does not open, paste the following link into a web browser manually'
    Write-Host "https://login.microsoftonline.com/$($ResultObjectHash.Organization)/adminconsent?client_id=$($ResultObjectHash.AzureAppRegistration.Appid)"
    Write-Host ''
    Start-Process "https://login.microsoftonline.com/$($ResultObjectHash.Organization)/adminconsent?client_id=$($ResultObjectHash.AzureAppRegistration.Appid)"
    $null = Read-Host ' Press enter once consent has been given'

    $null = New-MgRoleManagementDirectoryRoleAssignment -PrincipalId $ResultObjectHash.ServicePrincipal.id -RoleDefinitionId '29232cdf-9323-42fd-ade2-1d097af3e4de' -DirectoryScopeId /
    Write-Verbose 'Added role assignment'

    if ($PassThru)
    {
        Write-Output ([pscustomobject]$ResultObjectHash)
    }
    else
    {
        Write-Host ''
        Write-Host ' Use the following command to connect Exchange Online:'
        Write-Host ''
        Write-Host -ForegroundColor Cyan "Connect-ExchangeOnline -CertificateThumbprint `"$($ResultObjectHash.Certificate.Thumbprint)`" -AppId `"$($ResultObjectHash.AzureAppRegistration.AppId)`" -Organization `"$($ResultObjectHash.Organization)`""
        Write-Host ''
        Write-Host ' NOTE: Restart powershell before connecting to Exchange Online' -ForegroundColor Yellow
        Write-Host ' NOTE: It could take some time before the added roles are effective. If you get an error regarding missing permissions, please wait a minute and try again.' -ForegroundColor Yellow
        Write-Host ' NOTE: Password for the pfx file is (write it down) ' -NoNewline -ForegroundColor Yellow; Write-Host ((New-Object -TypeName pscredential -ArgumentList 'notused', $ResultObjectHash.CertificatePassword).GetNetworkCredential().Password) -ForegroundColor Magenta
        Write-Host ''
    }

    Remove-Variable ResultObjectHash -ErrorAction SilentlyContinue
}
#EndRegion '.\Public\New-EXOUnattendedAzureApp.ps1' 193
#Region '.\Public\New-EXOUnattendedCert.ps1' -1

<#PSLicenseInfo
Copyright © 2024 Hannes Palmquist
 
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.
 
PSLicenseInfo#>

<#PSScriptInfo
{
    "VERSION": "1.0.0.0",
    "GUID": "7fef8b41-e91d-4cb0-b656-1201c3820eb8",
    "FILENAME": "New-EXOUnattendedCert.ps1",
    "AUTHOR": "Hannes Palmquist",
    "AUTHOREMAIL": "hannes.palmquist@outlook.com",
    "CREATEDDATE": "2024-10-04",
    "COMPANYNAME": "N/A",
    "COPYRIGHT": "© 2024, Hannes Palmquist, All Rights Reserved"
}
PSScriptInfo#>


function New-EXOUnattendedCert
{
    <#
        .DESCRIPTION
            Creates a new self-signed certificate than can be used for azure app authentication.
        .PARAMETER Organization
            Defines the organization that the azure app is created in.
        .PARAMETER OutputDirectory
            Defines the output directory of the certificate
        .PARAMETER AppID
            Defines the AppID to add the new self signed certificate to.
        .PARAMETER DisplayName
            Defines the displayname of the new certificate
        .PARAMETER PassThru
            If specified an object is returned with the result data instead of written to host.
        .EXAMPLE
            New-EXOUnattendedCert -Organization contoso.onmicrosoft.com -OutputDirectory C:\output -AppID <guid> -DisplayName 'Exchange Management Certificate'
 
            Creates new azure app named ExchangeManagement.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Do not agree that a new item changes state')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Interactive script')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)][string]$Organization,
        [Parameter(Mandatory)][string]$OutputDirectory,
        [Parameter(Mandatory)][string]$AppID,
        [Parameter(Mandatory)][string]$DisplayName,
        [Parameter()][switch]$PassThru
    )

    $ErrorActionPreference = 'Stop'

    $ImportModules = @('PKI', 'Microsoft.Graph.Authentication', 'Microsoft.Graph.Applications', 'Microsoft.Graph.DeviceManagement.Enrollment', 'Microsoft.Graph.Identity.Governance')
    foreach ($Module in $ImportModules)
    {
        try
        {
            Write-Verbose "Importing module $Module"
            $SavedVerbosePreference = $VerbosePreference
            $VerbosePreference = 'SilentlyContinue'
            switch ($Module)
            {
                'PKI'
                {
                    Import-Module $Module -Force -Verbose:$false -ErrorAction Stop -Cmdlet 'New-SelfSignedCertificate', 'Export-PfxCertificate', 'Export-Certificate'
                }
                'Microsoft.Graph.Identity.Governance'
                {
                    Import-Module $Module -Force -Verbose:$false -ErrorAction Stop -MinimumVersion 2.15.0 -Cmdlet 'New-MgRoleManagementDirectoryRoleAssignment'
                }
                'Microsoft.Graph.Authentication'
                {
                    Import-Module $Module -Force -Verbose:$false -ErrorAction Stop -MinimumVersion 2.15.0 -Cmdlet 'Connect-Graph'
                }
                'Microsoft.Graph.Applications'
                {
                    Import-Module $Module -Force -Verbose:$false -ErrorAction Stop -MinimumVersion 2.15.0 -Cmdlet 'New-MgApplication', 'New-MgServicePrincipal'
                }
                default
                {
                    Import-Module $Module -Force -Verbose:$false -ErrorAction Stop
                }
            }
            $VerbosePreference = $SavedVerbosePreference
            Write-Verbose "Imported module $Module"
        }
        catch
        {
            Write-Error "Failed to import module $Module with error: $_" -ErrorAction Stop
        }
    }


    $ResultObjectHash = [ordered]@{
        Organization        = $Organization
        OutputDirectory     = $OutputDirectory
        AppId               = $AppId
        Certificate         = $null
        CertificatePassword = New-Password -Length 20 -ReturnSecureStringObject
        CertificatePFXPath  = (Join-Path -Path $OutputDirectory -ChildPath "$Organization.pfx")
        CertificateCERPath  = (Join-Path -Path $OutputDirectory -ChildPath "$Organization.cer")
    }

    $null = Connect-Graph -Scopes 'Application.ReadWrite.All', 'RoleManagement.ReadWrite.Directory' -TenantId $Organization

    $ResultObjectHash.Certificate = New-SelfSignedCertificate -DnsName $ResultObjectHash.Organization -CertStoreLocation 'cert:\CurrentUser\My' -NotAfter (Get-Date).AddYears(1) -KeySpec KeyExchange -FriendlyName $AppName
    Write-Verbose "Created self-signed certificate with thumbprint: $($ResultObjectHash.Certificate.Thumbprint)"

    $null = $ResultObjectHash.Certificate | Export-PfxCertificate -FilePath $ResultObjectHash.CertificatePFXPath -Password $ResultObjectHash.CertificatePassword
    Write-Verbose "Exported certificate with private key (pfx) to: $($ResultObjectHash.CertificatePFXPath)"

    $null = $ResultObjectHash.Certificate | Export-Certificate -FilePath $ResultObjectHash.CertificateCERPath
    Write-Verbose "Exported certificate with public key (cer) to: $($ResultObjectHash.CertificateCERPath)"

    $Base64String = [convert]::ToBase64String(($ResultObjectHash.Certificate.RawData))


    $params = @{
        keyCredentials = @(
            @{
                endDateTime   = $ResultObjectHash.Certificate.NotAfter
                startDateTime = $ResultObjectHash.Certificate.NotBefore
                type          = 'AsymmetricX509Cert'
                usage         = 'Verify'
                key           = [System.Text.Encoding]::ASCII.GetBytes($Base64String)
                displayname   = "$($DisplayName)_$((Get-Date).ToString('yyyy-MM-dd_HHmm'))"
            }
        )
    }

    $ExistingKeyCredentials = Get-MgApplication -Search "appid:$($ResultObjectHash.AppID)" -ConsistencyLevel eventual -Select keyCredentials

    foreach ($Key in $ExistingKeyCredentials.KeyCredentials)
    {
        $params.keyCredentials +=
        @{
            customKeyIdentifier = $Key.CustomKeyIdentifier
            type                = $Key.Type
            usage               = $Key.Usage
            key                 = $Key.Key
            displayname         = $Key.DisplayName
        }
    }

    $ExistingAppId = Get-MgApplication -Search "appid:$($ResultObjectHash.AppID)" -ConsistencyLevel eventual

    Update-MgApplication -ApplicationId $ExistingAppId.Id -BodyParameter $params

    if ($PassThru)
    {
        Write-Output ([pscustomobject]$ResultObjectHash)
    }
    else
    {
        Write-Host ''
        Write-Host ' Use the following command to connect Exchange Online:'
        Write-Host ''
        Write-Host -ForegroundColor Cyan "Connect-ExchangeOnline -CertificateThumbprint `"$($ResultObjectHash.Certificate.Thumbprint)`" -AppId `"$($ResultObjectHash.AppId)`" -Organization `"$($ResultObjectHash.Organization)`""
        Write-Host ''
        Write-Host ' NOTE: Restart powershell before connecting to Exchange Online' -ForegroundColor Yellow
        Write-Host ' NOTE: It could take some time before the added roles are effective. If you get an error regarding missing permissions, please wait a minute and try again.' -ForegroundColor Yellow
        Write-Host ' NOTE: Password for the pfx file is (write it down) ' -NoNewline -ForegroundColor Yellow; Write-Host ((New-Object -TypeName pscredential -ArgumentList 'notused', $ResultObjectHash.CertificatePassword).GetNetworkCredential().Password) -ForegroundColor Magenta
        Write-Host ''
    }

    Remove-Variable ResultObjectHash -ErrorAction SilentlyContinue
}
#EndRegion '.\Public\New-EXOUnattendedCert.ps1' 183
#Region '.\Public\New-Password.ps1' -1

function New-Password
{
    <#
        .DESCRIPTION
            Function that generates passwords
 
            The first characters is a captial consonant letter
            The second character is a lower vowel letter
            The third character is a lower consonant letter
            The fourth character is lower vowel letter
            The remaining four characters are digits.
 
            This structure creates a password that is easy to remember but is less secure. What
            makes the password easier to remember is that the most character combinations are
            reasonably easy to prenounce. These password should only be used temporary. An
            few examples might be:
 
            Wodi6380
            Jaki2830
            Kezo2617
        .PARAMETER SkipCompromisedCheck
            By default each generated password is check against the "have i been pawned" database. To disable this check specify this parameter.
            Note that the complete hash of the password is never sent, only the first 5 chars of the password hash is sent. The API then returns
            all known hashed that begin with those 5 chars. The check if the full hash exists in the returned list is performed locally. For more
            information see the help for the cmdlet Test-PasswordAgainstPwnedPasswordService.
        .PARAMETER Random
            Create a randomly generated password
        .PARAMETER Count
            Defines the number of passwords to generate. This can be used to create batches of passwords. Defaults to 1.
        .PARAMETER Length
            When the parameter set random is used this parameter lets the user select
            how long the password should be. Defaults to 8.
        .PARAMETER Signs
            When the parameter set random is used this parameter lets the user select
            how many signs/symbols that should be included in the password. Defaults to 3.
        .PARAMETER ReturnSecureStringObject
            Return password as secure string
        .PARAMETER AllowInterchangableCharacters
            Defines that characters as i|I and l|L and 0|O can be used in the password, defaults to false
        .PARAMETER Diceware
            Defines that a diceware password should be generated
        .PARAMETER WordCount
            Defines how many words the diceware password should made up of
        .PARAMETER CustomFirstWord
            Optionally define a custom first word of the diceware series.
        .PARAMETER CustomWordSeparator
            Optionally set a custom separator char when generating diceware
        .EXAMPLE
            New-Password -Diceware -WordCount 5 -Count 3
 
            robust.dawn\condense/poet#cost
            parchment\parcel:remedy:ultra'triage
            spelling,mooned*propeller%legwarmer*flagstick
 
        .EXAMPLE
            New-Password -Diceware -WordCount 5 -Count 3 -CustomFirstWord 'gardin'
 
            gardin\throwback"chirping#remake"poem
            gardin#juvenile!putt\jittery.palatable
            gardin'astonish\decaf"imprudent?specimen
 
        .EXAMPLE
            New-Password -Count 3
 
            Wuba9710
            Suve0945
            Zigo1479
 
            This example uses the method to create a password that is easy to remember but less secure.
        .EXAMPLE
            New-Password -Length 10 -Signs 5
 
            ..&La:J%NF
 
            This example creates a password of alphanumrerical characters including five signs/symbols.
        .EXAMPLE
            New-Password -Length 20
 
            J.rp318xVD?Twhah'K7b
 
            This example creates a password of alpanumerical+signs characters
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'No need, non-destructive change')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'FalsePositive, plain text string is generated by the script')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'False positive')]
    [CmdletBinding(DefaultParameterSetName = 'Simple')]
    param(
        # Generic parameters for all
        [Parameter()]
        [int]
        $Count = 1,

        [Parameter()]
        [switch]
        $ReturnSecureStringObject,

        [Parameter()]
        [switch]
        $AllowInterchangableCharacters,

        [Parameter()]
        [switch]
        $SkipCompromisedCheck,

        # Parameters specific to random
        [Parameter(ParameterSetName = 'Random')]
        [switch]
        $Random,

        [Parameter(ParameterSetName = 'Random')]
        [ValidateRange(0, 128)]
        [int]
        $Length = 12,

        [Parameter(ParameterSetName = 'Random')]
        [int]
        $Signs = 3,

        # Parameters specific to diceware
        [Parameter(ParameterSetName = 'diceware')]
        [switch]
        $Diceware,

        [Parameter(ParameterSetName = 'diceware')]
        [ValidateRange(2, 256)]
        [int]
        $WordCount = 4,


        [Parameter(ParameterSetName = 'diceware')]
        [string]
        $CustomFirstWord,

        [Parameter(ParameterSetName = 'diceware')]
        [char]
        $CustomWordSeparator
    )

    function ThrowDice
    {
        $String = ''
        for ($i = 0; $i -lt 5; $i++)
        {
            if ($PSVersionTable.PSEdition -eq 'core')
            {
                $String += ([System.Security.Cryptography.RandomNumberGenerator]::GetInt32(1, 7)).ToString()
            }
            else
            {
                $String += (Get-Random -Minimum 1 -Maximum 7).ToString()
            }
        }
        return ($String -as [int])
    }

    function SelectDiceWord
    {
        param (
            [ref]$WordListHash
        )
        return ($WordListHash.Value[(ThrowDice).ToString()])
    }

    function SelectRandomSign
    {
        param (
            [char[]]$Signs
        )
        if ($PSVersionTable.PSEdition -eq 'core')
        {
            return ($Signs[([System.Security.Cryptography.RandomNumberGenerator]::GetInt32(0, ($Signs.Count)))])
        }
        else
        {
            return ($Signs[(Get-Random -Minimum 0 -Maximum ($Signs.Count))])
        }
    }

    $Arrays = @{
        LettersAndDigits = [char[]]@(48..57 + 65..90 + 97..122)
        Signs            = [char[]]@(33..35 + 37..39 + 42 + 44 + 46..47 + 58..59 + 63..64 + 92)
        Digits           = [char[]]@(48..57)
        UpperConsonants  = [char[]]@('B', 'C', 'D', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'X', 'Z', 'W', 'Y')
        LowerConsonants  = [char[]]@('b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l' , 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'x', 'z', 'w', 'y')
        LowerVowels      = [char[]]@('a', 'e', 'i', 'o', 'u')
    }

    if ($PSCmdlet.ParameterSetName -eq 'diceware')
    {
        $WordListHash = @{}
        Import-Csv (Resolve-Path "$PSScriptRoot\include\eff_large_wordlist.txt") -Delimiter "`t" -Header 'Dice', 'Word' | ForEach-Object {
            $WordListHash.Add($PSItem.Dice, $PSItem.Word)
        }
    }

    if (-not $AllowInterchangableCharacters)
    {
        $InterchangableCharacters = @(
            [char]'l', [char]'I', [char]'O', [char]'o', [char]48
        )
        # Clone keys from hashtable so that the hashtable can be modified during enumeration
        $Arrays.Keys.Clone() | ForEach-Object {
            $CurrentArrayKey = $PSItem
            $Arrays[$CurrentArrayKey] = $Arrays[$CurrentArrayKey] | Where-Object { $InterchangableCharacters -notcontains $PSItem }
        }
    }

    if ($Signs -gt $Length)
    {
        Write-Warning ('Sign characters cannot be greater than the length, setting Signs to the specified length ({0})' -f $Length)
        $Signs = $Length
    }

    for ($i = 0; $i -lt $Count; $i++)
    {
        switch ($PSCmdlet.ParameterSetName)
        {
            'Simple'
            {
                $CharArray = [char[]]@()
                $CharArray += ($Arrays.UpperConsonants | Get-Random -Count 1)
                $CharArray += ($Arrays.LowerVowels | Get-Random -Count 1)
                $CharArray += ($Arrays.LowerConsonants | Get-Random -Count 1)
                $CharArray += ($Arrays.LowerVowels | Get-Random -Count 1)
                $CharArray += ($Arrays.LowerConsonants | Get-Random -Count 1)
                $CharArray += ($Arrays.LowerVowels | Get-Random -Count 1)
                $CharArray += ($Arrays.Digits | Get-Random -Count 5)

                $PasswordString = $CharArray -join ''
            }
            'Random'
            {
                $CharArray = [char[]]@()
                $NumberOfChars = $Signs
                1..$Length | ForEach-Object {
                    $CurrentChar = $_
                    $Decider = Get-Random -Minimum 0 -Maximum 100
                    $CharsLeft = ($Length + 1 - $CurrentChar)
                    $Chance = ($NumberOfChars / $CharsLeft * 100)
                    if ($Decider -lt $Chance -or $CharsLeft -le $NumberOfChars)
                    {
                        $CharArray += ($Arrays.Signs | Get-Random -Count 1)
                        $NumberOfChars--
                    }
                    else
                    {
                        $CharArray += ($Arrays.LettersAndDigits | Get-Random -Count 1)
                    }
                }
                $PasswordString = $CharArray -join ''
            }
            'diceware'
            {
                $PasswordString = ''

                if ($CustomFirstWord)
                {
                    if ($WordListHash.ContainsValue($CustomFirstWord))
                    {
                        Write-Warning -Message 'The chosen first word is already included in the diceware word list, please select another word.'
                    }
                    $PasswordString += $CustomFirstWord
                }
                else
                {
                    $PasswordString += SelectDiceWord -WordListHash ([ref]$WordListHash)
                }

                1..($WordCount - 1) | ForEach-Object {
                    if ($PSBoundParameters.ContainsKey('CustomWordSeparator'))
                    {
                        $PasswordString += $CustomWordSeparator
                    }
                    else
                    {
                        $PasswordString += SelectRandomSign -Signs $Arrays.Signs
                    }
                    $PasswordString += SelectDiceWord -WordListHash ([ref]$WordListHash)
                }

            }
        }

        if (-not $SkipCompromisedCheck)
        {
            $Result = Test-PasswordAgainstPwnedPasswordService -InputObject (ConvertTo-SecureString -String $PasswordString -AsPlainText -Force)
            if ($Result)
            {
                Write-Warning "Generated password [$PasswordString] was excluded because it existed in the pawned database"
                $i--
                continue
            }
        }

        if ($ReturnSecureStringObject)
        {
            ConvertTo-SecureString -String $PasswordString -AsPlainText -Force
        }
        else
        {
            $PasswordString
        }
    }
}
#EndRegion '.\Public\New-Password.ps1' 305
#Region '.\Public\Remove-GitHubArtifact.ps1' -1

function Remove-GitHubArtifact
{
    <#
    .SYNOPSIS
        Cleanup artifacts from GitHub repo
    .DESCRIPTION
        This script will remove all artifacts for a single repos or all repos for a given user
    .PARAMETER RepoName
        Defines a specific repository to remove artifacts for
    .PARAMETER GitHubSecret
        Defines the GitHubSecret (API Key) to use
    .PARAMETER GitHubOrg
        Defines the GitHub owner user name
    .PARAMETER Repo
        Optionally specify a repo to only remove artifacts for that specific repo
    .PARAMETER PageSize
        Optionally specify the PageSize when retreiving repos and artifacts. Valid values are in range of 1..100. Default is 30.
    .LINK
        https://getps.dev/blog/cleanup-github-artifacts
    .EXAMPLE
        Remove-GitHubArtifact -GitHubSecret "ABC" -GitHubOrg "user"
 
        Running this function without specifying a repo will cleanup all artifacts for all repos
    .EXAMPLE
        Remove-GitHubArtifact -GitHubSecret "ABC" -GitHubOrg "user" -Repo "RepoName"
 
        Running the script with a specified repo will cleanup all artifacts for that repo
#>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [parameter(Mandatory)]
        [string]
        $GitHubSecret,

        [parameter(Mandatory)]
        [string]
        $GitHubOrg,

        [parameter()]
        [string]
        $RepoName,

        [parameter()]
        [ValidateRange(1, 100)]
        [int]
        $PageSize = 30
    )

    $PSDefaultParameterValues = @{
        'Invoke-RestMethod:Headers' = @{Accept = 'application/vnd.github+json'; Authorization = "Bearer $GitHubSecret" }
    }

    # Find repos
    if ($RepoName)
    {
        $Repos = Invoke-RestMethod -Method get -Uri "https://api.github.com/repos/$GitHubOrg/$RepoName"
    }
    else
    {
        $Repos = [System.Collections.Generic.List[Object]]::New()
        $PageID = 1
        do
        {
            $Result = Invoke-RestMethod -Method get -Uri "https://api.github.com/user/repos?per_page=$PageSize&page=$PageID"
            if ($Result)
            {
                $Repos.AddRange([array]$Result)
            }
            $PageID++
        } until ($Result.Count -lt $PageSize)
    }

    foreach ($repo in $repos)
    {
        Write-Verbose -Message "Processing repo $($repo.name)"

        # Define result object
        $ObjectHash = [ordered]@{
            Repo              = $Repo.Name
            Artifacts_Found   = 0
            Artifacts_Removed = 0
            Artifacts_SizeMB  = 0
            Artifacts         = [System.Collections.Generic.List[Object]]::New()
        }

        # Find artifacts
        $Artifacts = [System.Collections.Generic.List[Object]]::New()
        $PageID = 1
        do
        {
            $Result = Invoke-RestMethod -Method get -Uri "https://api.github.com/repos/$GitHubOrg/$($Repo.Name)/actions/artifacts?per_page=$PageSize&page=$PageID" | Select-Object -ExpandProperty artifacts
            if ($Result)
            {
                $Artifacts.AddRange([array]$Result)
            }
            $PageID++
        } until ($Result.Count -lt $PageSize)

        # Remove artifacts
        if ($artifacts)
        {
            $ObjectHash.Artifacts_Found = $Artifacts.Count
            $ObjectHash.Artifacts_SizeMB = (($Artifacts | Measure-Object -Sum -Property size_in_bytes).Sum / 1MB)
            foreach ($artifact in $artifacts)
            {
                if ($PSCmdlet.ShouldProcess("Artifact: $($artifact.name) in Repo: $($Repo.Name)", 'DELETE'))
                {
                    $Result = Invoke-RestMethod -Method DELETE -Uri "https://api.github.com/repos/$GitHubOrg/$($Repo.Name)/actions/artifacts/$($artifact.id)"
                    $ObjectHash.Artifact_Removed++
                }
            }
        }

        # Return resultobject
        [pscustomobject]$ObjectHash
    }
}
#EndRegion '.\Public\Remove-GitHubArtifact.ps1' 118
#Region '.\Public\Resolve-IPinSubnet.ps1' -1

function Resolve-IPinSubnet
{
    <#
    .DESCRIPTION
        Checks if a specified IP address is included in the IP range of a specific network.
    .PARAMETER IP
        Defines the IP address to resolve.
    .PARAMETER Network
        Defines the network address to search within
    .PARAMETER MaskLength
        Defines the length of the mask
    .EXAMPLE
        Resolve-IPinSubnet -IP 213.199.154.5 -Network 213.199.154.0 -MaskLength 24
        Checks if the IP 212.199.154.5 is included in the 213.199.154.0/24 network
    #>

    [CmdletBinding()]
    [OutputType([system.boolean])]
    param(
        [Parameter(Mandatory = $true)][string]$IP,
        [Parameter(Mandatory = $true)][string]$Network,
        [Parameter(Mandatory = $true)][int]$MaskLength
    )

    $IPDec = [uint32](ConvertTo-DecimalIP -IPAddress $IP)
    $NetworkDec = [uint32](ConvertTo-DecimalIP -IPAddress $Network)
    $Mask = [uint32](ConvertTo-DecimalIP -IPAddress (ConvertTo-Mask -MaskLength $MaskLength))

    if ($NetworkDec -eq ($Mask -band $IPDec))
    {
        return $true
    }
    else
    {
        return $false
    }
}
#EndRegion '.\Public\Resolve-IPinSubnet.ps1' 37
#Region '.\Public\Set-EnvironmentVariable.ps1' -1

function Set-EnvironmentVariable
{
    <#
    .DESCRIPTION
        Functions that provides a shortcut to create environmental variables
    .PARAMETER Name
        Defines the name of the envioronmental variable
    .PARAMETER Value
        Defines the value of the environmental variable
    .PARAMETER Target
        Defines the target for the environmental variable. Valid values are Machine, User,
        Process. Defaults to Process. This means that the configured environmental variables
        are non-persistant. If persistant environmental variables are desirable user Machine or User.
    .EXAMPLE
        Set-EnvironmentVariable -Name 'ComputerOwner' -Value 'Will Smith' -Target Machine
        This example creates the environment variable computerowner to the machine
        scope and assigns the value 'Will Smith'
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [string]
        [Parameter(Mandatory)]
        $Name,

        [string]
        [AllowEmptyString()]
        [Parameter(Mandatory)]
        $Value,

        [System.EnvironmentVariableTarget]
        [ValidateSet('Machine', 'User', 'Process')]
        $Target = 'Process'
    )

    # Force target to process on Linux and MacOS and warn user.
    if ($Target -ne 'Process' -and ($IsLinux -or $IsMacOS))
    {
        Write-Warning -Message 'It is only supported to set process environment variables on Linux and MacOS, environment varable will be set in Process scope'
        $Target = [System.EnvironmentVariableTarget]::Process
    }

    if ($PSCmdlet.ShouldProcess($Name))
    {
        [Environment]::SetEnvironmentVariable($Name, $Value, $Target)
    }
}
#EndRegion '.\Public\Set-EnvironmentVariable.ps1' 47
#Region '.\Public\Switch-Object.ps1' -1

function Switch-Object {
    <#
    .DESCRIPTION
       Transposes an object, foreach parameter an object is created
    .PARAMETER InputObject
       Defined the object to transpose
    .EXAMPLE
       Get-Process | Select-object -first 1 | Switch-object
       Description
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]$InputObject
    )
    PROCESS {
        $InputObject | ForEach-Object {
            $instance = $_
            $instance.PSObject.Properties.Name | ForEach-Object {
                [PSCustomObject]@{
                    Name  = $_
                    Value = $instance.$_
                }
            }
        }
    }
}
#EndRegion '.\Public\Switch-Object.ps1' 27
#Region '.\Public\Test-AllHashKeysAreTrue.ps1' -1

function Test-AllHashKeysAreTrue {
    <#
    .DESCRIPTION
        This functions checks that all values of a hashtable evaluates to true. For values not of type boolean, a typecast to bool is performed.
    .PARAMETER HashTable
        Defines the hashtable object to test
    .EXAMPLE
        Validate-AllHashKeysAreTrue
        Description of example
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)][hashtable]$HashTable
    )

    PROCESS {
        $AllTrue = $true
        foreach ($Key in $HashTable.Keys) {
            if ($HashTable.$Key -as [boolean] -eq $false) {
                $AllTrue = $false
                break
            }
        }
        Write-Output $AllTrue
    }
}
#EndRegion '.\Public\Test-AllHashKeysAreTrue.ps1' 28
#Region '.\Public\Test-Office365IPURL.ps1' -1

function Test-Office365IPURL
{
    <#
      .DESCRIPTION
      Retreive a list of ip and urls required for communication to and from Office 365.
 
      .PARAMETER IP
      Defines the IP to search for with in the scopes of rules returned from Office 365.
 
      .PARAMETER Office365IPURL
      Defines the URL to the Office 365 IP URL Endpoint. Defaults to 'https://endpoints.office.com/endpoints/worldwide?clientrequestid=b10c5ed1-bad1-445f-b386-b919946339a7'.
      Provided as parameter to allow queries to other environments than worldwide as well as keep agility if Microsoft would change URL.
 
      .EXAMPLE
      Get-Office365IPURL -Services Exchange,Skype -OnlyRequired -Types IP4,URL -Outputformat JSON
 
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string[]]
        $IP,

        [Parameter()]
        [string]
        $Office365IPURL = 'https://endpoints.office.com/endpoints/worldwide?clientrequestid=b10c5ed1-bad1-445f-b386-b919946339a7'

    )

    $ErrorActionPreference = 'Stop'

    # Get latest IP URL info
    $Office365Endpoints = Invoke-RestMethod -Uri $Office365IPURL -Method Get

    # Import net module
    Import-Module indented.net.ip

    # Foreach service
    foreach ($item in $IP)
    {
        # Foreach rule in service
        foreach ($rule in $Office365Endpoints)
        {
            # Select Ipv4 ips
            $IPv4Ranges = $rule.ips.where({ $_ -notlike '*:*' })

            # Resolve IPs for URLs. There are two shortcomings of this part. First; Only the currently returned IPs are evaluated. In case other
            # records are returned due to GeoDNS, round robin etc those will not be known and therefor not evaluated. Second; URLs with wildcards are
            # not evalutated, there is no way for the script to know which URLs within the wildcard scope that will be called by services.
            $rule.urls | ForEach-Object {
                if ($_)
                {
                    Resolve-DnsName $_ -ErrorAction SilentlyContinue | Where-Object { $_.GetType().Name -eq 'DnsRecord_A' } | ForEach-Object {
                        $IPv4Ranges += $_.IPAddress
                    }
                }
            }

            # Test each entry in the array if the IP is equal or belongs to the returned IP/range
            foreach ($range in $IPv4Ranges)
            {
                [pscustomobject]@{
                    RuleID      = $rule.id
                    ServiceArea = $rule.ServiceArea
                    TCPPort     = $rule.tcpPorts
                    UDPPort     = $rule.udpPorts
                    Required    = $rule.Required
                    Range       = $range
                    Subject     = $item
                    IsMember    = (Test-SubnetMember -SubjectIPAddress $item -ObjectIPAddress $range)
                }
            }
        }
    }
}
#EndRegion '.\Public\Test-Office365IPURL.ps1' 77
#Region '.\Public\Test-PasswordAgainstPwnedPasswordService.ps1' -1

function Test-PasswordAgainstPwnedPasswordService
{
    <#
    .DESCRIPTION
        Return true if provided password is compromised
    .PARAMETER InputObject
        Defines the password to check
    .EXAMPLE
        Test-PasswordAgainstPwnedPasswordService
        Description of example
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingBrokenHashAlgorithms', '', Justification = 'API requires SHA1 hashes')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Security.SecureString]
        $InputObject
    )

    PROCESS
    {
        $hash = Get-StringHash -Strings (Convert-Object -FromSecureStringObject $InputObject -Property String) -Algorithm SHA1 -Iterations 0
        $First5HashChars = $hash.hash.ToString().SubString(0, 5)
        $RemainingHashChars = $hash.hash.ToString().SubString(5)

        $url = "https://api.pwnedpasswords.com/range/$First5HashChars"
        [Net.ServicePointManager]::SecurityProtocol = 'Tls12'
        $response = Invoke-RestMethod -Uri $url -UseBasicParsing
        $lines = $response -split '\n'
        $filteredLines = $lines -like "$remainingHashChars*"

        return ([boolean]([int]($filteredLines -split ':')[-1]))
    }
}
#EndRegion '.\Public\Test-PasswordAgainstPwnedPasswordService.ps1' 35
#Region '.\Public\Test-PSGalleryNameAvailability.ps1' -1

function Test-PSGalleryNameAvailability
{
    <#
        .DESCRIPTION
        Checks if the specified PackageName is already taken in PSGallery
 
        .PARAMETER PackageName
        Defines the package name to search for
 
        .EXAMPLE
        Test-PSGalleryNameAvailability -PackageName PowershellGet
    #>

    [CmdletBinding()]
    [OutputType([boolean])]
    param(
        [Parameter(Mandatory)]
        [string]
        $PackageName
    )

    $Response = Invoke-WebRequest -Uri "https://www.powershellgallery.com/packages/$PackageName" -SkipHttpErrorCheck
    if ($Response.RawContent -like '*Page not found*')
    {
        return $true
    }
    else
    {
        return $false
    }

}
#EndRegion '.\Public\Test-PSGalleryNameAvailability.ps1' 32
#Region '.\Public\Test-RebootPending.ps1' -1

function Test-RebootPending
{
    <#
    .DESCRIPTION
        Queries the registry for the rebootpending key and returns the status
    .PARAMETER Name
        Description
    .EXAMPLE
        Test-RebootPending
        Description of example
    #>


    [CmdletBinding()]
    param(
    )

    PROCESS
    {
        $rebootRequired = Test-Path -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending'
        return $rebootRequired
    }
}
#EndRegion '.\Public\Test-RebootPending.ps1' 23
#Region '.\suffix.ps1' -1

# The content of this file will be appended to the top of the psm1 module file. This is useful for custom procesedures after all module functions are loaded.
#EndRegion '.\suffix.ps1' 2