From d26d292eac11efb383cd0799a5d119cbf77ac314 Mon Sep 17 00:00:00 2001 From: Pete Maan Date: Fri, 28 Jun 2019 23:19:36 +0100 Subject: [PATCH 01/15] :label: REMOVE CopyToSafe Method Classic API specific - removing for now --- psPAS/psPAS.CyberArk.Vault.Account.Type.ps1xml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/psPAS/psPAS.CyberArk.Vault.Account.Type.ps1xml b/psPAS/psPAS.CyberArk.Vault.Account.Type.ps1xml index 09f7b436..19f8ce96 100644 --- a/psPAS/psPAS.CyberArk.Vault.Account.Type.ps1xml +++ b/psPAS/psPAS.CyberArk.Vault.Account.Type.ps1xml @@ -45,17 +45,6 @@ } - - CopyToSafe - - \ No newline at end of file From 6fd349ec74e5d68125b42f897e78cd3ff5d03884 Mon Sep 17 00:00:00 2001 From: Pete Maan Date: Sun, 30 Jun 2019 02:30:25 +0100 Subject: [PATCH 02/15] :heavy_plus_sign: ADD Out-PASFile New helper function to save byte stream as file. --- Tests/Out-PASFile.Tests.ps1 | 71 +++++++++++++++++++++++++++++ psPAS/Private/Out-PASFile.ps1 | 86 +++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 Tests/Out-PASFile.Tests.ps1 create mode 100644 psPAS/Private/Out-PASFile.ps1 diff --git a/Tests/Out-PASFile.Tests.ps1 b/Tests/Out-PASFile.Tests.ps1 new file mode 100644 index 00000000..4cc39aa5 --- /dev/null +++ b/Tests/Out-PASFile.Tests.ps1 @@ -0,0 +1,71 @@ +#Get Current Directory +$Here = Split-Path -Parent $MyInvocation.MyCommand.Path + +#Get Function Name +$FunctionName = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -Replace ".Tests.ps1" + +#Assume ModuleName from Repository Root folder +$ModuleName = Split-Path (Split-Path $Here -Parent) -Leaf + +#Resolve Path to Module Directory +$ModulePath = Resolve-Path "$Here\..\$ModuleName" + +#Define Path to Module Manifest +$ManifestPath = Join-Path "$ModulePath" "$ModuleName.psd1" + +if ( -not (Get-Module -Name $ModuleName -All)) { + + Import-Module -Name "$ManifestPath" -ArgumentList $true -Force -ErrorAction Stop + +} + +BeforeAll { + + $Script:RequestBody = $null + $Script:BaseURI = "https://SomeURL/SomeApp" + $Script:ExternalVersion = "0.0" + $Script:WebSession = New-Object Microsoft.PowerShell.Commands.WebRequestSession + +} + +AfterAll { + + $Script:RequestBody = $null + +} + +Describe $FunctionName { + + InModuleScope $ModuleName { + + + + Context "Standard Operation" { + BeforeEach { + + $Object = [PSCustomObject]@{ + Content = New-Object Byte[] 512 + Headers = @{"Content-Disposition" = "attachment; filename=FILENAME.zip" } + } + + + Mock Set-Content -MockWith { } + + } + + it "does not throw" { + + { Out-PASFile -InputObject $Object -Path "C:\Temp" } | Should -Not -Throw + + } + + It "throws on Set-Content error" { + Mock Set-Content -MockWith { throw "error" } + { Out-PASFile -InputObject $Object } | Should -Throw + } + + } + + } + +} diff --git a/psPAS/Private/Out-PASFile.ps1 b/psPAS/Private/Out-PASFile.ps1 new file mode 100644 index 00000000..ff97411b --- /dev/null +++ b/psPAS/Private/Out-PASFile.ps1 @@ -0,0 +1,86 @@ +function Out-PASFile { + <# + .SYNOPSIS + Writes a Byte Array to a file + + .DESCRIPTION + Takes a Byte Array from a web response and writes it to a file. + Suggested filename from Content-Disposition Header is used for naming. + + .PARAMETER InputObject + Content and Header properties from a web response + + .PARAMETER Path + Output folder for the file. + Defaults to $ENV:TEMP + + .EXAMPLE + Out-PASFile -InputObject $result + + #> + [CmdletBinding(SupportsShouldProcess)] + param( + [parameter( + Mandatory = $false, + ValueFromPipelinebyPropertyName = $true + )] + $InputObject, + + [parameter( + Mandatory = $false, + ValueFromPipelinebyPropertyName = $true + )] + [string]$Path + ) + + Begin { } + + Process { + + #Get filename from Content-Disposition Header element. + $FileName = ($InputObject.Headers["Content-Disposition"] -split "filename=")[1] -replace '"' + + If (-not ($Path)) { + + #Default to TEMP if path not provided + $Path = [Environment]::GetEnvironmentVariable("Temp") + + } + + #Define output path + $OutputPath = Join-Path $Path $FileName + + if ($PSCmdlet.ShouldProcess($OutputPath, "Save File")) { + + try { + + #Command Parameters + $output = @{ + Path = $OutputPath + Value = $InputObject.Content + Encoding = "Byte" + } + + If ($IsCoreCLR) { + + #amend parameters for splatting if we are in Core + $output.Add("AsByteStream", $true) + $output.Remove("Encoding") + + } + + #write file + Set-Content @output -ErrorAction Stop + + #return file object + Get-Item -Path $OutputPath + + } catch { throw "Error Saving $OutputPath" } + + } + + } + + End { } + +} \ No newline at end of file From e5bc7fe07a40945015ef0a530b5baa7d7329a100 Mon Sep 17 00:00:00 2001 From: Pete Maan Date: Sun, 30 Jun 2019 02:30:50 +0100 Subject: [PATCH 03/15] Update README.md --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 54e429a9..aca87ea5 100644 --- a/README.md +++ b/README.md @@ -533,6 +533,23 @@ secretManagement : @{automaticManagementEnabled=True; lastModifiedTime=155986422 createdTime : 06/06/2019 23:37:02 ```` +#### Using Methods + +Methods present on objects returned from psPAS functions can be leveraged to get the data you need with ease. + +- The `psPAS.CyberArk.Vault.Safe` object returned by `Get-PASSafe` has a ScriptMethod (`SafeMembers()`), which will run a query for the members of the safe: + +```powershell +#List all safes where AppUser is not a member +Get-PASSafe | Where-Object{ ($_.safemembers() | Select-Object -ExpandProperty UserName) -notcontains "AppUser"} +``` + +- Retrieved credentials can be immediately converted into Secure Strings: + +```powershell +(Get-PASAccount -id 330_5 | Get-PASAccountPassword).ToSecureString() +``` + #### API Sessions - If actions are required to be performed under the context of different user accounts, it is possible to work with different authenticated sessions: From 16896e31517617afa64fa184383ade4ed52fbeb8 Mon Sep 17 00:00:00 2001 From: Pete Maan Date: Sun, 30 Jun 2019 02:33:04 +0100 Subject: [PATCH 04/15] :fire: REMOVE psPAS.CyberArk.Vault.PSM.Type Reworking output of RDP files - redundant type removed. --- psPAS/psPAS.CyberArk.Vault.PSM.Type.ps1xml | 56 ---------------------- psPAS/psPAS.psd1 | 5 +- 2 files changed, 2 insertions(+), 59 deletions(-) delete mode 100644 psPAS/psPAS.CyberArk.Vault.PSM.Type.ps1xml diff --git a/psPAS/psPAS.CyberArk.Vault.PSM.Type.ps1xml b/psPAS/psPAS.CyberArk.Vault.PSM.Type.ps1xml deleted file mode 100644 index 1576625c..00000000 --- a/psPAS/psPAS.CyberArk.Vault.PSM.Type.ps1xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - psPAS.CyberArk.Vault.PSM.Connection.RDP - - - ToRDPFile - - - - - \ No newline at end of file diff --git a/psPAS/psPAS.psd1 b/psPAS/psPAS.psd1 index 432b7b20..09ae7e7a 100644 --- a/psPAS/psPAS.psd1 +++ b/psPAS/psPAS.psd1 @@ -52,10 +52,9 @@ TypesToProcess = @( 'psPAS.CyberArk.Vault.Account.Type.ps1xml', 'psPAS.CyberArk.Vault.ACL.Type.ps1xml', - 'psPAS.CyberArk.Vault.Credential.Type.ps1xml' + 'psPAS.CyberArk.Vault.Credential.Type.ps1xml', 'psPAS.CyberArk.Vault.Safe.Type.ps1xml', - 'psPAS.CyberArk.Vault.User.Type.ps1xml', - 'psPAS.CyberArk.Vault.PSM.Type.ps1xml' + 'psPAS.CyberArk.Vault.User.Type.ps1xml' ) # Format files (.ps1xml) to be loaded when importing this module From 53c429df33863caf1598275e9641b3d8aef913b5 Mon Sep 17 00:00:00 2001 From: Pete Maan Date: Sun, 30 Jun 2019 02:41:00 +0100 Subject: [PATCH 05/15] :zap: :sparkles: UPDATE File Output Moving logic to save byte stream output to file to `Out-PASFile`. Updated `Get-PASPSMConnectionParameter` to output an RDP file by default, or the HTML5 GW details. `Get-PASResponse` updated to return api response content and header if content type is `octet-stream`. This allows `Out-PASFile` to use information in the response header to name the output file. --- Tests/Export-PASPlatform.Tests.ps1 | 34 +++------------- Tests/Get-PASPSMConnectionParameter.Tests.ps1 | 40 ++++++------------- .../Get-PASPSMConnectionParameter.ps1 | 29 +++++++++----- .../Platforms/Export-PASPlatform.ps1 | 27 ++----------- psPAS/Private/Get-PASResponse.ps1 | 12 ++++++ 5 files changed, 52 insertions(+), 90 deletions(-) diff --git a/Tests/Export-PASPlatform.Tests.ps1 b/Tests/Export-PASPlatform.Tests.ps1 index b8e54afe..ada1f674 100644 --- a/Tests/Export-PASPlatform.Tests.ps1 +++ b/Tests/Export-PASPlatform.Tests.ps1 @@ -44,6 +44,8 @@ Describe $FunctionName { } + Mock Out-PASFile -MockWith { } + Context "Mandatory Parameters" { $Parameters = @{Parameter = 'PlatformID' }, @@ -67,14 +69,6 @@ Describe $FunctionName { { Export-PASPlatform -PlatformID SomePlatform -path A:\test.txt } | Should throw } - It "throws if InputFile resolves to a folder" { - { Export-PASPlatform -PlatformID SomePlatform -path $pwd } | Should throw - } - - It "throws if InputFile does not have a zip extention" { - { Export-PASPlatform -PlatformID SomePlatform -path README.MD } | Should throw - } - It "sends request" { Assert-MockCalled Invoke-PASRestMethod -Scope Describe -Times 1 -Exactly @@ -98,29 +92,11 @@ Describe $FunctionName { } It "throws error if version requirement not met" { -$Script:ExternalVersion = "1.0" - { Export-PASPlatform -PlatformID SomePlatform -path "$env:Temp\testExport.zip" } | Should Throw -$Script:ExternalVersion = "0.0" + $Script:ExternalVersion = "1.0" + { Export-PASPlatform -PlatformID SomePlatform -path "$env:Temp\testExport.zip" } | Should Throw + $Script:ExternalVersion = "0.0" } - - } - - Context "Output" { - - it "saves output file" { - - Test-Path "$env:Temp\testExport.zip" | should Be $true - - } - - it "reports error saving outputfile" { - Mock Set-Content -MockWith { throw something } - { Export-PASPlatform -PlatformID SomePlatform -path "$env:Temp\testExport.zip" } | should throw "Error Saving $env:Temp\testExport.zip" - } - - - } } diff --git a/Tests/Get-PASPSMConnectionParameter.Tests.ps1 b/Tests/Get-PASPSMConnectionParameter.Tests.ps1 index 6c3569a4..fcf5a882 100644 --- a/Tests/Get-PASPSMConnectionParameter.Tests.ps1 +++ b/Tests/Get-PASPSMConnectionParameter.Tests.ps1 @@ -76,6 +76,12 @@ Describe $FunctionName { $Script:BaseURI = "https://SomeURL/SomeApp" $Script:ExternalVersion = "0.0" $Script:WebSession = New-Object Microsoft.PowerShell.Commands.WebRequestSession + + Mock Out-PASFile -MockWith { } + } + + It "throws if path is invalid" { + { $InputObj | Get-PASPSMConnectionParameter -ConnectionMethod RDP -path A:\test.txt } | Should throw } It "sends request" { @@ -134,7 +140,7 @@ Describe $FunctionName { Assert-MockCalled Invoke-PASRestMethod -ParameterFilter { - $WebSession.Headers["Accept"] -eq 'application/json' } -Times 1 -Exactly -Scope It + $WebSession.Headers["Accept"] -eq 'application/octet-stream' } -Times 1 -Exactly -Scope It } @@ -182,9 +188,10 @@ Describe $FunctionName { Context "Output" { + BeforeEach { Mock Invoke-PASRestMethod -MockWith { - [PSCustomObject]@{"Prop1" = "VAL1"; "Prop2" = "Val2"; "Prop3" = "Val3" } + [PSCustomObject]@{"PSMGWRequest" = "VAL1"; "PSMGWURL" = "Val2"; "Prop3" = "Val3" } } $InputObj = [pscustomobject]@{ @@ -194,40 +201,17 @@ Describe $FunctionName { } - $AdHocObj = [pscustomobject]@{ - "ConnectionComponent" = "SomeConnectionComponent" - "UserName" = "SomeUser" - "secret" = "SomeSecret" | ConvertTo-SecureString -AsPlainText -Force - "address" = "Some.Address" - "platformID" = "SomePlatform" - - } - $Script:BaseURI = "https://SomeURL/SomeApp" $Script:ExternalVersion = "0.0" $Script:WebSession = New-Object Microsoft.PowerShell.Commands.WebRequestSession - } - - it "provides output" { - - $InputObj | Get-PASPSMConnectionParameter -ConnectionMethod RDP | Should not BeNullOrEmpty - - } - - It "has output with expected number of properties" { - - ($InputObj | Get-PASPSMConnectionParameter -ConnectionMethod RDP | Get-Member -MemberType NoteProperty).length | Should Be 3 + Mock Out-PASFile -MockWith { } } - it "outputs object with expected typename" { - - $InputObj | Get-PASPSMConnectionParameter -ConnectionMethod RDP | get-member | select-object -expandproperty typename -Unique | Should Be psPAS.CyberArk.Vault.PSM.Connection.RDP - + It "outputs PSMGW connection information" { + $InputObj | Get-PASPSMConnectionParameter -ConnectionMethod PSMGW | Should -Not -Be Null } - - } } diff --git a/psPAS/Functions/Connections/Get-PASPSMConnectionParameter.ps1 b/psPAS/Functions/Connections/Get-PASPSMConnectionParameter.ps1 index 5c6eed17..76c71e14 100644 --- a/psPAS/Functions/Connections/Get-PASPSMConnectionParameter.ps1 +++ b/psPAS/Functions/Connections/Get-PASPSMConnectionParameter.ps1 @@ -4,10 +4,8 @@ function Get-PASPSMConnectionParameter { Get required parameters to connect through PSM .DESCRIPTION -This method enables you to connect to an account through PSM (PSMConnect) using -a connection method defined in the PVWA. -The function returns the parameters to be used in an RDP file or with a Remote Desktop Manager, or if -a PSMGW is configured, the HTML5 connection data and the required PSMGW URL. +This method enables you to connect to an account through PSM (PSMConnect) using. +The function returns either an RDP file or URL for PSM connections. It requires the PVWA and PSM to be configured for either transparent connections through PSM with RDP files or the HTML5 Gateway. @@ -49,10 +47,13 @@ The expected parameters to be returned, either RDP or PSMGW. PSMGW is only available from version 10.2 onwards +.PARAMETER Path +The folder to save the output file in. + .EXAMPLE Get-PASPSMConnectionParameter -AccountID $ID -ConnectionComponent PSM-SSH -reason "Fix XYZ" -Outputs RDP file contents for Direct Connection via PSM using account with ID in $ID +Outputs RDP file for Direct Connection via PSM using account with ID in $ID .NOTES Minimum CyberArk Version 9.10 @@ -175,7 +176,14 @@ Ad-Hoc connections require 10.5 ParameterSetName = "AdHocConnect" )] [ValidateSet("RDP", "PSMGW")] - [string]$ConnectionMethod + [string]$ConnectionMethod, + + [parameter( + Mandatory = $false, + ValueFromPipelinebyPropertyName = $true + )] + [ValidateScript( { Test-Path -Path $_ -PathType Container -IsValid })] + [string]$Path ) BEGIN { @@ -237,7 +245,7 @@ Ad-Hoc connections require 10.5 if ($PSBoundParameters["ConnectionMethod"] -eq "RDP") { #RDP accept "application/json" response - $Accept = "application/json" + $Accept = "application/octet-stream" } elseif ($PSBoundParameters["ConnectionMethod"] -eq "PSMGW") { @@ -258,8 +266,11 @@ Ad-Hoc connections require 10.5 If ($result) { - #Return PSM Connection Parameters - $result | Add-ObjectDetail -typename "psPAS.CyberArk.Vault.PSM.Connection.RDP" + If (($result | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name) -contains "PSMGWRequest") { + + $result + + } Else { Out-PASFile -InputObject $result -Path $Path } } diff --git a/psPAS/Functions/Platforms/Export-PASPlatform.ps1 b/psPAS/Functions/Platforms/Export-PASPlatform.ps1 index 440c9461..3844113f 100644 --- a/psPAS/Functions/Platforms/Export-PASPlatform.ps1 +++ b/psPAS/Functions/Platforms/Export-PASPlatform.ps1 @@ -11,7 +11,7 @@ function Export-PASPlatform { The name of the platform. .PARAMETER Path - The output zip file to save the platform configuration in. + The folder to export the platform configuration to. .EXAMPLE Export-PASPlatform -PlatformID YourPlatform -Path C:\Platform.zip @@ -34,9 +34,7 @@ function Export-PASPlatform { Mandatory = $true, ValueFromPipelinebyPropertyName = $true )] - [ValidateNotNullOrEmpty()] - [ValidateScript( { Test-Path -Path $_ -PathType Leaf -IsValid })] - [ValidatePattern( '\.zip$' )] + [ValidateScript( { Test-Path -Path $_ -PathType Container -IsValid })] [string]$path ) @@ -59,26 +57,7 @@ function Export-PASPlatform { #if we get a platform byte array if ($result) { - try { - - $output = @{ - Path = $path - Value = $result - Encoding = "Byte" - } - - If ($IsCoreCLR) { - - #amend parameters for splatting if we are in Core - $output.Add("AsByteStream", $true) - $output.Remove("Encoding") - - } - - #write it to a file - Set-Content @output -ErrorAction Stop - - } catch { throw "Error Saving $path" } + Out-PASFile -InputObject $result -Path $path } diff --git a/psPAS/Private/Get-PASResponse.ps1 b/psPAS/Private/Get-PASResponse.ps1 index e799149b..12cf0438 100644 --- a/psPAS/Private/Get-PASResponse.ps1 +++ b/psPAS/Private/Get-PASResponse.ps1 @@ -50,6 +50,18 @@ function Get-PASResponse { #handle content type switch ($ContentType) { + 'application/octet-stream' { + + #'application/octet-stream' is expected for files returned in web requests + if ($($PASResponse | Get-Member | Select-Object -ExpandProperty typename) -eq "System.Byte" ) { + + #return content and headers + $PASResponse = $APIResponse | Select-Object Content, Headers + + } + + } + 'text/html; charset=utf-8' { #text/html response is expected from the Password/Retrieve Uri path. From 03ec1e0a3573f85fe688f26eefb5efc9f38724b1 Mon Sep 17 00:00:00 2001 From: Pete Maan Date: Sun, 30 Jun 2019 21:02:51 +0100 Subject: [PATCH 06/15] :recycle: UPDATE Export-PASPSMRecording Updated `Export-PASPSMRecording`to use `Out-PASFile` to save the recording file. `Get-PASResponse` updated to return the content & header when COntent-Type `application-save` is returned. (expected content type from `Export-PASPSMRecording`. --- Tests/Export-PASPSMRecording.Tests.ps1 | 56 +++++++------------ Tests/Get-PASResponse.Tests.ps1 | 8 +-- .../Monitoring/Export-PASPSMRecording.ps1 | 31 ++-------- psPAS/Private/Get-PASResponse.ps1 | 12 ++++ 4 files changed, 41 insertions(+), 66 deletions(-) diff --git a/Tests/Export-PASPSMRecording.Tests.ps1 b/Tests/Export-PASPSMRecording.Tests.ps1 index e364293a..22e2ecae 100644 --- a/Tests/Export-PASPSMRecording.Tests.ps1 +++ b/Tests/Export-PASPSMRecording.Tests.ps1 @@ -57,22 +57,36 @@ Describe $FunctionName { BeforeEach { - Mock Invoke-PASRestMethod -MockWith { } + Mock Invoke-PASRestMethod -MockWith { + [PSCustomObject]@{ + Content = New-Object Byte[] 512 + Headers = @{"Content-Disposition" = "attachment; filename=FILENAME.zip" } + } + } $InputObj = [pscustomobject]@{ "RecordingID" = "SomeID" - "path" = "$env:Temp\test.avi" + "path" = "$env:Temp" } + Mock Out-PASFile -MockWith { } + } It "throws if path is invalid" { - { $InputObj | Export-PASPlatform -PlatformID SomePlatform -path A:\test.avi } | Should throw + { $InputObj | Export-PASPSMRecording -PlatformID SomePlatform -path A:\test.avi } | Should throw } - It "throws if InputFile resolves to a folder" { - { $InputObj | Export-PASPlatform -PlatformID SomePlatform -path $pwd } | Should throw + It "throws if InputFile resolves to a file" { + + $InputObj = [pscustomobject]@{ + "RecordingID" = "SomeID" + "path" = "$env:Temp\test.avi" + + } + + { $InputObj | Export-PASPSMRecording -PlatformID SomePlatform -path $pwd } | Should throw } It "sends request" { @@ -112,38 +126,6 @@ Describe $FunctionName { } - Context "Output" { - - BeforeEach { - - Mock Invoke-PASRestMethod -MockWith { - - New-Object Byte[] 512 - - } - - $InputObj = [pscustomobject]@{ - "RecordingID" = "SomeID" - "path" = "$env:Temp\test.avi" - } - - } - - it "saves output file" { - $InputObj | Export-PASPSMRecording - Test-Path "$env:Temp\test.avi" | should Be $true - - } - - it "reports error saving outputfile" { - Mock Set-Content -MockWith { throw something } - { $InputObj | Export-PASPSMRecording } | should throw "Error Saving $env:Temp\test.avi" - } - - - - } - } } \ No newline at end of file diff --git a/Tests/Get-PASResponse.Tests.ps1 b/Tests/Get-PASResponse.Tests.ps1 index b00f4fbb..77f012ef 100644 --- a/Tests/Get-PASResponse.Tests.ps1 +++ b/Tests/Get-PASResponse.Tests.ps1 @@ -64,12 +64,12 @@ Describe $FunctionName { $ApplicationSave = New-MockObject -Type Microsoft.PowerShell.Commands.WebResponseObject $ApplicationSave | Add-Member -MemberType NoteProperty -Name StatusCode -Value 200 -Force - $ApplicationSave | Add-Member -MemberType NoteProperty -Name Headers -Value @{ "Content-Type" = 'application/save' } -Force + $ApplicationSave | Add-Member -MemberType NoteProperty -Name Headers -Value @{ "Content-Type" = 'application/save' ; "Content-Disposition" = "attachment; filename=FILENAME.zip" } -Force $ApplicationSave | Add-Member -MemberType NoteProperty -Name Content -Value $([System.Text.Encoding]::Ascii.GetBytes("Expected")) -Force $OctetStream = New-MockObject -Type Microsoft.PowerShell.Commands.WebResponseObject $OctetStream | Add-Member -MemberType NoteProperty -Name StatusCode -Value 200 -Force - $OctetStream | Add-Member -MemberType NoteProperty -Name Headers -Value @{ "Content-Type" = 'application/octet-stream' } -Force + $OctetStream | Add-Member -MemberType NoteProperty -Name Headers -Value @{ "Content-Type" = 'application/octet-stream' ; "Content-Disposition" = "attachment; filename=FILENAME.zip" } -Force $OctetStream | Add-Member -MemberType NoteProperty -Name Content -Value $([System.Text.Encoding]::Ascii.GetBytes("Expected")) -Force } @@ -89,12 +89,12 @@ Describe $FunctionName { It "returns expected application-save value" { $result = Get-PASResponse -APIResponse $ApplicationSave - $([System.Text.Encoding]::ASCII.GetString($result)) | Should Be "Expected" + $([System.Text.Encoding]::ASCII.GetString($result.Content)) | Should Be "Expected" } It "returns expected octet-stream value" { $result = Get-PASResponse -APIResponse $OctetStream - $([System.Text.Encoding]::ASCII.GetString($result)) | Should Be "Expected" + $([System.Text.Encoding]::ASCII.GetString($result.Content)) | Should Be "Expected" } } diff --git a/psPAS/Functions/Monitoring/Export-PASPSMRecording.ps1 b/psPAS/Functions/Monitoring/Export-PASPSMRecording.ps1 index fbfe8bb0..16c1bb14 100644 --- a/psPAS/Functions/Monitoring/Export-PASPSMRecording.ps1 +++ b/psPAS/Functions/Monitoring/Export-PASPSMRecording.ps1 @@ -10,7 +10,7 @@ Saves a specific recorded session to a file Unique ID of the recorded PSM session .PARAMETER Path -The output file path for the recording. +The folder to export the platform configuration to. .EXAMPLE Export-PASPSMRecording -RecordingID 123_45 -path C:\PSMRecording.avi @@ -38,7 +38,7 @@ Minimum CyberArk Version 10.6 ValueFromPipelinebyPropertyName = $true )] [ValidateNotNullOrEmpty()] - [ValidateScript( { Test-Path -Path $_ -PathType Leaf -IsValid})] + [ValidateScript( { Test-Path -Path $_ -PathType Container -IsValid })] [string]$path ) @@ -56,34 +56,15 @@ Minimum CyberArk Version 10.6 #send request to PAS web service $result = Invoke-PASRestMethod -Uri $URI -Method POST -WebSession $Script:WebSession - #if we get a platform byte array - if($result) { + #if we get a byte array + if ($result) { - try { - - $output = @{ - Path = $path - Value = $result - Encoding = "Byte" - } - - If($IsCoreCLR) { - - #amend parameters for splatting if we are in Core - $output.Add("AsByteStream", $true) - $output.Remove("Encoding") - - } - - #write it to a file - Set-Content @output -ErrorAction Stop - - } catch {throw "Error Saving $path"} + Out-PASFile -InputObject $result -Path $path } } #process - END {}#end + END { }#end } \ No newline at end of file diff --git a/psPAS/Private/Get-PASResponse.ps1 b/psPAS/Private/Get-PASResponse.ps1 index 12cf0438..c8f24a79 100644 --- a/psPAS/Private/Get-PASResponse.ps1 +++ b/psPAS/Private/Get-PASResponse.ps1 @@ -62,6 +62,18 @@ function Get-PASResponse { } + 'application/save' { + + #'application/save' is expected for PSM recordings returned in web requests + if ($($PASResponse | Get-Member | Select-Object -ExpandProperty typename) -eq "System.Byte" ) { + + #return content and headers + $PASResponse = $APIResponse | Select-Object Content, Headers + + } + + } + 'text/html; charset=utf-8' { #text/html response is expected from the Password/Retrieve Uri path. From c1787edc0c0f39ece794a2f4ba47a59a2f280286 Mon Sep 17 00:00:00 2001 From: Pete Maan Date: Sun, 30 Jun 2019 22:34:19 +0100 Subject: [PATCH 07/15] Update CHANGELOG.md --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f26abec0..62de02ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,16 @@ _2 years since first commit Anniversary Edition_ - Added SAML authentication option. - Added Shared authentication option - Removed `$SecureMode` & `$AdditionalInfo` parameters. + -`Get-PASPSMConnectionParameter` + - Now saves an RDP file returned from an API request. + - `path` parameter now expects a folder to save the file to. + - Output file is named automatically + - `Export-PASPlatform` + - `path` parameter now expects a folder to save the file to. + - Output file is named automatically + - `Export-PASPSMRecording` + - `path` parameter now expects a folder to save the file to. + - Output file is named automatically - Fixes - `New-PASUser` - Added `ChangePassOnNextLogon` parameter for working with latest API method From 975e5d8dd76b45bfe99624c9391135ddd18a88c6 Mon Sep 17 00:00:00 2001 From: Pete Maan Date: Sun, 30 Jun 2019 22:38:41 +0100 Subject: [PATCH 08/15] :recycle: :fire: UPDATE Get-PASResponse Instead of processing `application/octet-stream` & `application/save` responses separatley, move return of objects specific to the `System.Byte` type. Removed logic related to `New-PASSession` - moving this code into `New-PASSession` (where it belongs). --- Tests/Get-PASResponse.Tests.ps1 | 45 ------------------- psPAS/Private/Get-PASResponse.ps1 | 72 +++++-------------------------- 2 files changed, 10 insertions(+), 107 deletions(-) diff --git a/Tests/Get-PASResponse.Tests.ps1 b/Tests/Get-PASResponse.Tests.ps1 index 77f012ef..64fd8c42 100644 --- a/Tests/Get-PASResponse.Tests.ps1 +++ b/Tests/Get-PASResponse.Tests.ps1 @@ -99,51 +99,6 @@ Describe $FunctionName { } - Context New-PASSession { - - BeforeEach { - - Mock Get-ParentFunction -MockWith { - - [PSCustomObject]@{ - FunctionName = "New-PASSession" - } - - } - - $RandomString = "ZDE0YTY3MzYtNTk5Ni00YjFiLWFhMWUtYjVjMGFhNjM5MmJiOzY0MjY0NkYyRkE1NjY3N0M7MDAwMDAwMDI4ODY3MDkxRDUzMjE3NjcxM0ZBODM2REZGQTA2MTQ5NkFCRTdEQTAzNzQ1Q0JDNkRBQ0Q0NkRBMzRCODcwNjA0MDAwMDAwMDA7" - - $ClassicToken = New-MockObject -Type Microsoft.PowerShell.Commands.WebResponseObject - $ClassicToken | Add-Member -MemberType NoteProperty -Name StatusCode -Value 200 -Force - $ClassicToken | Add-Member -MemberType NoteProperty -Name Headers -Value @{ "Content-Type" = 'application/json; charset=utf-8' } -Force - $ClassicToken | Add-Member -MemberType NoteProperty -Name Content -Value $([PSCustomObject]@{CyberArkLogonResult = $RandomString } | ConvertTo-Json) -Force - - $V10Token = New-MockObject -Type Microsoft.PowerShell.Commands.WebResponseObject - $V10Token | Add-Member -MemberType NoteProperty -Name StatusCode -Value 200 -Force - $V10Token | Add-Member -MemberType NoteProperty -Name Headers -Value @{ "Content-Type" = 'application/json; charset=utf-8' } -Force - $V10Token | Add-Member -MemberType NoteProperty -Name Content -Value $($RandomString | ConvertTo-Json) -Force - - $SharedToken = New-MockObject -Type Microsoft.PowerShell.Commands.WebResponseObject - $SharedToken | Add-Member -MemberType NoteProperty -Name StatusCode -Value 200 -Force - $SharedToken | Add-Member -MemberType NoteProperty -Name Headers -Value @{ "Content-Type" = 'application/json; charset=utf-8' } -Force - $SharedToken | Add-Member -MemberType NoteProperty -Name Content -Value $([PSCustomObject]@{LogonResult = $RandomString } | ConvertTo-Json) -Force - - } - - It "returns expected Classic API Logon Token" { - Get-PASResponse -APIResponse $ClassicToken | Select-Object -ExpandProperty CyberArkLogonResult | Should Be $RandomString - } - - It "returns expected V10 API Logon Token" { - Get-PASResponse -APIResponse $V10Token | Select-Object -ExpandProperty CyberArkLogonResult | Should Be $RandomString - } - - It "returns expected Shared Authentication Logon Token" { - Get-PASResponse -APIResponse $SharedToken | Select-Object -ExpandProperty CyberArkLogonResult | Should Be $RandomString - } - - } - } } \ No newline at end of file diff --git a/psPAS/Private/Get-PASResponse.ps1 b/psPAS/Private/Get-PASResponse.ps1 index c8f24a79..d8fc174f 100644 --- a/psPAS/Private/Get-PASResponse.ps1 +++ b/psPAS/Private/Get-PASResponse.ps1 @@ -10,12 +10,12 @@ function Get-PASResponse { the format required by the functions which initiated the request. .PARAMETER APIResponse - A WebResponseObject, as returned form the PAS API using Invoke-WebRequest + A WebResponseObject, as returned from the PAS API using Invoke-WebRequest .EXAMPLE $WebResponseObject | Get-PASResponse - Parses, if required, and returns, the content property of $WebResponseObject + Parses, if required, and returns, the required properties of $WebResponseObject #> [CmdletBinding()] @@ -30,12 +30,7 @@ function Get-PASResponse { ) - BEGIN { - - #Get the name of the first function in the invocation stack - $CommandOrigin = Get-ParentFunction -Scope 3 | Select-Object -ExpandProperty FunctionName - - }#begin + BEGIN { }#begin PROCESS { @@ -50,30 +45,6 @@ function Get-PASResponse { #handle content type switch ($ContentType) { - 'application/octet-stream' { - - #'application/octet-stream' is expected for files returned in web requests - if ($($PASResponse | Get-Member | Select-Object -ExpandProperty typename) -eq "System.Byte" ) { - - #return content and headers - $PASResponse = $APIResponse | Select-Object Content, Headers - - } - - } - - 'application/save' { - - #'application/save' is expected for PSM recordings returned in web requests - if ($($PASResponse | Get-Member | Select-Object -ExpandProperty typename) -eq "System.Byte" ) { - - #return content and headers - $PASResponse = $APIResponse | Select-Object Content, Headers - - } - - } - 'text/html; charset=utf-8' { #text/html response is expected from the Password/Retrieve Uri path. @@ -110,40 +81,17 @@ function Get-PASResponse { #Create Return Object from Returned JSON $PASResponse = ConvertFrom-Json -InputObject $APIResponse.Content - #Handle Logon Token Return - If ($CommandOrigin -eq "New-PASSession") { - - #Classic API returns the auth token in the CyberArkLogonResult property - #Other auth methods/endpoints need further processing to assign the auth - #token to a property named CyberArkLogonResult. - - #Version 10 - If ($PASResponse.length -eq 180) { - - #If calling function is New-PASSession, and result is a 180 character string - #Create a new object and assign the token to the CyberArkLogonResult property. - $PASResponse = [PSCustomObject]@{ - - CyberArkLogonResult = $PASResponse - - } - - } - - #Shared Auth - If ($PASResponse.LogonResult) { - - #If calling function is New-PASSession, and result has a LogonResult property. - #Create a new object and assign the LogonResult value to the CyberArkLogonResult property. - $PASResponse = [PSCustomObject]@{ + } - CyberArkLogonResult = $PASResponse.LogonResult + default { - } + # Byte Array expected for files to be saved + if ($($PASResponse | Get-Member | Select-Object -ExpandProperty typename) -eq "System.Byte" ) { - } + #return content and headers + $PASResponse = $APIResponse | Select-Object Content, Headers - #?SAML Auth? + #! to be passed to `Out-PASFile` } From 8f9a8eb5105c55ca117b061910cab858747daec9 Mon Sep 17 00:00:00 2001 From: Pete Maan Date: Sun, 30 Jun 2019 22:40:12 +0100 Subject: [PATCH 09/15] :recycle: UPDATE New-PASSession Moved logic related to obtaining token from API response to here from `Get-PASResponse`. --- .../Authentication/New-PASSession.ps1 | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/psPAS/Functions/Authentication/New-PASSession.ps1 b/psPAS/Functions/Authentication/New-PASSession.ps1 index 2be64af6..ee18ede9 100644 --- a/psPAS/Functions/Authentication/New-PASSession.ps1 +++ b/psPAS/Functions/Authentication/New-PASSession.ps1 @@ -376,9 +376,41 @@ #If Logon Result If ($PASSession) { + #Version 10 + If ($PASSession.length -eq 180) { + + #V10 Auth Token. + + $CyberArkLogonResult = $PASSession + + } + + #Shared Auth + ElseIf ($PASSession.LogonResult) { + + #Shared Auth LogonResult. + + $CyberArkLogonResult = $PASSession.LogonResult + + + } + + #Classic + Else { + + #Classic CyberArkLogonResult + + $CyberArkLogonResult = $PASSession.CyberArkLogonResult + + } + + #?SAML Auth? + + #BaseURI set in Module Scope Set-Variable -Name BaseURI -Value "$BaseURI/$PVWAAppName" -Scope Script - $Script:WebSession.Headers["Authorization"] = [string]$($PASSession.CyberArkLogonResult) + #Auth token added to WebSession + $Script:WebSession.Headers["Authorization"] = [string]$CyberArkLogonResult #Initial Value for Version variable [System.Version]$Version = "0.0" @@ -395,6 +427,7 @@ } + #Version information available in module scope. Set-Variable -Name ExternalVersion -Value $Version -Scope Script } From 8110f53e29677b32c60eec0838cf048a05b3c29e Mon Sep 17 00:00:00 2001 From: Pete Maan Date: Sun, 30 Jun 2019 22:42:09 +0100 Subject: [PATCH 10/15] :recycle: UPDATE Get-PASPSMConnectionParameter Changed ValidateScript, removed PathType check. --- .../Connections/Get-PASPSMConnectionParameter.ps1 | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/psPAS/Functions/Connections/Get-PASPSMConnectionParameter.ps1 b/psPAS/Functions/Connections/Get-PASPSMConnectionParameter.ps1 index 76c71e14..f578f01a 100644 --- a/psPAS/Functions/Connections/Get-PASPSMConnectionParameter.ps1 +++ b/psPAS/Functions/Connections/Get-PASPSMConnectionParameter.ps1 @@ -182,7 +182,7 @@ Ad-Hoc connections require 10.5 Mandatory = $false, ValueFromPipelinebyPropertyName = $true )] - [ValidateScript( { Test-Path -Path $_ -PathType Container -IsValid })] + [ValidateScript( { Test-Path -Path $_ -IsValid })] [string]$Path ) @@ -268,9 +268,15 @@ Ad-Hoc connections require 10.5 If (($result | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name) -contains "PSMGWRequest") { + #Return PSM GW URL Details $result - } Else { Out-PASFile -InputObject $result -Path $Path } + } Else { + + #Save the RDP file to disk + Out-PASFile -InputObject $result -Path $Path + + } } From bcf92098d21fd4e43e9d91ac2e58b0c1045da345 Mon Sep 17 00:00:00 2001 From: Pete Maan Date: Sun, 30 Jun 2019 22:42:54 +0100 Subject: [PATCH 11/15] :recycle: UPDATE Export-PASPSMRecording Changed ValidateScript, removed PathType check. --- .../Monitoring/Export-PASPSMRecording.ps1 | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/psPAS/Functions/Monitoring/Export-PASPSMRecording.ps1 b/psPAS/Functions/Monitoring/Export-PASPSMRecording.ps1 index 16c1bb14..2557e7a2 100644 --- a/psPAS/Functions/Monitoring/Export-PASPSMRecording.ps1 +++ b/psPAS/Functions/Monitoring/Export-PASPSMRecording.ps1 @@ -1,30 +1,25 @@ function Export-PASPSMRecording { <# -.SYNOPSIS -Saves a PSM Recording + .SYNOPSIS + Saves a PSM Recording -.DESCRIPTION -Saves a specific recorded session to a file + .DESCRIPTION + Saves a specific recorded session to a file -.PARAMETER RecordingID -Unique ID of the recorded PSM session + .PARAMETER RecordingID + Unique ID of the recorded PSM session -.PARAMETER Path -The folder to export the platform configuration to. + .PARAMETER Path + The folder to export the PSM recording to. -.EXAMPLE -Export-PASPSMRecording -RecordingID 123_45 -path C:\PSMRecording.avi + .EXAMPLE + Export-PASPSMRecording -RecordingID 123_45 -path C:\PSMRecording.avi -Saves PSM Recording with Id 123_45 to C:\PSMRecording.avi + Saves PSM Recording with Id 123_45 to C:\PSMRecording.avi -.INPUTS -All parameters can be piped by property name - -.OUTPUTS - -.NOTES -Minimum CyberArk Version 10.6 -#> + .NOTES + Minimum CyberArk Version 10.6 + #> [CmdletBinding()] param( [parameter( @@ -38,7 +33,7 @@ Minimum CyberArk Version 10.6 ValueFromPipelinebyPropertyName = $true )] [ValidateNotNullOrEmpty()] - [ValidateScript( { Test-Path -Path $_ -PathType Container -IsValid })] + [ValidateScript( { Test-Path -Path $_ -IsValid })] [string]$path ) From c7464bef710b62de3e33ffd00bd9305ff808991c Mon Sep 17 00:00:00 2001 From: Pete Maan Date: Sun, 30 Jun 2019 22:44:01 +0100 Subject: [PATCH 12/15] :recycle: UPDATE Export-PASPlatform Changed ValidateScript, removed PathType check. --- psPAS/Functions/Platforms/Export-PASPlatform.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psPAS/Functions/Platforms/Export-PASPlatform.ps1 b/psPAS/Functions/Platforms/Export-PASPlatform.ps1 index 3844113f..69b31354 100644 --- a/psPAS/Functions/Platforms/Export-PASPlatform.ps1 +++ b/psPAS/Functions/Platforms/Export-PASPlatform.ps1 @@ -34,7 +34,7 @@ function Export-PASPlatform { Mandatory = $true, ValueFromPipelinebyPropertyName = $true )] - [ValidateScript( { Test-Path -Path $_ -PathType Container -IsValid })] + [ValidateScript( { Test-Path -Path $_ -IsValid })] [string]$path ) From babdac5335663554da679282152c5de0e63604bd Mon Sep 17 00:00:00 2001 From: Pete Maan Date: Sun, 30 Jun 2019 22:44:59 +0100 Subject: [PATCH 13/15] :recycle: UPDATE Out-PASFile Added more robust `throw` --- psPAS/Private/Out-PASFile.ps1 | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/psPAS/Private/Out-PASFile.ps1 b/psPAS/Private/Out-PASFile.ps1 index ff97411b..9a0ff875 100644 --- a/psPAS/Private/Out-PASFile.ps1 +++ b/psPAS/Private/Out-PASFile.ps1 @@ -37,9 +37,6 @@ function Out-PASFile { Process { - #Get filename from Content-Disposition Header element. - $FileName = ($InputObject.Headers["Content-Disposition"] -split "filename=")[1] -replace '"' - If (-not ($Path)) { #Default to TEMP if path not provided @@ -47,6 +44,9 @@ function Out-PASFile { } + #Get filename from Content-Disposition Header element. + $FileName = ($InputObject.Headers["Content-Disposition"] -split "filename=")[1] -replace '"' + #Define output path $OutputPath = Join-Path $Path $FileName @@ -75,7 +75,22 @@ function Out-PASFile { #return file object Get-Item -Path $OutputPath - } catch { throw "Error Saving $OutputPath" } + } catch { + + #throw the error + $PSCmdlet.ThrowTerminatingError( + + [System.Management.Automation.ErrorRecord]::new( + + "Error Saving $OutputPath", + $null, + [System.Management.Automation.ErrorCategory]::NotSpecified, + $PSItem + + ) + + ) + } } From 0891d85038575335b134769765c4c7cbaf7ba8a2 Mon Sep 17 00:00:00 2001 From: Pete Maan Date: Sun, 30 Jun 2019 23:19:57 +0100 Subject: [PATCH 14/15] :white_check_mark: :100: Quick update to cover off new code paths in `New-PASSession` to deal with expected logon tokens for different authentication types. --- Tests/New-PASSession.Tests.ps1 | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Tests/New-PASSession.Tests.ps1 b/Tests/New-PASSession.Tests.ps1 index 5cf8e69e..63bc7e34 100644 --- a/Tests/New-PASSession.Tests.ps1 +++ b/Tests/New-PASSession.Tests.ps1 @@ -163,6 +163,15 @@ Describe $FunctionName { It "sends request to expected v10 URL for CyberArk Authentication" { + $RandomString = "ZDE0YTY3MzYtNTk5Ni00YjFiLWFhMWUtYjVjMGFhNjM5MmJiOzY0MjY0NkYyRkE1NjY3N0M7MDAwMDAwMDI4ODY3MDkxRDUzMjE3NjcxM0ZBODM2REZGQTA2MTQ5NkFCRTdEQTAzNzQ1Q0JDNkRBQ0Q0NkRBMzRCODcwNjA0MDAwMDAwMDA7" + + + Mock Invoke-PASRestMethod -MockWith { + + $RandomString + + } + $Credentials | New-PASSession -BaseURI "https://P_URI" -type CyberArk Assert-MockCalled Invoke-PASRestMethod -ParameterFilter { @@ -242,6 +251,12 @@ Describe $FunctionName { It "sends request to expected URL for Shared Authentication" { + Mock Invoke-PASRestMethod -MockWith { + [PSCustomObject]@{ + "LogonResult" = "AAAAAAA\\\REEEAAAAALLLLYYYYY\\\\LOOOOONNNNGGGGG\\\ACCCCCEEEEEEEESSSSSSS\\\\\\TTTTTOOOOOKKKKKEEEEEN" + } + } + New-PASSession -BaseURI "https://P_URI" -UseSharedAuthentication Assert-MockCalled Invoke-PASRestMethod -ParameterFilter { From cba49783748de8d4c0a302e850b8648c9943b694 Mon Sep 17 00:00:00 2001 From: Pete Maan Date: Sun, 30 Jun 2019 23:20:45 +0100 Subject: [PATCH 15/15] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62de02ec..c44e9a74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # psPAS -## **3.0.0** (June 30th 2019) +## **3.0.0** (July 1st 2019) _2 years since first commit Anniversary Edition_