AutomateNOW.psm1
$InformationPreference = 'Continue' #Region - Authentication Function Confirm-AutomateNOWSession { <# .SYNOPSIS Confirms that the local session variable created by Connect-AutomateNOW is still apparently valid. .DESCRIPTION The `Confirm-AutomateNOWSession` function confirms that the local session variable created by Connect-AutomateNOW is still apparently valid (not expired yet). This function does not make any network connections. It is only reviewing and advising on the currently stored session variable. .PARAMETER Quiet Switch parameter to silence the extraneous output that this outputs by default .PARAMETER IgnoreEmptyDomain Switch parameter to ignore the lack of configured domain in the session header. This was intended for development purposes and is likely to be removed in the future. .PARAMETER MaximumTokenRefreshAge int32 parameter to specify the minimum age (in seconds) of the refresh token before updating it occurs automatically. .INPUTS None. You cannot pipe objects to Confirm-AutomateNOWSession (yet). .OUTPUTS Returns a boolean $True if the local session variable appears to be valid (not expired yet) .EXAMPLE Confirm-AutomateNOWSession -Quiet .NOTES You must use Connect-AutomateNOW to establish the token before you can confirm it #> [OutputType([boolean])] [Cmdletbinding()] Param( [Parameter(Mandatory = $false)] [switch]$Quiet, [Parameter(Mandatory = $false)] [switch]$IgnoreEmptyDomain, [Parameter(Mandatory = $false)] [switch]$DoNotRefresh, [Parameter(Mandatory = $false)] [int32]$MaximumTokenRefreshAge = 3300 ) If ($anow_header.values.count -eq 0) { Write-Warning -Message "Please use Connect-AutomateNOW to establish your access token." Break } ElseIf ($anow_header.Authorization -notmatch '^Bearer [a-zA-Z-_/=:,."0-9]{1,}$') { [string]$malformed_token = $anow_header.values Write-Warning -Message "Somehow the access token is not in the expected format. Please contact the author with this apparently malformed token: [$malformed_token]" Break } ElseIf ($anow_session.ExpirationDate -isnot [datetime]) { Write-Warning -Message 'Somehow there is no expiration date available. Please use Connect-AutomateNOW to establish your session properties.' Break } ElseIf ($anow_session.RefreshToken -notmatch '^[a-zA-Z-_/=:,."0-9]{1,}$' -and $anow_session.RefreshToken.Length -gt 0) { [string]$malformed_refresh_token = $anow_session.RefreshToken Write-Warning -Message "Somehow the refresh token does not appear to be valid. Please contact the author about this apparently malformed token: [$malformed_refresh_token]" Break } If ($null -eq (Get-Command -Name Invoke-AutomateNOWAPI -EA 0)) { Write-Warning -Message 'Somehow the Invoke-AutomateNOWAPI function is not available in this session. Did you install -and- import the module?' Break } [datetime]$current_date = Get-Date [datetime]$ExpirationDate = $anow_session.ExpirationDate [string]$ExpirationDateDisplay = Get-Date -Date $ExpirationDate -Format 'yyyy-MM-dd HH:mm:ss' [timespan]$TimeRemaining = ($ExpirationDate - $current_date) [int32]$SecondsRemaining = $TimeRemaining.TotalSeconds If ($SecondsRemaining -lt 0) { Write-Warning -Message "This token expired [$SecondsRemaining] seconds ago at [$ExpirationDateDisplay]. Kindly refresh your token by using Connect-AutomateNOW." Break } ElseIf ($SecondsRemaining -lt $MaximumTokenRefreshAge -and $DoNotRefesh -ne $true) { [int32]$minutes_elapsed = ($TimeRemaining.TotalMinutes) Write-Verbose -Message "This token will expire in [$minutes_elapsed] minutes. Refreshing your token automatically. Use -DoNotRefresh with Connect-AutomateNOW to stop this behavior." Update-AutomateNOWToken } Else { Write-Verbose -Message "Debug: This token still has [$SecondsRemaining] seconds remaining" } If ($anow_header.domain.Length -eq 0 -and $IgnoreEmptyDomain -ne $true) { Write-Warning -Message 'You somehow do have not have a domain selected. You can try re-connecting again with Connect-AutomateNOW and the -Domain parameter or use Switch-Domain please.' Break } Return $true } Function Connect-AutomateNOW { <# .SYNOPSIS Connects to the API of an AutomateNOW! instance .DESCRIPTION The `Connect-AutomateNow` function authenticates to the API of an AutomateNOW! instance. It then sets the access token globally. .PARAMETER User Specifies the user connecting to the API .PARAMETER Pass Specifies the password of the user connecting to the API .PARAMETER Instance Specifies the name of the AutomateNOW! instance. For example: s2.infinitedata.com .PARAMETER Domain Optional string to set the AutomateNOW domain manually .PARAMETER NotSecure Switch parameter to accomodate instances that use the http protocol (typically on port 8080) .PARAMETER Quiet Switch parameter to silence the extraneous output that this outputs by default .PARAMETER Key Optional 16-byte array for when InfiniteDATA has changed their encryption key .INPUTS None. You cannot pipe objects to Connect-AutomateNOW (yet). .OUTPUTS There is no direct output. Rather, a global variable $anow_header with the bearer access token is set in the current powershell session. .EXAMPLE Connect-AutomateNOW -User 'user.10' -Pass 'MyCoolPassword!' -Instance 's2.infinitedata.com' .NOTES More features will be added in the future if this turns out to be useful #> [OutputType([string])] [CmdletBinding(DefaultParameterSetName = 'Default')] Param( [Parameter(Mandatory = $true, ParameterSetName = 'Default')] [Parameter(Mandatory = $true, ParameterSetName = 'DirectCredential')] [string]$Instance, [Parameter(Mandatory = $true, ParameterSetName = 'DirectCredential')] [string]$User, [Parameter(Mandatory = $true, ParameterSetName = 'DirectCredential')] [string]$Pass, [Parameter(Mandatory = $false, ParameterSetName = 'Default')] [Parameter(Mandatory = $false, ParameterSetName = 'DirectCredential')] [string]$Domain, [Parameter(Mandatory = $false, ParameterSetName = 'Default')] [Parameter(Mandatory = $false, ParameterSetName = 'DirectCredential')] [switch]$NotSecure, [Parameter(Mandatory = $false, ParameterSetName = 'Default')] [Parameter(Mandatory = $false, ParameterSetName = 'DirectCredential')] [switch]$Quiet, [Parameter(Mandatory = $false, ParameterSetName = 'Default')] [Parameter(Mandatory = $false, ParameterSetName = 'DirectCredential')] [byte[]]$Key = @(7, 22, 15, 11, 1, 24, 8, 13, 16, 10, 5, 17, 12, 19, 27, 9) ) Function New-ANowAuthenticationPayload { [OutputType([string])] [CmdletBinding()] Param( [Parameter(Mandatory = $true)] [string]$User, [Parameter(Mandatory = $true)] [string]$Pass, [Parameter(Mandatory = $false)] [boolean]$SuperUser = $false, [Parameter(Mandatory = $false)] [byte[]]$Key = @(7, 22, 15, 11, 1, 24, 8, 13, 16, 10, 5, 17, 12, 19, 27, 9) ) [byte[]]$passwd_array = [System.Text.Encoding]::UTF8.GetBytes($pass) [byte[]]$encrytped_array = For ($i = 0; $i -lt ($passwd_array.Length); $i++) { [byte]$current_byte = $passwd_array[$i] [int32]$first = (-bnot $current_byte -shr 0) -band 0x0f [int32]$second = (-bnot $current_byte -shr 4) -band 0x0f $Key[$first] $Key[$second] } [string]$encrypted_string = [System.Convert]::ToBase64String($encrytped_array) [hashtable]$payload = @{} $payload.Add('j_username', $user) $payload.Add('j_password', "ENCRYPTED::$encrypted_string") $payload.Add('superuser', $superuser) [string]$payload_json = $payload | ConvertTo-Json -Compress Write-Verbose -Message "Sending payload $payload_json" Return $payload_json } Function New-ANOWAuthenticationProperties { [OutputType([hashtable])] [CmdletBinding()] Param( [Parameter(Mandatory = $true)] [string]$User, [Parameter(Mandatory = $true)] [string]$Pass ) [string]$body = New-ANOWAuthenticationPayload -User $User -Pass $Pass If ($NotSecure -eq $true) { [string]$protocol = 'http' } Else { [string]$protocol = 'https' } [string]$login_url = ($protocol + '://' + $instance + '/automatenow/api/login/authenticate') [hashtable]$parameters = @{} [int32]$ps_version_major = $PSVersionTable.PSVersion.Major If ($ps_version_major -eq 5) { # The below C# code provides the equivalent of the -SkipCertificateCheck parameter for Windows PowerShell 5.1 Invoke-WebRequest If (($null -eq ("TrustAllCertsPolicy" -as [type])) -and ($protocol -eq 'http')) { [string]$certificate_policy = @" using System.Net; using System.Security.Cryptography.X509Certificates; public class TrustAllCertsPolicy : ICertificatePolicy { public bool CheckValidationResult( ServicePoint srvPoint, X509Certificate certificate, WebRequest request, int certificateProblem) { return true; } } "@ $Error.Clear() Try { Add-Type -TypeDefinition $certificate_policy } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Add-Type failed due to [$Message]" Break } $Error.Clear() Try { [System.Net.ServicePointManager]::CertificatePolicy = New-Object -TypeName TrustAllCertsPolicy } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "New-Object failed to create a new 'TrustAllCertsPolicy' CertificatePolicy object due to [$Message]." Break } } $parameters.Add('UseBasicParsing', $true) } ElseIf ( $ps_version_major -gt 5) { $parameters.Add('SkipCertificateCheck', $true) } Else { Write-Warning -Message "Please use either Windows PowerShell 5.1 or PowerShell Core." Break } $parameters.Add('Uri', $login_url) $parameters.Add('Method', 'POST') $parameters.Add('Body', $body) $parameters.Add('ContentType', 'application/json') $Error.Clear() Try { [Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject]$results = Invoke-WebRequest @parameters } Catch { [string]$Message = $_.Exception.Message If ($Message -match '(The underlying connection was closed|The SSL connection could not be established)') { Write-Warning -Message 'Please try again with the -NotSecure parameter if you are connecting to an insecure instance.' Break } ElseIf ($Message -match '(Response status code does not indicate success:|The remote server returned an error)') { $Error.Clear() Try { [int32]$return_code = $Message -split 'success: ' -split ' ' -replace '\(' -replace '\)' | Select-Object -Last 1 -Skip 1 # note: the replacing of the left and right parenthesis is only needed for Windows PowerShell } Catch { [string]$Message2 = $_.Exception.Message Write-Warning -Message "Unable to extract the error code from [$Message] due to [$Message2]" } Write-Verbose "Received status code $return_code instead of 200!" [string]$ReturnCodeWarning = Switch ($return_code) { 401 { "You received HTTP Code $return_code (Unauthorized). DID YOU MAYBE ENTER THE WRONG PASSWORD? :-)" } 403 { "You received HTTP Code $return_code (Forbidden). DO YOU MAYBE NOT HAVE PERMISSION TO THIS? [$command]" } 404 { "You received HTTP Code $return_code (Page Not Found). ARE YOU SURE THIS ENDPOINT REALLY EXISTS? [$command]" } Default { "You received HTTP Code $return_code instead of '200 OK'. Apparently, something is wrong..." } } Write-Warning -Message $ReturnCodeWarning } Else { Write-Warning -Message "Invoke-WebRequest failed due to [$Message]" } Break } [string]$content = $Results.Content If ($content -notmatch '^{"token_type":"Bearer","access_token":"[a-zA-Z-_:,."0-9]{1,}"}$') { [string]$content = "The returned content does not contain a bearer token. Please check the credential you are using." } Write-Verbose -Message "`r`nToken properties: $content`r`n" $Error.Clear() Try { [PSCustomObject]$token_properties = $content | ConvertFrom-Json } Catch { [string]$Message = $_.Exception.Message Write-Warning "ConvertFrom-Json or Select-Object failed due to [$Message]." Break } Return $token_properties } If ($null -ne $anow_header) { $Error.Clear() Try { Remove-Variable -Name anow_header -Scope Global -Force } Catch { [string]$Message = $_.Exception.Message Write-Warning "Remove-Variable failed to remove the `$anow_header variable due to [$Message]." Break } } If ($null -ne $anow_session) { $Error.Clear() Try { Remove-Variable -Name anow_session -Scope Global -Force } Catch { [string]$Message = $_.Exception.Message Write-Warning "Remove-Variable failed to remove the `$anow_session variable due to [$Message]." Break } } If ($User.Length -eq 0) { $Error.Clear() Try { [string]$User = Read-Host -Prompt 'Please enter username (e.g. username, domain\username, userame@domain.com)' } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Read-Host failed to receive the current username due to [$Message]." Break } } If ($Pass.Length -eq 0) { If ($ps_version_major -gt 5) { $Error.Clear() Try { [string]$Pass = Read-Host -Prompt 'Please enter the password (e.g. ********)' -MaskInput } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Read-Host failed to receive the current password on PowerShell Core due to [$Message]." Break } } Else { $Error.Clear() Try { [securestring]$SecurePass = Read-Host -Prompt 'Please enter the password (e.g. ********)' -AsSecureString } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Read-Host failed to receive the current password on Windows PowerShell due to [$Message]." Break } $Error.Clear() Try { [string]$Pass = [System.Net.NetworkCredential]::new("", $SecurePass).Password } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "The 'new' constructor of the System.Net.NetworkCredential class failed to convert a secure string to plain text due to [$Message]." Break } } } $Error.Clear() Try { [PSCustomObject]$token_properties = New-ANOWAuthenticationProperties -User $User -Pass $Pass } Catch { [string]$Message = $_.Exception.Message Write-Warning "New-ANOWAuthenticationProperties failed due to [$Message]." Break } If ( $token_properties.expirationDate -isnot [int64]) { Write-Warning -Message "How is it that the expiration date value is not a 64-bit integer? Something must be wrong. Are we in a time machine?" Break } [string]$access_token = $token_properties.access_token [string]$refresh_token = $token_properties.refresh_token [int32]$expires_in = $token_properties.expires_in [hashtable]$authorization_header = @{'Authorization' = "Bearer $access_token"; 'domain' = '' } $Error.Clear() Try { New-Variable -Name 'anow_header' -Scope Global -Value $authorization_header } Catch { [string]$Message = $_.Exception.Message Write-Warning "New-Variable failed due to create the header globally due to [$Message]." Break } Write-Verbose -Message 'Global variable $anow_header has been set. Use this as your authentication header.' $Error.Clear() Try { [System.TimeZoneInfo]$timezone = Get-TimeZone } Catch { [string]$Message = $_.Exception.Message Write-Warning "Get-TimeZone failed due to get the time zone due to [$Message]." Break } [System.TimeSpan]$utc_offset = $timezone.BaseUtcOffset [System.TimeSpan]$expiration_offset = New-TimeSpan -Seconds $expires_in $Error.Clear() Try { [datetime]$expiration_date_utc = (Get-Date -Date '1970-01-01').AddMilliseconds($token_properties.expirationDate) } Catch { [string]$Message = $_.Exception.Message Write-Warning "Get-Date failed due to process the authentication properties due to [$Message]" Break } [datetime]$expiration_date = ($expiration_date_utc + $utc_offset + $expiration_offset) # We're adding 3 values here for the final expiration date. The current time in UTC, the current machine's UTC offset and the duration of the session (typically 3600 seconds) [hashtable]$anow_session = @{} $anow_session.Add('User', $User) $anow_session.Add('Instance', $Instance) If ($NotSecure -eq $true) { $anow_session.Add('NotSecure', $True) } $anow_session.Add('ExpirationDate', $expiration_date) $anow_session.Add('AccessToken', $access_token) $anow_session.Add('RefreshToken', $refresh_token) If ($Domain.Length -gt 0) { $anow_session.Add('current_domain', $Domain) } $Error.Clear() Try { [PSCustomObject]$userInfo = Get-AutomateNOWUser } Catch { [string]$Message = $_.Exception.Message Write-Warning "Get-AutomateNOWUser failed to get the currently logged in user info due to [$Message]." Break } If ($userInfo.domains.length -eq 0) { Write-Warning "Somehow the user info object is malformed." Break } [array]$domains = $userInfo.domains -split ',' [int32]$domain_count = $domains.Count Write-Verbose -Message "Detected $domain_count domains" If ($domain_count -eq 0) { Write-Warning "Somehow the count of domains is zero." Break } ElseIf ($domain_count -eq 1) { If ($Domain.Length -eq 0) { [string]$Domain = $domains If ($null -ne $anow_header.Domain) { $anow_header.Remove('domain') } If ($null -ne $anow_session.current_domain) { $anow_session.Remove('current_domain') } If ($null -ne $anow_session.domain) { $anow_session.Remove('domain') } If ($null -ne $anow_session.domains) { $anow_session.Remove('domains') } $anow_header.Add('domain', $Domain) $anow_session.Add('current_domain', $Domain) $anow_session.Add('domains', @($Domain)) Write-Verbose -Message "Automatically choosing the [$Domain] domain as it is the only one available." } ElseIf ($userInfo.domains -ne $Domain) { Write-Warning -Message "The domain you chose with -Domain is not the same as the one on [$instance]. Are you sure you entered the domain correctly?" Break } } Elseif ($domain_count -gt 1 -and $Domain.Length -gt 0) { If ($domains -notcontains $Domain) { Write-Warning -Message "The domain you chose with -Domain is not available on [$instance]. Are you sure you entered the domain correctly?" Break } } Else { [string]$domains_display = $domains -join ', ' $anow_session.Add('domains', $domains) Write-Information -MessageData "Please use Switch-AutomateNOWDomain to choose from one of these $domain_count domains [$domains_display]" } $Error.Clear() Try { New-Variable -Name 'anow_session' -Scope Global -Value $anow_session } Catch { [string]$Message = $_.Exception.Message Write-Warning "New-Variable failed due to create the session properties object due to [$Message]" Break } Write-Verbose -Message 'Global variable $anow_session has been set. Use this for other session properties.' If ($Quiet -ne $true) { Return [PSCustomObject]@{ instance = $instance; token_expires = $expiration_date; domain = $Domain; } } } Function Disconnect-AutomateNOW { <# .SYNOPSIS Disconnects from the API of an AutomateNOW! instance .DESCRIPTION The `Disconnect-AutomateNOW` function logs out of the API of an AutomateNOW! instance. It then removes the global session variable object. .INPUTS None. You cannot pipe objects to Disconnect-AutomateNOW. .OUTPUTS A string indicating the results of the disconnection attempt. .EXAMPLE Disconnect-AutomateNOW .NOTES You should do this whenever you are finished with your session. This prevents your token (a.k.a. cookie) from being stolen. #> [CmdletBinding()] Param( ) If ((Confirm-AutomateNOWSession -Quiet) -ne $true) { Write-Warning -Message "Somehow there is not a valid token confirmed." Break } [string]$command = '/logoutEvent' [hashtable]$parameters = @{} $parameters.Add('Command', $command) $parameters.Add('Method', 'POST') If ($anow_session.NotSecure -eq $true) { $parameters.Add('NotSecure', $true) } $Error.Clear() Try { [PSCustomObject]$results = Invoke-AutomateNOWAPI @parameters } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Invoke-AutomateNOWAPI failed due to execute [$command] due to [$Message]." Break } [PSCustomObject]$response = $results.response If ($response.status -eq 0) { [string]$Instance = $anow_session.Instance $Error.Clear() Try { Remove-Variable -Name anow_header -Scope Global -Force } Catch { [string]$Message = $_.Exception.Message Write-Warning "Remove-Variable failed to remove the `$anow_header variable due to [$Message]." Break } $Error.Clear() Try { Remove-Variable -Name anow_session -Scope Global -Force } Catch { [string]$Message = $_.Exception.Message Write-Warning "Remove-Variable failed to remove the `$anow_session variable due to [$Message]." Break } Write-Information -MessageData "Successfully disconnected from [$Instance]." } } Function Set-AutomateNOWPassword { <# .SYNOPSIS Sets the password of the authenticated user of an AutomateNOW! instance .DESCRIPTION The `Set-AutomateNOWPassword` sets the password of the authenticated user of an AutomateNOW! instance .PARAMETER OldPasswd String representing the current password of the authenticated user (use -Secure for masked input) .PARAMETER NewPasswd String representing the new password of the authenticated user (use -Secure for masked input) .PARAMETER Secure Prompts for current and new passwords using Read-Host with the -MaskInput parameter to hide the input .INPUTS None. You cannot pipe objects to Set-AutomateNOWPassword. .OUTPUTS None except for confirmation from Write-Information .EXAMPLE Set-AutomateNOWPassword -OldPasswd 'MyCoolPassword1!' -NewPasswd 'MyCoolPassword4#' Set-AutomateNOWPassword -Secure .NOTES You must use Connect-AutomateNOW to establish the token by way of global variable. #> [Cmdletbinding(DefaultParameterSetName = 'PlainText')] Param( [Parameter(Mandatory = $true, ParameterSetName = 'PlainText')] [string]$OldPasswd, [Parameter(Mandatory = $true, ParameterSetName = 'PlainText')] [string]$NewPasswd, [Parameter(Mandatory = $true, ParameterSetName = 'Secure')] [switch]$Secure ) #[string]$regex_passwd_reqs = '^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?#&])[A-Za-z\d@$!%*?#&]{8,}$' # Note: this needs to be confirmed If ($Secure -eq $true) { $Error.Clear() Try { [string]$OldPasswd = Read-Host -Prompt 'Enter current password' -MaskInput } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Read-Host failed to receive the current password due to [$Message]." Break } If ($OldPasswd.Length -eq 0) { Write-Warning -Message "You must provide the current password. Please try again." Break } $Error.Clear() Try { [string]$NewPasswd = Read-Host -Prompt 'Enter new password' -MaskInput } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Read-Host failed to receive the new password due to [$Message]." Break } If ($NewPasswd.Length -eq 0) { Write-Warning -Message "You must provide the new password. Please try again." Break } } [string]$regex_passwd_reqs = '.{8,}' If ($OldPasswd -notmatch $regex_passwd_reqs) { Write-Warning -Message "Somehow the current password does not meet complexity requirements (minimum 8 chars, 1 upper, 1 lower, 1 number, 1 special character). Please check the password that you supplied here." Break } If ($NewPasswd -notmatch $regex_passwd_reqs) { Write-Warning -Message "Somehow the new password did not meet complexity requirements (minimum 8 chars, 1 upper, 1 lower, 1 number, 1 special character). Please check the password that you supplied here." Break } If ((Confirm-AutomateNOWSession -Quiet) -ne $true) { Write-Warning -Message "Somehow there is not a valid token confirmed." Break } [string]$User = ($anow_session.User) Try { [string]$OldPasswordEncoded = [System.Net.WebUtility]::UrlEncode($OldPasswd) [string]$NewPasswordEncoded = [System.Net.WebUtility]::UrlEncode($NewPasswd) } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Somehow the UrlEncode method of the [System.Net.WebUtility] class failed due to [$Message]" Break } [string]$Body = ('id=' + $User + '&oldPassword=' + $OldPasswordEncoded + '&newPassword=' + $NewPasswordEncoded + '&repeatPassword=' + $NewPasswordEncoded) [string]$command = '/secUser/updatePassword' [hashtable]$parameters = @{} $parameters.Add('ContentType', 'application/x-www-form-urlencoded; charset=UTF-8') $parameters.Add('Command', $command) $parameters.Add('Method', 'POST') $parameters.Add('Body', $Body) If ($anow_session.NotSecure -eq $true) { $parameters.Add('NotSecure', $true) } [string]$parameters_display = $parameters | ConvertTo-Json -Compress Write-Verbose -Message $parameters_display $Error.Clear() Try { [PSCustomObject]$results = Invoke-AutomateNOWAPI @parameters } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Invoke-AutomateNOWAPI failed to execute [$command] due to [$Message]." Break } If ($results.response.status -eq 0) { Write-Information -MessageData "Password successfully changed for $User" [string]$response_display = $results.response | ConvertTo-Json Write-Verbose -Message $response_display } ElseIf ($null -eq $results.response.status) { Write-Warning -Message "Somehow there was no response data. Please look into this." Break } Else { [string]$response_display = $results.response | ConvertTo-Json Write-Warning -Message "The attempt to change the password failed. Please see the returned data: $response_display" Break } } Function Update-AutomateNOWToken { <# .SYNOPSIS Updates the session token used to connect to an instance of AutomateNOW! .DESCRIPTION The `Update-AutomateNOWToken` function updates the existing session token that is being used to connect to an instance of AutomateNOW! .INPUTS None. You cannot pipe objects to Update-AutomateNOWToken (yet). .OUTPUTS .EXAMPLE Invoke-AutomateNOWAPI -command '/secUser/getUserInfo' -method GET .NOTES You must use Connect-AutomateNOW to establish the token by way of global variable. This function has no parameters. It assumes you already have a global session variable ($anow_session) #> If ($anow_session.RefreshToken.Length -eq 0) { Write-Warning -Message "Somehow there is no refresh token." Break } [string]$command = '/oauth/access_token' [string]$ContentType = 'application/x-www-form-urlencoded; charset=UTF-8' [string]$RefreshToken = $anow_session.RefreshToken [string]$Body = 'grant_type=refresh_token&refresh_token=' + $RefreshToken [hashtable]$parameters = @{} $parameters.Add('Method','POST') $parameters.Add('Command',$command) $parameters.Add('ContentType', $ContentType) $parameters.Add('NotAPICommand', $true) $parameters.Add('Body', $Body) If (($anow_session.NotSecure) -eq $true) { $parameters.Add('NotSecure', $true) } $Error.Clear() Try { [PSCustomObject]$token_properties = Invoke-AutomateNOWAPI @parameters } Catch { [string]$Message = $_.Exception.Message Write-Warning "Invoke-AutomateNOWAPI failed to access the [$command] endpoint due to [$Message]." Break } [string]$access_token = $token_properties.access_token [string]$refresh_token = $token_properties.refresh_token [int32]$expires_in = $token_properties.expires_in $Error.Clear() Try { [System.TimeZoneInfo]$timezone = Get-TimeZone } Catch { [string]$Message = $_.Exception.Message Write-Warning "Get-TimeZone failed due to get the time zone due to [$Message]." Break } [System.TimeSpan]$utc_offset = $timezone.BaseUtcOffset [System.TimeSpan]$expiration_offset = New-TimeSpan -Seconds $expires_in $Error.Clear() Try { [datetime]$expiration_date_utc = (Get-Date -Date '1970-01-01').AddMilliseconds($token_properties.expirationDate) } Catch { [string]$Message = $_.Exception.Message Write-Warning "Get-Date failed due to process the authentication properties due to [$Message]" Break } [datetime]$expiration_date = ($expiration_date_utc + $utc_offset + $expiration_offset) # We're adding 3 values here for the final expiration date. The current time in UTC, the current machine's UTC offset and the duration of the session (typically 3600 seconds) $anow_session.'ExpirationDate' = $expiration_date $anow_session.'AccessToken' = $access_token $anow_session.'RefreshToken' = $refresh_token [string]$expiration_date_display = Get-Date -Date $expiration_date -Format 'yyyy-MM-dd HH:mm:ss' $anow_header.'Authorization' = "Bearer $access_token" Write-Verbose -Message 'Global variable $anow_header has been set. Use this as your authentication header.' Write-Information -MessageData "Your token has been refreshed. The new expiration date is [$expiration_date_display]" } #endregion #Region - API Function Invoke-AutomateNOWAPI { <# .SYNOPSIS Invokes the API of an AutomateNOW instance .DESCRIPTION The `Invoke-AutomateNOWAPI` cmdlet sends API commands (in the form of HTTPS requests) to an instance of AutomateNOW. It returns the results in either JSON or PSCustomObject. .PARAMETER Command Specifies the command to invoke with the API call. The value must begin with a forward slash. For example: /secUser/getUserInfo .PARAMETER Method Specifies the method to use with the API call. Valid values are GET and POST. .PARAMETER NotSecure Switch parameter to accomodate instances using the http protocol. Only use this if the instance is on http and not https. .PARAMETER Body Specifies the body object. The format will depend on what you have for content type. Usually, this is a string or a hashtable. .PARAMETER ContentType Specifies the content type of the body (only needed if a body is included) .PARAMETER Instance Specifies the name of the AutomateNOW instance. For example: s2.infinitedata.com .PARAMETER JustGiveMeJSON Switch parameter to return the results in a JSON string instead of a PSCustomObject .PARAMETER NotAPICommand Rarely used switch parameter that removes the '/api' portion of the API URL. Note: This parameter is slated for removal .INPUTS None. You cannot pipe objects to Invoke-AutomateNOWAPI (yet). .OUTPUTS PSCustomObject or String is returned .EXAMPLE Invoke-AutomateNOWAPI -command '/secUser/getUserInfo' -method GET .NOTES You must use Connect-AutomateNOW to establish the token by way of global variable. #> [CmdletBinding()] Param( [Parameter(Mandatory = $true)] [string]$Command, [Parameter(Mandatory = $true)] [ValidateSet('GET', 'POST')] [string]$Method, [Parameter(Mandatory = $false)] [switch]$NotSecure = $false, [Parameter(Mandatory = $false)] [string]$Body, [Parameter(Mandatory = $false)] [string]$ContentType = 'application/json', [Parameter(Mandatory = $false)] [string]$Instance, [Parameter(Mandatory = $false)] [switch]$JustGiveMeJSON, [Parameter(Mandatory = $false)] [switch]$NotAPICommand = $false ) If ($anow_header.values.count -eq 0 -or $anow_session.Instance.Length -eq 0) { Write-Warning -Message "Please use Connect-AutomateNOW to establish your access token." Break } ElseIf ($anow_header.Authorization -notmatch '^Bearer [a-zA-Z-_:,."0-9]{1,}$') { [string]$malformed_token = $anow_header.values Write-Warning -Message "Somehow the access token is not in the expected format. Please contact the author with this apparently malformed token: $malformed_token" Break } ElseIf ($command -notmatch '^/.{1,}') { Write-Warning -Message "Please prefix the command with a forward slash (for example: /secUser/getUserInfo)." Break } If ($Instance.Length -eq 0) { [string]$Instance = $anow_session.Instance } [hashtable]$parameters = @{} If ($NotSecure -eq $true) { [string]$protocol = 'http' } Else { [string]$protocol = 'https' } [int64]$ps_version_major = $PSVersionTable.PSVersion.Major If ($ps_version_major -eq 5) { $parameters.Add('UseBasicParsing', $true) } ElseIf ($ps_version_major -gt 5) { If ($protocol -eq 'http') { $parameters.Add('SkipCertificateCheck', $true) } } Else { Write-Warning -Message "Please use either Windows PowerShell 5.x or PowerShell Core. This module is not compatible with PowerShell 4 or below." Break } If ($NotAPICommand -ne $true) { [string]$api_url = ($protocol + '://' + $instance + '/automatenow/api' + $command) } Else { [string]$api_url = ($protocol + '://' + $instance + '/automatenow' + $command) } $parameters.Add('Uri', $api_url) $parameters.Add('Headers', $anow_header) $parameters.Add('Method', $Method) $parameters.Add('ContentType', $ContentType) If ($Body.Length -gt 0) { If ($Method -eq 'GET') { Write-Warning -Message "Cannot send a content-body with this verb-type. Please use POST instead :-)." Break } $parameters.Add('Body', $Body) } [string]$parameters_display = $parameters | ConvertTo-Json Write-Verbose -Message "Sending the following parameters to $Instance -> $parameters_display." $Error.Clear() Try { [Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject]$results = Invoke-WebRequest @parameters } Catch { [string]$Message = $_.Exception.Message If ($Message -match '(The underlying connection was closed|The SSL connection could not be established)') { Write-Warning -Message 'Please try again with the -NotSecure parameter if you are connecting to an insecure instance.' Break } ElseIf ($Message -match 'Response status code does not indicate success:') { $Error.Clear() Try { [int32]$return_code = $Message -split 'success: ' -split ' ' | Select-Object -Last 1 -Skip 1 } Catch { [string]$Message2 = $_.Exception.Message Write-Warning -Message "Unable to extract the error code from [$Message] due to [$Message2]" } Write-Verbose "Received status code $return_code instead of 200!" [string]$ReturnCodeWarning = Switch ($return_code) { 401 { "You received HTTP Code $return_code (Unauthorized). HAS YOUR TOKEN EXPIRED? DID YOU UPDATE? :-)" } 403 { "You received HTTP Code $return_code (Forbidden). DO YOU MAYBE NOT HAVE PERMISSION TO THIS? [$command]" } 404 { "You received HTTP Code $return_code (Page Not Found). ARE YOU SURE THIS ENDPOINT REALLY EXISTS? [$command]" } Default { "You received HTTP Code $return_code instead of '200 OK'. Apparently, something is wrong..." } } Write-Warning -Message $ReturnCodeWarning } Else { Write-Warning -Message "Invoke-WebRequest failed due to [$Message]" } Break } [string]$content = $Results.Content If ($content -notmatch '^{.{1,}}$') { Write-Warning -Message "The returned results were somehow not a JSON object." Break } If ($JustGiveMeJSON -eq $true) { Return $content } $Error.Clear() Try { [PSCustomObject]$content_object = $content | ConvertFrom-JSON } Catch { [string]$Message = $_.Exception.Message Write-Warning "ConvertFrom-JSON failed to convert the resturned results due to [$Message]." Break } Return $content_object } #EndRegion #Region - Domain Function Get-AutomateNOWDomain { <# .SYNOPSIS Gets the details of the available domains from an AutomateNOW instance .DESCRIPTION The `Get-AutomateNOWDomain` cmdlet invokes the /domain/read endpoint to retrieve information about the available domains on the instance of AutomateNOW that you are connected to .INPUTS None. You cannot pipe objects to Get-AutomateNOWUser. .OUTPUTS An array of PSCustomObjects (1 for each available domain) .EXAMPLE Get-AutomateNOWDomain .NOTES You must use Connect-AutomateNOW to establish the token by way of global variable. There are no parameters yet for this function. #> [OutputType([array])] [Cmdletbinding()] Param( ) If ((Confirm-AutomateNOWSession -Quiet) -ne $true) { Write-Warning -Message "Somehow there is not a valid token confirmed." Break } [string]$command = '/domain/read' [hashtable]$parameters = @{} $parameters.Add('Command', $command) $parameters.Add('Method', 'GET') If ($anow_session.NotSecure -eq $true) { $parameters.Add('NotSecure', $true) } $Error.Clear() Try { [PSCustomObject]$results = Invoke-AutomateNOWAPI @parameters } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Invoke-AutomateNOWAPI failed due to execute [$command] due to [$Message]." Break } [PSCustomObject[]]$domains = $results.response.data [int32]$domain_count = $domains.Count If ($domain_count -eq 0) { Write-Warning -Message "Somehow there are no domains available. Please look into this..." Break } Return $domains } Function Show-AutomateNOWDomain { <# .SYNOPSIS Shows the details of the available domains from an AutomateNOW instance .DESCRIPTION The `Show-AutomateNOWDomain` cmdlet invokes the Get-AutomateNOWDomain function to retrieve information about the available domains on the instance of AutomateNOW that you are connected to and to show them .INPUTS None. You cannot pipe objects to Show-AutomateNOWDomain. .OUTPUTS None except for Write-Information messages. .EXAMPLE Show-AutomateNOWDomain .NOTES You must use Connect-AutomateNOW to establish the token by way of global variable. There are no parameters yet for this function. #> [Cmdletbinding()] Param( ) If ((Confirm-AutomateNOWSession -Quiet) -ne $true) { Write-Warning -Message "Somehow there is not a valid token confirmed." Break } [PSCustomObject[]]$available_domains = Get-AutomateNOWDomain [string]$Instance = $anow_session.Instance [int32]$domain_count = $available_domains.Count If ($domain_count -gt 1) { [string]$available_domains_display = $available_domains.id -join ', ' Write-Information -MessageData "The [$available_domains_display] domains are available on [$Instance]. Use Switch-AutomateNOWDomain to switch domains." } Else { [string]$available_domains_display = $available_domains.id Write-Information -MessageData "The [$available_domains_display] domain is available on [$Instance]." } } Function Switch-AutomateNOWDomain { <# .SYNOPSIS Switches the currently selected domain for the logged on user of an AutomateNOW! instance .DESCRIPTION The `Switch-AutomateNOWDomain` cmdlet does not actually communicate with the AutomateNOW! instance. It modifies the $anow_session and $anow_header global variables. .PARAMETER Domain Required string representing the name of the domain to switch to. .INPUTS None. You cannot pipe objects to Switch-AutomateNOWDomain. .OUTPUTS None except for Write-Information messages. .EXAMPLE Switch-AutomateNOWDomain -Domain 'Sandbox' .NOTES You must use Connect-AutomateNOW to establish the token by way of global variable. #> [CmdletBinding()] Param( [string]$Domain ) If ((Confirm-AutomateNOWSession -Quiet -IgnoreEmptyDomain) -ne $true) { Write-Warning -Message "Somehow there is not a valid token confirmed." Break } [string]$Instance = $anow_session.Instance If ($anow_session.domains -cnotcontains $Domain) { [string]$available_domains = $anow_session.domains -join ', ' If ($anow_session.domains -contains $Domain) { Write-Warning -Message "The domains are case-sensitive. Please choose from [$available_domains]." Break } Write-Warning -Message "The domain [$Domain] is not on [$Instance]. Please choose from [$available_domains]." Break } $Error.Clear() Try { $anow_session.Remove('current_domain') $anow_session.Add('current_domain', $Domain) } Catch { [string]$Message = $_.Exception.Message Write-Warning "The Add/Remove method failed on `$anow_session` due to [$Message]." Break } $Error.Clear() Try { $anow_header.Remove('domain') $anow_header.Add('domain', $Domain) } Catch { [string]$Message = $_.Exception.Message Write-Warning "The Add/Remove method failed on `$anow_header` due to [$Message]." Break } Write-Information -MessageData "The [$Domain] domain has been selected for [$Instance]." } #EndRegion #Region - Node Function Get-AutomateNOWNode { <# .SYNOPSIS Gets the nodes of all domains from an AutomateNOW! instance .DESCRIPTION The `Get-AutomateNOWNode` retrieves all of the nodes from the connected AutomateNOW! instance .INPUTS None. You cannot pipe objects to Get-AutomateNOWNode. .OUTPUTS An array of PSCustomObjects .EXAMPLE Get-AutomateNOWNode .NOTES You must use Connect-AutomateNOW to establish the token by way of global variable. There are no parameters yet for this function. #> [OutputType([array])] [Cmdletbinding()] Param( ) If ((Confirm-AutomateNOWSession -Quiet) -ne $true) { Write-Warning -Message "Somehow there is not a valid token confirmed." Break } [string]$command = '/serverNode' [hashtable]$parameters = @{} $parameters.Add('Command', $command) $parameters.Add('Method', 'GET') If ($anow_session.NotSecure -eq $true) { $parameters.Add('NotSecure', $true) } $Error.Clear() Try { [PSCustomObject]$results = Invoke-AutomateNOWAPI @parameters } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Invoke-AutomateNOWAPI failed due to execute [$command] due to [$Message]." Break } [PSCustomObject[]]$nodes = $results.response.data [int32]$nodes_count = $nodes.Count If ($nodes_count -eq 0) { Write-Warning -Message "Somehow there are no nodes available. Is this a newly installed instance which has not been configured yet?" Break } Return $nodes } #Endregion #Region - Trigger Log Function Get-AutomateNOWTriggerLog { <# .SYNOPSIS Gets the trigger logs from the domain of an AutomateNOW! instance .DESCRIPTION The `Get-AutomateNOWTriggerLog` retrieves all of the trigger logs from the domain of an AutomateNOW! instance .INPUTS None. You cannot pipe objects to Get-AutomateNOWTriggerLog. .OUTPUTS An array of PSCustomObjects .EXAMPLE Get-AutomateNOWTriggerLog .NOTES You must use Connect-AutomateNOW to establish the token by way of global variable. There are no parameters yet for this function. #> [OutputType([array])] [Cmdletbinding()] Param( ) If ((Confirm-AutomateNOWSession -Quiet) -ne $true) { Write-Warning -Message "Somehow there is not a valid token confirmed." Break } [string]$command = '/executeProcessingTriggerLog' [hashtable]$parameters = @{} $parameters.Add('Command', $command) $parameters.Add('Method', 'GET') If ($anow_session.NotSecure -eq $true) { $parameters.Add('NotSecure', $true) } $Error.Clear() Try { [PSCustomObject]$results = Invoke-AutomateNOWAPI @parameters } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Invoke-AutomateNOWAPI failed due to execute [$command] due to [$Message]." Break } [PSCustomObject[]]$nodes = $results.response.data [int32]$nodes_count = $nodes.Count If ($nodes_count -eq 0) { Write-Warning -Message "Somehow there are no trigger logs available. Is this a newly installed instance which has not been configured yet?" Break } Return $nodes } #Endregion #Region - User Function Get-AutomateNOWUser { <# .SYNOPSIS Gets the details of the currently authenticated user .DESCRIPTION The `Get-AutomateNOWUser` cmdlet invokes the /secUser/getUserInfo endpoint to retrieve information about the currently authenticated user (meaning you) .INPUTS None. You cannot pipe objects to Get-AutomateNOWUser. .OUTPUTS A PSCustomObject .EXAMPLE Get-AutomateNOWUser .NOTES You must use Connect-AutomateNOW to establish the token by way of global variable. There are no parameters yet for this function. #> [OutputType([PSCustomObject])] [Cmdletbinding()] Param( ) If ((Confirm-AutomateNOWSession -Quiet -IgnoreEmptyDomain ) -ne $true) { Write-Warning -Message "Somehow there is not a valid token confirmed." Break } [string]$command = '/secUser/getUserInfo' [string]$Instance = $anow_session.Instance [hashtable]$parameters = @{} $parameters.Add('Command', $command) $parameters.Add('Method', 'GET') $parameters.Add('Instance', $Instance) If ($anow_session.NotSecure -eq $true) { $parameters.Add('NotSecure', $true) } $Error.Clear() Try { [PSCustomObject]$results = Invoke-AutomateNOWAPI @parameters } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Invoke-AutomateNOWAPI failed due to execute [$command] due to [$Message]." Break } Return $results } #Endregion #Region - Icon Function Import-AutomateNOWIcon { <# .SYNOPSIS Imports the icon asset information from an AutomateNOW! instance .DESCRIPTION The `Import-AutomateNOWIcon` function imports the icon asset information from an AutomateNOW! instance and makes it available for other functions (e.g. Export-AutomateNOWIcon) .INPUTS None. You cannot pipe objects to Import-AutomateNOWIcon. .OUTPUTS The output is set into the global variable anow_assets. A .csv file may optionally be created to capture the output. .PARAMETER Instance Specifies the name of the AutomateNOW! instance. For example: s2.infinitedata.com .PARAMETER ExportToFile Switch parameter which enables file export. The name of the file will be chosen automatically (e.g. Export-AutomateNOW-Icons-20251103121511.csv) .EXAMPLE Import-AutomateNOWIcon -Instance 'z4.infinitedata.com' -ExportToFile .NOTES You DO NOT need to authenticate to the instance to execute this function. #> [CmdletBinding()] Param( [Parameter(Mandatory = $false)] [string]$Instance, [Parameter(Mandatory = $false)] [switch]$ExportToFile ) Function Export-AutomateNOWIcon { [CmdletBinding()] Param( [Parameter(Mandatory = $true)] [ValidateSet('FatCow', 'Fugue', 'FontAwesome')] [string]$Library, [Parameter(Mandatory = $true)] [array]$assets_content, [Parameter(Mandatory = $true)] [string]$first_icon_name, [Parameter(Mandatory = $true)] [string]$last_icon_name ) [int32]$assets_content_count = $assets_content.Count If ($assets_content_count -eq 0) { Write-Warning -Message "Somehow there was no content..." Break } [string]$icon_index_first_string_id = ('"ID": "' + $Library + 'DataSource",') [string]$icon_index_first_string_name = ("{name: '$first_icon_name'") [string]$icon_index_last_string_name = ("{name: '$last_icon_name'") $Error.Clear() Try { [int32]$icon_index_first_number1 = $assets_content.IndexOf($($assets_content -match $icon_index_first_string_id | Select-Object -First 1)) } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Failed to extract the first index position of the first icon from the [$Library] icon library due to [$Message]" Break } $Error.Clear() Try { [int32]$icon_index_first_number2 = $assets_content[$icon_index_first_number1..$assets_content_count].IndexOf($($assets_content[$icon_index_first_number1..$assets_content_count] -match $icon_index_first_string_name | Select-Object -First 1)) } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Failed to extract the second index position of the first icon from the [$Library] icon library due to [$Message]" Break } [int32]$icon_index_first_number = ($icon_index_first_number1 + $icon_index_first_number2) Write-Verbose "Extracted first index of [$icon_index_first_number] from the [$Library] icon library" $Error.Clear() Try { [int32]$icon_index_last_number = $assets_content[$icon_index_first_number..$assets_content_count].IndexOf($($assets_content[$icon_index_first_number..$assets_content_count] -match $icon_index_last_string_name | Select-Object -First 1)) } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Failed to extract the index position of the last icon from the [$Library] icon library due to [$Message]" Break } [int32]$icon_index_last_number = ($icon_index_last_number + $icon_index_first_number) Write-Verbose "Extracted last index of [$icon_index_last_number] for [$Library]" $Error.Clear() Try { [array]$icon_raw_array = $assets_content[$icon_index_first_number..$icon_index_last_number] } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Failed to extract the icon data from the assets library from position [$icon_index_first_number] to [$icon_index_last_number] from the [$Library] icon library due to [$Message]" Break } [int32]$icon_raw_array_count = $icon_raw_array.Count Write-Verbose -Message "Found [$icon_raw_array_count] icons from the [$Library] library" [array]$icon_list = $icon_raw_array | ForEach-Object { $_ -replace '\s' -replace 'name:' -replace "{'" -replace "'}," -replace "'}" } Return $icon_list } If ($Instance.Length -eq 0) { [string]$Instance = $anow_session.Instance If ($Instance.Length -eq 0) { Write-Warning -Message 'You need to either supply the instance via -Instance or use Connect-AutomateNOW to define it for you' Break } } If ($Instance -match '/' -or $Instance -match 'http') { Write-Warning -Message 'Please do not include http or any slashes in the instance name. Following are 2 valid examples: a2.InfiniteData.com, contoso-sbox-anow.region.cloudapp.azure.com:8080' Break } [string]$url_homepage = ('https://' + $Instance + '/automatenow/') # Note the backslash at the end. This is required! Write-Verbose -Message "The instance url is set to [$url_homepage]" If ($PSVersionTable.PSVersion.Major -gt 5) { [Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject]$request_homepage = Invoke-WebRequest -uri $url_homepage } ElseIf ($PSVersionTable.PSVersion.Major -eq 5) { [Microsoft.PowerShell.Commands.HtmlWebResponseObject]$request_homepage = Invoke-WebRequest -uri $url_homepage } Else { Write-Warning -Message "Only Windows PowerShell 5.1 and PowerShell Core (7+) are supported." } [int32]$request_statuscode = $request_homepage.StatusCode If ($request_statuscode -ne 200) { Write-Warning -Message "Somehow the response code was [$request_statuscode] instead of 200. Please look into this." Break } [array]$homepage_content = $request_homepage.Content -split "`n" [int32]$homepage_content_line_count = $homepage_content.Count Write-Verbose -Message "The homepage content from [$Instance] has [$homepage_content_line_count] lines" [string]$asset_url = ($url_homepage + ($homepage_content -match 'assets/application/automateNow-[0-9a-z]{32}.js' -replace '"></script>' -replace '<script type="text/javascript" src="/automatenow/' | Select-Object -First 1)) Write-Verbose -Message "Fetching assets from [$asset_url]" If ($PSVersionTable.PSVersion.Major -gt 5) { [Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject]$request_assets = Invoke-WebRequest -Uri $asset_url } ElseIf ($PSVersionTable.PSVersion.Major -eq 5) { [Microsoft.PowerShell.Commands.HtmlWebResponseObject]$request_assets = Invoke-WebRequest -Uri $asset_url } Else { Write-Warning -Message "Only Windows PowerShell 5.1 and PowerShell Core (7+) are supported." } [int32]$request_statuscode = $request_assets.StatusCode If ($request_statuscode -ne 200) { Write-Warning -Message "Somehow the response code was [$request_statuscode] instead of 200. Please look into this." Break } [array]$assets_content = $request_assets.Content -split "`r" -split "`n" [int32]$assets_content_line_count = $assets_content.Count Write-Verbose -Message "The assets content from [$Instance] has [$assets_content_line_count] lines" $Error.Clear() Try { [array]$IconNames_FatCow = Export-AutomateNOWIcon -assets_content $assets_content -Library 'FatCow' -first_icon_name '32_bit' -last_icon_name 'zootool' } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Export-AutomateNOWIcon failed to extract the FatCow icons due to [$Message]" Break } $Error.Clear() Try { [array]$IconNames_Fugue = Export-AutomateNOWIcon -assets_content $assets_content -Library 'Fugue' -first_icon_name 'abacus' -last_icon_name 'zootool' } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Export-AutomateNOWIcon failed to extract the Fugue icons due to [$Message]" Break } $Error.Clear() Try { [array]$IconNames_FontAwesome = Export-AutomateNOWIcon -assets_content $assets_content -Library 'FontAwesome' -first_icon_name '500px' -last_icon_name 'youtube-square' } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Export-AutomateNOWIcon failed to extract the FontAwesome icons due to [$Message]" Break } [int32]$IconCount_FatCow = ($IconNames_FatCow.Count) [int32]$IconCount_Fugue = ($IconNames_Fugue.Count) [int32]$IconCount_FontAwesome = ($IconNames_FontAwesome.Count) If ( $IconCount_FatCow -eq 0 -or $IconCount_Fugue -eq 0 -or $IconCount_FontAwesome -eq 0) { Write-Warning -Message "Somehow one or more of the icon counts summed to zero. Please look into this. [FatCow = $IconCount_FatCow; Fugue = $IconCount_Fugue; FontAwesome = $IconCount_FontAwesome;]" Break } [int32]$IconCount = ($IconCount_FatCow + $IconCount_Fugue + $IconCount_FontAwesome) [PSCustomObject]$icon_library = [PSCustomObject]@{ 'FatCow' = $IconNames_FatCow; 'FatCowCount' = $IconCount_FatCow; 'Fugue' = $IconNames_Fugue; 'FugueCount' = $IconCount_Fugue; 'FontAwesome' = $IconNames_FontAwesome; 'FontAwesomeCount' = $IconCount_FontAwesome; 'TotalCount' = $IconCount; } If ($null -ne $anow_assets.icon_library) { Remove-Variable -Name anow_assets -Force -Scope Global } $Error.Clear() Try { New-Variable -Name anow_assets -Scope Global -Value ([PSCustomObject]@{ icon_library = $icon_library; }) } Catch { [string]$Message = $_.Exception.Message Write-Warning "New-Variable failed due to create the header globally due to [$Message]." Break } Write-Verbose -Message 'Global variable $anow_assets has been set. Use this for asset resources.' If ($ExportToFile -eq $true) { [PSCustomObject[]]$ExportTableFatCow = ForEach ($Icon in $IconNames_FatCow) { [PSCustomObject]@{Library = 'FatCow'; Icon = $Icon; } } [PSCustomObject[]]$ExportTableFugue = ForEach ($Icon in $IconNames_Fugue) { [PSCustomObject]@{Library = 'Fugue'; Icon = $Icon; } } [PSCustomObject[]]$ExportTableFontAwesome = ForEach ($Icon in $IconNames_FontAwesome) { [PSCustomObject]@{Library = 'FontAwesome'; Icon = $Icon; } } [PSCustomObject[]]$DataToExport = ($ExportTableFatCow + $ExportTableFugue + $ExportTableFontAwesome) [int32]$DataToExportCount = $DataToExport.Count If ($DataToExportCount -eq 0) { Write-Warning -Message "Somehow there are zero icons to export. Please look into this." Break } $Error.Clear() Try { [array]$ConvertedData = $DataToExport | ConvertTo-CSV -Delimiter "`t" -NoTypeInformation } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "ConvertTo-CSV failed to convert the icon objects due to [$Message]" Break } [array]$FormattedData = $ConvertedData | ForEach-Object { $_ -replace '"' } [string]$current_time = Get-Date -Format 'yyyyMMddHHmmssfff' [string]$ExportFileName = 'Export-AutomateNOW-Icons-' + $current_time + '.csv' [string]$ExportFilePath = ($PSScriptRoot + '\' + $ExportFileName) $Error.Clear() Try { $FormattedData | Out-File -FilePath $ExportFilePath -Encoding utf8BOM # Note: Use utf8 encoding to guarantee that this file opens correctly in Excel } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Out-File failed to convert the icon objects due to [$Message]" Break } $Error.Clear() [System.IO.FileInfo]$fileinfo = Get-Item -Path "$ExportFilePath" [int32]$filelength = $fileinfo.Length [string]$filelength_display = "{0:N0}" -f $filelength Write-Information -MessageData "Created file $ExportFileName ($filelength_display bytes)" } } #EndRegion #Region - Tag Function New-AutomateNOWTag { <# .SYNOPSIS Creates a new tag on an AutomateNOW! instance .DESCRIPTION The `New-AutomateNOWTag` function creates a new tag on an AutomateNOW! instance. .INPUTS None. You cannot pipe objects to New-AutomateNOWTag. .OUTPUTS A PSCustomObject representing the newly created tag .PARAMETER id The intended name of the tag. For example: 'MyCoolTag' .PARAMETER description The description of the tag. This parameter is not required. For example: 'My cool tag description' .PARAMETER iconSet The name of the icon library (if you choose to use one). Possible choices are: FatCow, Fugue, FontAwesome .PARAMETER iconName The name of the icon which matches the chosen library. .PARAMETER textColor The RGB in hex of the tag's foreground (text) color. For example: FFFFFF (note there is no # symbol) .PARAMETER backgroundColor The RGB in hex of the tag's background color. For example: A0A0A0 (note there is no # symbol) .EXAMPLE New-AutomateNOWTag -id 'MyCoolTag123' -description 'My tags description' -iconSet 'Fugue' -IconCode 'abacus' -textColor '0A0A0A' -backgroundColor 'F0F0F0' .NOTES You must use Connect-AutomateNOW to establish the token by way of global variable. #> [Cmdletbinding()] Param( [Parameter(Mandatory = $true)] [string]$id, [Parameter(Mandatory = $false)] [string]$description = '', [Parameter(Mandatory = $true, ParameterSetName = 'WithIcon')] [ValidateSet('FatCow', 'Fugue', 'FontAwesome')] [string]$iconSet, [Parameter(Mandatory = $true, ParameterSetName = 'WithIcon')] [string]$iconCode, [Parameter(Mandatory = $true, ParameterSetName = 'WithIcon')] [ValidateScript( { $_ -match '[0-9A-F]{6}' } ) ] [string]$textColor = 'FFFFFF', [Parameter(Mandatory = $true, ParameterSetName = 'WithIcon')] [ValidateScript( { $_ -match '[0-9A-F]{6}' } ) ] [string]$backgroundColor = 'FF0000' ) If ($id.Length -eq 0) { Write-Warning -Message "The Id must be at least 1 character in length. Please try again." Break } If (($iconSet.Length -gt 0) -and ($iconCode.Length -eq 0)) { Write-Warning -Message "If you specify an icon library then you must also specify an icon" Break } If ((Confirm-AutomateNOWSession -IgnoreEmptyDomain -Quiet) -ne $true) { Write-Warning -Message "Somehow there is no global session token" Break } [string]$iconSet = Switch ($iconSet) { 'FatCow' { 'FAT_COW'; Break } 'Fegue' { 'FEGUE'; Break } 'FontAwesome' { 'FONT_AWESOME'; Break } 'Default' { 'FAT_COW' } } [string]$command = '/tag/create' [string]$description = [System.Net.WebUtility]::UrlEncode($description) [string]$ContentType = 'application/x-www-form-urlencoded; charset=UTF-8' [string]$Body = ('textColor=%23' + $textColor + '&backgroundColor=%23' + $backgroundColor + '&id=' + $id + '&description=' + $description + '&iconSet=' + $iconSet + '&iconCode=' + $iconCode) #'&_operationType=add&_textMatchStyle=exact&_oldValues=%7B%22textColor%22%3A%22%23FFFFFF%22%2C%22backgroundColor%22%3A%22%23FF0000%22%7D&_componentId=TagCreateWindow_form&_dataSource=TagDataSource&isc_metaDataPrefix=_&isc_dataFormat=json" [hashtable]$parameters = @{} $parameters.Add('Method', 'POST') $parameters.Add('ContentType', $ContentType) $parameters.Add('Command', $command) $parameters.Add('Body', $Body) If ($Verbose -eq $true) { $parameters.Add('Verbose', $true) } $Error.Clear() Try { [PSCustomObject]$response = Invoke-AutomateNOWAPI @parameters } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Invoke-AutomateNOWAPI failed due to [$Message]" } If ($response.response.status -isnot [int64] -and $response.response.status -isnot [int32]) { # Yes, the API really does return back an int64 [string]$parameters_display = $parameters | ConvertTo-Json -Compress Write-Warning -Message "Somehow there was not a valid response to the [$command] command. Please look into this. Parameters: $parameters_display" Break } [int32]$response_code = $response.response.status If ($response_code -ne 0) { [string]$full_response_display = $response.response | ConvertTo-Json -Compress Write-Warning -Message "Somehow the response code was not 0 but was $response_code. Please look into this. Body: $full_response_display" } [PSCustomObject]$tag_data = $response.response.data [string]$tag_display = $tag_data | ConvertTo-Json -Compress Write-Verbose -Message "Created tag: $tag_display" Return $tag_data } Function Get-AutomateNOWTag { <# .SYNOPSIS Gets the tags of all domains from an AutomateNOW! instance .DESCRIPTION The `Get-AutomateNOWTag` function retrieves all of the tags from the connected AutomateNOW! instance .INPUTS None. You cannot pipe objects to Get-AutomateNOWTag. .OUTPUTS An array of PSCustomObjects .EXAMPLE Get-AutomateNOWTag .NOTES You must use Connect-AutomateNOW to establish the token by way of global variable. There are no parameters yet for this function. #> [OutputType([array])] [Cmdletbinding()] Param( ) If ((Confirm-AutomateNOWSession -Quiet) -ne $true) { Write-Warning -Message "Somehow there is not a valid token confirmed." Break } [string]$command = '/tag/readAllDomains' [hashtable]$parameters = @{} $parameters.Add('Command', $command) $parameters.Add('Method', 'GET') If ($anow_session.NotSecure -eq $true) { $parameters.Add('NotSecure', $true) } $Error.Clear() Try { [PSCustomObject]$results = Invoke-AutomateNOWAPI @parameters } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Invoke-AutomateNOWAPI failed due to execute [$command] due to [$Message]." Break } [PSCustomObject[]]$Tags = $results.response.data [int32]$Tags_count = $Tags.Count If ($Tags_count -eq 0) { Write-Warning -Message "Somehow there are no tags available. Was this instance just created 5 minutes ago?" Break } Return $Tags } Function Remove-AutomateNOWTag { <# .SYNOPSIS Removes one tag from an AutomateNOW! instance .DESCRIPTION The `Remove-AutomateNOWTag` function removes one tag from an AutomateNOW! instance .PARAMETER Id The Id of the tag to delete. You can specify like this: -Id 'MyCoolTag' -Domain 'Training' or omit the -Domain parameter and format the Id yourself with -Id '[Training]MyCoolTag' .PARAMETER Domain The domain of the instance you are removing the tag from. If you do not include the domain here then you must include it with the -Id parameter in the excepted format. .PARAMETER Quiet Use this switch to silently remove the tag without any confirmation that it was successful. This is ideal for batch operations. .INPUTS `Remove-AutomateNOWTag` accepts pipeline input on the Id parameter .OUTPUTS None. The status will be written to the console with Write-Information. .EXAMPLE Remove-AutomateNOWTag -Id @('[Training]MyCoolTag123', '[Training]MyCoolTag456') .NOTES You must use Connect-AutomateNOW to establish the token by way of global variable. #> [OutputType([array])] [Cmdletbinding()] Param( [Parameter(Mandatory = $True)] [string]$Id, [Parameter(Mandatory = $false)] [string]$Domain, [Parameter(Mandatory = $false)] [switch]$Quiet ) If ((Confirm-AutomateNOWSession -Quiet) -ne $true) { Write-Warning -Message "Somehow there is not a valid token confirmed." Break } If ($Id -match '^(\s.{1,}|.{1,}\s)$') { Write-Warning -Message "You seem to have whitespace characters in the beginning or end of [$Id]. Please fix this." Break } ElseIf ($Domain -match '^(\s.{1,}|.{1,}\s)$') { Write-Warning -Message "You seem to have whitespace characters in the beginning or end of [$Domain]. Please fix this." Break } If ($Id -notmatch '[.{1,}].{1,}') { If ($Domain.Length -eq 0) { Write-Warning -Message "You must include the domain either in the Id (e.g. -Id '[Training]MyCoolTag') or with the -Domain parameter (e.g. -Domain 'Training' -Id 'MyCoolTag'). Please try again." Break } Else { [string]$Id = ('[' + $Domain + "]$Id") } } [string]$command = '/tag/delete' [string]$Body = 'id=' + $id [hashtable]$parameters = @{} $parameters.Add('Command', $command) $parameters.Add('Method', 'POST') $parameters.Add('Body', $Body) $parameters.Add('ContentType', 'application/x-www-form-urlencoded; charset=UTF-8') If ($anow_session.NotSecure -eq $true) { $parameters.Add('NotSecure', $true) } $Error.Clear() Try { [PSCustomObject]$response = Invoke-AutomateNOWAPI @parameters } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Invoke-AutomateNOWAPI failed due to execute [$command] on [$Id] due to [$Message]." Break } If ($response.response.status -isnot [int64] -and $response.response.status -isnot [int32]) { # Yes, the API really does return back an int64 [string]$parameters_display = $parameters | ConvertTo-Json -Compress Write-Warning -Message "Somehow there was not a valid response to the [$command] command. Please look into this. Parameters: $parameters_display" Break } [int32]$response_code = $response.response.status If ($response_code -ne 0) { [string]$full_response_display = $response.response | ConvertTo-Json -Compress Write-Warning -Message "Somehow the response code was not 0 but was [$response_code]. Please look into this. Body: $full_response_display" } If ($Quiet -ne $true) { Write-Information -MessageData "Tag $Id successfully deleted" } } #EndRegion #Region - Workflows Function Get-AutomateNOWWorkflow { <# .SYNOPSIS Gets the workflow objects from an instance of AutomateNOW! .DESCRIPTION The `Get-AutomateNOWWorkflow` cmdlet gets the workflow objects from an instance of AutomateNOW! .INPUTS None. You cannot pipe objects to Get-AutomateNOWWorkflow. .OUTPUTS An array of PSCustomObjects .EXAMPLE Get-AutomateNOWWorkflow .NOTES You must use Connect-AutomateNOW to establish the token by way of global variable. There are no parameters yet for this function. #> [OutputType([PSCustomObject])] [Cmdletbinding()] Param( ) If ((Confirm-AutomateNOWSession -Quiet -IgnoreEmptyDomain ) -ne $true) { Write-Warning -Message "Somehow there is not a valid token confirmed." Break } [string]$command = '/processingTemplate/read' [string]$Body = '_constructor=AdvancedCriteria&operator=and&criteria=%7B%22fieldName%22%3A%22workflowType%22%2C%22operator%22%3A%22equals%22%2C%22value%22%3A%22STANDARD%22%7D' [string]$Instance = $anow_session.Instance [hashtable]$parameters = @{} $parameters.Add('Command', $command) $parameters.Add('Method', 'POST') $parameters.Add('Body', $Body) $parameters.Add('ContentType', 'application/x-www-form-urlencoded; charset=UTF-8') $parameters.Add('Instance', $Instance) $parameters.Add('JustGiveMeJSON', $True) If ($anow_session.NotSecure -eq $true) { $parameters.Add('NotSecure', $true) } $Error.Clear() Try { [PSCustomObject]$results = Invoke-AutomateNOWAPI @parameters } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Invoke-AutomateNOWAPI failed due to execute [$command] due to [$Message]." Break } [int32]$ps_version_major = $PSVersionTable.PSVersion.Major If ($ps_version_major -eq 5) { $Error.Clear() Try { Add-Type -AssemblyName System.Web.Extensions } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Add-Type failed to add type [System.Web.Extensions] due to [$Message]" Break } $Error.Clear() Try { [Web.Script.Serialization.JavaScriptSerializer]$JavaScriptSerializer = [Web.Script.Serialization.JavaScriptSerializer]::new() } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "The new() method of Web.Script.Serialization.JavaScriptSerializer failed due to [$Message]" Break } $Error.Clear() Try { [hashtable]$Workflow_hashtable = $JavaScriptSerializer.Deserialize($results, [hashtable]) } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "The Deserialize() method of Web.Script.Serialization.JavaScriptSerializer failed due to [$Message]" Break } } Else { $Error.Clear() Try { [hashtable]$Workflow_hashtable = $results | ConvertFrom-Json -AsHashTable } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "ConvertFrom-JSON failed to create a hashtable due to [$Message]" Break } } If ($Workflow_hashtable.response.status -isnot [int32]) { Write-Warning -Message "The response to Get-AutomateNOWWorkflow did not include a response code. Please look into this." Break } ElseIf ($Workflow_hashtable.response.status -ne 0) { [int32]$response_status_code = $Workflow_hashtable.response.status Write-Warning -Message "Received a response code of [$response_status_code] instead of 0. Please look into this." Break } [array]$WorkFlow_array = $Workflow_hashtable.response.data [int32]$WorkFlow_array_count = $WorkFlow_array.Count If ($WorkFlow_array_count -eq 0) { Write-Warning -Message "Somehow there were no workflows returned from Get-AutomateNOWWorkflow. Please look into this." Break } Return $WorkFlow_array } #endregion #Region Folders Function Get-AutomateNOWFolder { <# .SYNOPSIS Gets the folder objects from an instance of AutomateNOW! .DESCRIPTION The `Get-AutomateNOWFolder` cmdlet gets the folder objects from an instance of AutomateNOW! .INPUTS None. You cannot pipe objects to Get-AutomateNOWFolder. .OUTPUTS An array of PSCustomObjects .EXAMPLE Get-AutomateNOWFolder .NOTES You must use Connect-AutomateNOW to establish the token by way of global variable. There are no parameters yet for this function. #> [OutputType([PSCustomObject])] [Cmdletbinding()] Param( ) If ((Confirm-AutomateNOWSession -Quiet -IgnoreEmptyDomain ) -ne $true) { Write-Warning -Message "Somehow there is not a valid token confirmed." Break } [string]$command = '/folder/read' [string]$Instance = $anow_session.Instance [hashtable]$parameters = @{} $parameters.Add('Command', $command) $parameters.Add('Method', 'GET') $parameters.Add('Instance', $Instance) If ($anow_session.NotSecure -eq $true) { $parameters.Add('NotSecure', $true) } $Error.Clear() Try { [PSCustomObject]$response = Invoke-AutomateNOWAPI @parameters } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Invoke-AutomateNOWAPI failed due to execute [$command] due to [$Message]." Break } If ($response.response.status -isnot [int64] -and $response.response.status -isnot [int32]) { # Yes, the API really does return back an int64 [string]$parameters_display = $parameters | ConvertTo-Json -Compress Write-Warning -Message "Somehow there was not a valid response to the [$command] command. Please look into this. Parameters: $parameters_display" Break } [int32]$response_code = $response.response.status If ($response_code -ne 0) { [string]$full_response_display = $response.response | ConvertTo-Json -Compress Write-Warning -Message "Somehow the response code was not 0 but was [$response_code]. Please look into this. Body: $full_response_display" } [array]$folders = $response.response.data [int32]$folders_count = $folders.Count If ($folders_count -eq 0) { Write-Warning -Message "It appears there are zero folders created yet. Did you create this instance recently?" } If ($Quiet -ne $true) { Write-Information -MessageData "Returned the properties of all [$folders_count] folders" } Return $folders } Function New-AutomateNOWFolder { <# .SYNOPSIS Creates a new folder object in an instance of AutomateNOW! .DESCRIPTION The `New-AutomateNOWFolder` cmdlet creates a new folder object in an instance of AutomateNOW! .PARAMETER Id The name of the folder. For example: 'MyCoolFolder' .PARAMETER Description The description of the folder (may contain unicode characters). For example: 'My folder description' .INPUTS None. You cannot pipe objects to New-AutomateNOWFolder. .OUTPUTS A PSCustomObject representing the properties of the newly created folder .EXAMPLE New-AutomateNOWFolder .NOTES You must use Connect-AutomateNOW to establish the token by way of global variable. #> [OutputType([PSCustomObject])] [Cmdletbinding()] Param( [Parameter(Mandatory = $True)] [string]$Id, [Parameter(Mandatory = $false)] [string]$Description, [Parameter(Mandatory = $false)] [string]$Repository, [Parameter(Mandatory = $false)] [switch]$Quiet ) If ((Confirm-AutomateNOWSession -Quiet -IgnoreEmptyDomain ) -ne $true) { Write-Warning -Message "Somehow there is not a valid token confirmed." Break } If ($Repository.Length -gt 0) { Write-Warning -Message 'Unfortunately, it seems that associating a folder with a repository is broken. Hence, this parameter is disabled.' Break } If ($Id -notmatch '^[a-zA-Z0-9-._]{1,}$') { Write-Warning -Message 'The name of the folder may only consist of letters, numbers, underscores, periods (dot) and hyphens (dash). Please try again' } [string]$command = '/folder/create' [string]$Instance = $anow_session.Instance [string]$Id_Encoded = [System.Net.WebUtility]::UrlEncode($Id) [string]$Body = 'id=' + $Id_Encoded If ($Description.Length -gt 0) { [string]$Description_Encoded = [System.Net.WebUtility]::UrlEncode($Description) $Body = $Body + '&description=' + $Description_Encoded } [hashtable]$parameters = @{} $parameters.Add('Command', $command) $parameters.Add('Method', 'POST') $parameters.Add('Instance', $Instance) $parameters.Add('Body', $Body) If ($anow_session.NotSecure -eq $true) { $parameters.Add('NotSecure', $true) } $Error.Clear() Try { [PSCustomObject]$response = Invoke-AutomateNOWAPI @parameters } Catch { [string]$Message = $_.Exception.Message Write-Warning -Message "Invoke-AutomateNOWAPI failed due to execute [$command] due to [$Message]." Break } If ($response.response.status -isnot [int64] -and $response.response.status -isnot [int32]) { # Yes, the API really does return back an int64 [string]$parameters_display = $parameters | ConvertTo-Json -Compress Write-Warning -Message "Somehow there was not a valid response to the [$command] command. Please look into this. Parameters: $parameters_display" Break } [int32]$response_code = $response.response.status If ($response_code -ne 0) { [string]$full_response_display = $response.response | ConvertTo-Json -Compress Write-Warning -Message "Somehow the response code was not 0 but was [$response_code]. Please look into this. Body: $full_response_display" } [PSCustomObject[]]$folders = $response.response.data [int32]$folders_count = $folders.Count If ($folders_count -ne 1) { Write-Warning -Message "Somehow there was an error and folder [$id] was not created" } If ($Quiet -ne $true) { Write-Information -MessageData "Folder [$id] was created" } [PSCustomObject]$folder = $folders | Select-Object -First 1 Return $folder } #endregion |