diff --git a/DscResources/MSFT_WindowsProcess/MSFT_WindowsProcess.psm1 b/DscResources/MSFT_WindowsProcess/MSFT_WindowsProcess.psm1 new file mode 100644 index 0000000..6e43653 --- /dev/null +++ b/DscResources/MSFT_WindowsProcess/MSFT_WindowsProcess.psm1 @@ -0,0 +1,1486 @@ +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +Import-Module -Name (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) ` + -ChildPath 'CommonResourceHelper.psm1') + +# Localized messages for verbose and error statements in this resource +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_WindowsProcess' + +<# + .SYNOPSIS + Retrieves the current state of the Windows process(es) with the specified + executable and arguments. + + If more than one process is found, only the information of the first process is retrieved. + ProcessCount will contain the actual number of processes that were found. + + .PARAMETER Path + The path to the process executable. If this is the file name of the executable + (not the fully qualified path), the DSC resource will search the environment Path variable + ($env:Path) to find the executable file. If the value of this property is a fully qualified + path, DSC will not use the Path environment variable to find the file, and will throw an + error if the path does not exist. Relative paths are not allowed. + + .PARAMETER Arguments + The arguments to the process as a single string. + + .PARAMETER Credential + The credential of the user account to start the process under. +#> +function Get-TargetResource +{ + [OutputType([Hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Path, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $Arguments, + + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + Write-Verbose -Message ($script:localizedData.GetTargetResourceStartMessage -f $Path) + + $Path = Expand-Path -Path $Path + + $getProcessCimInstanceArguments = @{ + Path = $Path + Arguments = $Arguments + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $getProcessCimInstanceArguments['Credential'] = $Credential + } + + $processCimInstance = @( Get-ProcessCimInstance @getProcessCimInstanceArguments ) + + $processToReturn = @{} + + if ($processCimInstance.Count -eq 0) + { + $processToReturn = @{ + Path = $Path + Arguments = $Arguments + Ensure ='Absent' + } + } + else + { + $processId = $processCimInstance[0].ProcessId + $getProcessResult = Get-Process -ID $processId + + $processToReturn = @{ + Path = $Path + Arguments = $Arguments + PagedMemorySize = $getProcessResult.PagedMemorySize64 + NonPagedMemorySize = $getProcessResult.NonpagedSystemMemorySize64 + VirtualMemorySize = $getProcessResult.VirtualMemorySize64 + HandleCount = $getProcessResult.HandleCount + Ensure = 'Present' + ProcessId = $processId + ProcessCount = $processCimInstance.Count + } + } + + Write-Verbose -Message ($script:localizedData.GetTargetResourceEndMessage -f $Path) + return $processToReturn +} + +<# + .SYNOPSIS + Sets the Windows process with the specified executable path and arguments + to the specified state. + + If multiple process are found, the specified state will be set for all of them. + + .PARAMETER Path + The path to the process executable. If this is the file name of the executable + (not the fully qualified path), the DSC resource will search the environment Path variable + ($env:Path) to find the executable file. If the value of this property is a fully qualified + path, DSC will not use the Path environment variable to find the file, and will throw an + error if the path does not exist. Relative paths are not allowed. + + .PARAMETER Arguments + The arguments to the process as a single string. + + .PARAMETER Credential + The credential of the user account to start the process under. + + .PARAMETER Ensure + Specifies whether or not the process should exist. + To start or modify a process, set this property to Present. + To stop a process, set this property to Absent. + The default value is Present. + + .PARAMETER StandardOutputPath + The file path to write the standard output to. Any existing file at this path + will be overwritten.This property cannot be specified at the same time as Credential + when running the process as a local user. + + .PARAMETER StandardErrorPath + The file path to write the standard error output to. Any existing file at this path + will be overwritten. + + .PARAMETER StandardInputPath + The file path to get standard input from. This property cannot be specified at the + same time as Credential when running the process as a local user. + + .PARAMETER WorkingDirectory + The file path to use as the working directory for the process. Any existing file + at this path will be overwritten. This property cannot be specified at the same time + as Credential when running the process as a local user. +#> +function Set-TargetResource +{ + [CmdletBinding(SupportsShouldProcess = $true)] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Path, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $Arguments, + + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + [ValidateSet('Present', 'Absent')] + [String] + $Ensure = 'Present', + + [String] + $StandardOutputPath, + + [String] + $StandardErrorPath, + + [String] + $StandardInputPath, + + [String] + $WorkingDirectory + ) + + Write-Verbose -Message ($script:localizedData.SetTargetResourceStartMessage -f $Path) + + Assert-PsDscContextNotRunAsUser + + $Path = Expand-Path -Path $Path + + $getProcessCimInstanceArguments = @{ + Path = $Path + Arguments = $Arguments + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $getProcessCimInstanceArguments['Credential'] = $Credential + } + + $processCimInstance = @( Get-ProcessCimInstance @getProcessCimInstanceArguments ) + + if ($Ensure -eq 'Absent') + { + $assertHashtableParams = @{ + Hashtable = $PSBoundParameters + Key = @( 'StandardOutputPath', + 'StandardErrorPath', + 'StandardInputPath', + 'WorkingDirectory' ) + } + Assert-HashtableDoesNotContainKey @assertHashtableParams + + $whatIfShouldProcess = $PSCmdlet.ShouldProcess($Path, $script:localizedData.StoppingProcessWhatif) + if ($processCimInstance.Count -gt 0 -and $whatIfShouldProcess) + { + # If there are multiple process Ids, all will be included to be stopped + $processIds = $processCimInstance.ProcessId + + # Redirecting error output to standard output while we try to stop the processes + $stopProcessError = Stop-Process -Id $processIds -Force 2>&1 + + if ($null -eq $stopProcessError) + { + Write-Verbose -Message ($script:localizedData.ProcessesStopped -f $Path, ($processIds -join ',')) + } + else + { + $errorMessage = ($script:localizedData.ErrorStopping -f $Path, + ($processIds -join ','), + ($stopProcessError | Out-String)) + + New-InvalidOperationException -Message $errorMessage + } + <# + Before returning from Set-TargetResource we have to ensure a subsequent + Test-TargetResource is going to work + #> + if (-not (Wait-ProcessCount -ProcessSettings $getProcessCimInstanceArguments -ProcessCount 0)) + { + $message = $script:localizedData.ErrorStopping -f $Path, ($processIds -join ','), + $script:localizedData.FailureWaitingForProcessesToStop + + New-InvalidOperationException -Message $message + } + } + else + { + Write-Verbose -Message ($script:localizedData.ProcessAlreadyStopped -f $Path) + } + } + # Ensure = 'Present' + else + { + $shouldBeRootedPathArguments = @( 'StandardInputPath', + 'WorkingDirectory', + 'StandardOutputPath', + 'StandardErrorPath' ) + + foreach ($shouldBeRootedPathArgument in $shouldBeRootedPathArguments) + { + if (-not [String]::IsNullOrEmpty($PSBoundParameters[$shouldBeRootedPathArgument])) + { + $assertPathArgumentRootedParams = @{ + PathArgumentName = $shouldBeRootedPathArgument + PathArgument = $PSBoundParameters[$shouldBeRootedPathArgument] + } + Assert-PathArgumentRooted @assertPathArgumentRootedParams + } + } + + $shouldExistPathArguments = @( 'StandardInputPath', 'WorkingDirectory' ) + + foreach ($shouldExistPathArgument in $shouldExistPathArguments) + { + if (-not [String]::IsNullOrEmpty($PSBoundParameters[$shouldExistPathArgument])) + { + $assertPathArgumentValidParams = @{ + PathArgumentName = $shouldExistPathArgument + PathArgument = $PSBoundParameters[$shouldExistPathArgument] + } + Assert-PathArgumentValid @assertPathArgumentValidParams + } + } + + if ($processCimInstance.Count -eq 0) + { + $startProcessArguments = @{ + FilePath = $Path + } + + $startProcessOptionalArgumentMap = @{ + Credential = 'Credential' + RedirectStandardOutput = 'StandardOutputPath' + RedirectStandardError = 'StandardErrorPath' + RedirectStandardInput = 'StandardInputPath' + WorkingDirectory = 'WorkingDirectory' + } + + foreach ($startProcessOptionalArgumentName in $startProcessOptionalArgumentMap.Keys) + { + $parameterKey = $startProcessOptionalArgumentMap[$startProcessOptionalArgumentName] + $parameterValue = $PSBoundParameters[$parameterKey] + + if (-not [String]::IsNullOrEmpty($parameterValue)) + { + $startProcessArguments[$startProcessOptionalArgumentName] = $parameterValue + } + } + + if (-not [String]::IsNullOrEmpty($Arguments)) + { + $startProcessArguments['ArgumentList'] = $Arguments + } + + if ($PSCmdlet.ShouldProcess($Path, $script:localizedData.StartingProcessWhatif)) + { + <# + Start-Process calls .net Process.Start() + If -Credential is present Process.Start() uses win32 api CreateProcessWithLogonW + http://msdn.microsoft.com/en-us/library/0w4h05yb(v=vs.110).aspx + CreateProcessWithLogonW cannot be called as LocalSystem user. + Details http://msdn.microsoft.com/en-us/library/windows/desktop/ms682431(v=vs.85).aspx + (section Remarks/Windows XP with SP2 and Windows Server 2003) + + In this case we call another api. + #> + if (($PSBoundParameters.ContainsKey('Credential')) -and (Test-IsRunFromLocalSystemUser)) + { + # Throw an exception if any of the below parameters are included with Credential passed + foreach ($key in @('StandardOutputPath','StandardInputPath','WorkingDirectory')) + { + if ($PSBoundParameters.Keys -contains $key) + { + $newInvalidArgumentExceptionParams = @{ + ArgumentName = $key + Message = $script:localizedData.ErrorParametersNotSupportedWithCredential + } + New-InvalidArgumentException @newInvalidArgumentExceptionParams + } + } + try + { + Start-ProcessAsLocalSystemUser -Path $Path -Arguments $Arguments -Credential $Credential + } + catch + { + throw (New-Object -TypeName 'System.Management.Automation.ErrorRecord' ` + -ArgumentList @( $_.Exception, 'Win32Exception', 'OperationStopped', $null )) + } + } + # Credential not passed in or running from a LocalSystem + else + { + try + { + Start-Process @startProcessArguments + } + catch [System.Exception] + { + $errorMessage = ($script:localizedData.ErrorStarting -f $Path, $_.Exception.Message) + + New-InvalidOperationException -Message $errorMessage + } + } + + Write-Verbose -Message ($script:localizedData.ProcessesStarted -f $Path) + + # Before returning from Set-TargetResource we have to ensure a subsequent Test-TargetResource is going to work + if (-not (Wait-ProcessCount -ProcessSettings $getProcessCimInstanceArguments -ProcessCount 1)) + { + $message = $script:localizedData.ErrorStarting -f $Path, + $script:localizedData.FailureWaitingForProcessesToStart + + New-InvalidOperationException -Message $message + } + } + } + else + { + Write-Verbose -Message ($script:localizedData.ProcessAlreadyStarted -f $Path) + } + } + + Write-Verbose -Message ($script:localizedData.SetTargetResourceEndMessage -f $Path) +} + +<# + .SYNOPSIS + Tests if the Windows process with the specified executable path and arguments is in + the specified state. + + .PARAMETER Path + The path to the process executable. If this is the file name of the executable + (not the fully qualified path), the DSC resource will search the environment Path variable + ($env:Path) to find the executable file. If the value of this property is a fully qualified + path, DSC will not use the Path environment variable to find the file, and will throw an + error if the path does not exist. Relative paths are not allowed. + + .PARAMETER Arguments + The arguments to the process as a single string. + + .PARAMETER Credential + The credential of the user account the process should be running under. + + .PARAMETER Ensure + Specifies whether or not the process should exist. + If the process should exist, set this property to Present. + If the process should not exist, set this property to Absent. + The default value is Present. + + .PARAMETER StandardOutputPath + Not used in Test-TargetResource. + + .PARAMETER StandardErrorPath + Not used in Test-TargetResource. + + .PARAMETER StandardInputPath + Not used in Test-TargetResource. + + .PARAMETER WorkingDirectory + Not used in Test-TargetResource. +#> +function Test-TargetResource +{ + [OutputType([Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Path, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $Arguments, + + [ValidateNotNullOrEmpty()] + [PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + [ValidateSet('Present', 'Absent')] + [String] + $Ensure = 'Present', + + [String] + $StandardOutputPath, + + [String] + $StandardErrorPath, + + [String] + $StandardInputPath, + + [String] + $WorkingDirectory + ) + + Write-Verbose -Message ($script:localizedData.TestTargetResourceStartMessage -f $Path) + + Assert-PsDscContextNotRunAsUser + + $Path = Expand-Path -Path $Path + + $getProcessCimInstanceArguments = @{ + Path = $Path + Arguments = $Arguments + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $getProcessCimInstanceArguments['Credential'] = $Credential + } + + $processCimInstances = @( Get-ProcessCimInstance @getProcessCimInstanceArguments ) + + Write-Verbose -Message ($script:localizedData.TestTargetResourceEndMessage -f $Path) + + if ($Ensure -eq 'Absent') + { + return ($processCimInstances.Count -eq 0) + } + else + { + return ($processCimInstances.Count -gt 0) + } +} + +<# + .SYNOPSIS + Expands a relative leaf path into a full, rooted path. Throws an invalid argument exception + if the path is not valid. + + .PARAMETER Path + The relative leaf path to expand. +#> +function Expand-Path +{ + [OutputType([String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Path + ) + + $Path = [Environment]::ExpandEnvironmentVariables($Path) + + # Check to see if the path is rooted. If so, return it as is. + if ([IO.Path]::IsPathRooted($Path)) + { + if (-not (Test-Path -Path $Path -PathType 'Leaf')) + { + New-InvalidArgumentException -ArgumentName 'Path' -Message ($script:localizedData.FileNotFound -f $Path) + } + + return $Path + } + + # Check to see if the path to the file exists in the current location. If so, return the full rooted path. + $rootedPath = [System.IO.Path]::GetFullPath($Path) + if ([System.IO.File]::Exists($rootedPath)) + { + return $rootedPath + } + + # If the path is not found, throw an exception + New-InvalidArgumentException -ArgumentName 'Path' -Message ($script:localizedData.FileNotFound -f $Path) +} + +<# + .SYNOPSIS + Retrieves any process CIM instance objects that match the given path, arguments, and credential. + + .PARAMETER Path + The executable path of the process to retrieve. + + .PARAMETER Arguments + The arguments of the process to retrieve as a single string. + + .PARAMETER Credential + The credential of the user account of the process to retrieve + + .PARAMETER UseGetCimInstanceThreshold + If the number of processes returned by the Get-Process method is greater than or equal to + this value, this function will retrieve all processes at the executable path. This will + help the function execute faster. Otherwise, this function will retrieve each process + CIM instance with the process IDs retrieved from Get-Process. +#> +function Get-ProcessCimInstance +{ + [OutputType([CimInstance[]])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Path, + + [String] + $Arguments, + + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + [ValidateRange(0, [Int]::MaxValue)] + [Int] + $UseGetCimInstanceThreshold = 8 + ) + + $processName = [IO.Path]::GetFileNameWithoutExtension($Path) + + $getProcessResult = @( Get-Process -Name $processName -ErrorAction 'SilentlyContinue' ) + + $processCimInstances = @() + + if ($getProcessResult.Count -ge $UseGetCimInstanceThreshold) + { + + $escapedPathForWqlFilter = ConvertTo-EscapedStringForWqlFilter -FilterString $Path + $wqlFilter = "ExecutablePath = '$escapedPathForWqlFilter'" + + $processCimInstances = Get-CimInstance -ClassName 'Win32_Process' -Filter $wqlFilter + } + else + { + foreach ($process in $getProcessResult) + { + if ($process.Path -ieq $Path) + { + Write-Verbose -Message ($script:localizedData.VerboseInProcessHandle -f $process.Id) + $getCimInstanceParams = @{ + ClassName = 'Win32_Process' + Filter = "ProcessId = $($process.Id)" + ErrorAction = 'SilentlyContinue' + } + $processCimInstances += Get-CimInstance @getCimInstanceParams + } + } + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $splitCredentialResult = Split-Credential -Credential $Credential + $domain = $splitCredentialResult.Domain + $userName = $splitCredentialResult.UserName + $processesWithCredential = @() + + foreach ($process in $processCimInstances) + { + if ((Get-ProcessOwner -Process $process) -eq "$domain\$userName") + { + $processesWithCredential += $process + } + } + $processCimInstances = $processesWithCredential + } + + if ($null -eq $Arguments) + { + $Arguments = [String]::Empty + } + + $processesWithMatchingArguments = @() + + foreach ($process in $processCimInstances) + { + if ((Get-ArgumentsFromCommandLineInput -CommandLineInput $process.CommandLine) -eq $Arguments) + { + $processesWithMatchingArguments += $process + } + } + + return $processesWithMatchingArguments +} + +<# + .SYNOPSIS + Converts a string to an escaped string to be used in a WQL filter such as the one passed in + the Filter parameter of Get-WmiObject. + + .PARAMETER FilterString + The string to convert. +#> +function ConvertTo-EscapedStringForWqlFilter +{ + [OutputType([String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $FilterString + ) + + return $FilterString.Replace("\","\\").Replace('"','\"').Replace("'","\'") +} + +<# + .SYNOPSIS + Retrieves the owner of a Process. + + .PARAMETER Process + The Process to retrieve the owner of. + + .NOTES + If the process was killed by the time this function is called, this function will throw a + WMIMethodException with the message "Not found". +#> +function Get-ProcessOwner +{ + [OutputType([String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [Object] + $Process + ) + + $owner = Get-ProcessOwnerCimInstance -Process $Process + + if ($null -ne $owner.Domain) + { + return ($owner.Domain + '\' + $owner.User) + } + else + { + # return the default domain + return ($env:computerName + '\' + $owner.User) + } +} + +<# + .SYNOPSIS + Wrapper function to retrieve the CIM instance of the owner of a process + + .PARAMETER Process + The process to retrieve the CIM instance of the owner of. + + .NOTES + If the process was killed by the time this function is called, this function will throw a + WMIMethodException with the message "Not found". +#> +function Get-ProcessOwnerCimInstance +{ + [OutputType([CimInstance])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [Object] + $Process + ) + + return Invoke-CimMethod -InputObject $Process -MethodName 'GetOwner' +} + +<# + .SYNOPSIS + Retrieves the 'arguments' part of command line input. + + .PARAMETER CommandLineInput + The command line input to retrieve the arguments from. + + .EXAMPLE + Get-ArgumentsFromCommandLineInput -CommandLineInput 'C:\temp\a.exe X Y Z' + Returns 'X Y Z'. +#> +function Get-ArgumentsFromCommandLineInput +{ + [OutputType([String])] + [CmdletBinding()] + param + ( + [String] + $CommandLineInput + ) + + if ([String]::IsNullOrWhitespace($CommandLineInput)) + { + return [String]::Empty + } + + $CommandLineInput = $CommandLineInput.Trim() + + if ($CommandLineInput.StartsWith('"')) + { + $endOfCommandChar = [Char]'"' + } + else + { + $endOfCommandChar = [Char]' ' + } + + $endofCommandIndex = $CommandLineInput.IndexOf($endOfCommandChar, 1) + + if ($endofCommandIndex -eq -1) + { + return [String]::Empty + } + + return $CommandLineInput.Substring($endofCommandIndex + 1).Trim() +} + +<# + .SYNOPSIS + Throws an invalid argument exception if the given hashtable contains the given key(s). + + .PARAMETER Hashtable + The hashtable to check the keys of. + + .PARAMETER Key + The key(s) that should not be in the hashtable. +#> +function Assert-HashtableDoesNotContainKey +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [Hashtable] + $Hashtable, + + [Parameter(Mandatory = $true)] + [String[]] + $Key + ) + + foreach ($keyName in $Key) + { + if ($Hashtable.ContainsKey($keyName)) + { + New-InvalidArgumentException -ArgumentName $keyName ` + -Message ($script:localizedData.ParameterShouldNotBeSpecified -f $keyName) + } + } +} + +<# + .SYNOPSIS + Waits for the given amount of time for the given number of processes with the given settings + to be running. If not all processes are running by 'WaitTime', the function returns + false, otherwise it returns true. + + .PARAMETER ProcessSettings + The settings for the running process(es) that we're getting the count of. + + .PARAMETER ProcessCount + The number of processes running to wait for. + + .PARAMETER WaitTime + The amount of milliseconds to wait for all processes to be running. + Default is 2000. +#> +function Wait-ProcessCount +{ + [OutputType([Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Hashtable] + $ProcessSettings, + + [Parameter(Mandatory = $true)] + [ValidateRange(0, [Int]::MaxValue)] + [Int] + $ProcessCount, + + [Int] + $WaitTime = 200000 + ) + + $startTime = [DateTime]::Now + + do + { + $actualProcessCount = @( Get-ProcessCimInstance @ProcessSettings ).Count + } while ($actualProcessCount -ne $ProcessCount -and ([DateTime]::Now - $startTime).TotalMilliseconds -lt $WaitTime) + + return $actualProcessCount -eq $ProcessCount +} + +<# + .SYNOPSIS + Throws an error if the given path argument is not rooted. + + .PARAMETER PathArgumentName + The name of the path argument that should be rooted. + + .PARAMETER PathArgument + The path arguments that should be rooted. +#> +function Assert-PathArgumentRooted +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $PathArgumentName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $PathArgument + ) + + if (-not ([IO.Path]::IsPathRooted($PathArgument))) + { + $message = $script:localizedData.PathShouldBeAbsolute -f $PathArgumentName, $PathArgument + + New-InvalidArgumentException -ArgumentName 'Path' ` + -Message $message + } +} + +<# + .SYNOPSIS + Throws an error if the given path argument does not exist. + + .PARAMETER PathArgumentName + The name of the path argument that should exist. + + .PARAMETER PathArgument + The path argument that should exist. +#> +function Assert-PathArgumentValid +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $PathArgumentName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $PathArgument + ) + + if (-not (Test-Path -Path $PathArgument)) + { + $message = $script:localizedData.PathShouldExist -f $PathArgument, $PathArgumentName + + New-InvalidArgumentException -ArgumentName 'Path' ` + -Message $message + } +} + +<# + .SYNOPSIS + Tests if the current user is from the local system. +#> +function Test-IsRunFromLocalSystemUser +{ + [OutputType([Boolean])] + [CmdletBinding()] + param () + + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = New-Object -TypeName Security.Principal.WindowsPrincipal -ArgumentList $identity + + return $principal.Identity.IsSystem +} + +<# + .SYNOPSIS + Starts the process with the given credential when the user is a local system user. + + .PARAMETER Path + The path to the process executable. + + .PARAMETER Arguments + Indicates a string of arguments to pass to the process as-is. + + .PARAMETER Credential + Indicates the credential for starting the process. +#> +function Start-ProcessAsLocalSystemUser +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Path, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $Arguments, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + $splitCredentialResult = Split-Credential -Credential $Credential + + <# + Internally we use win32 api LogonUser() with + dwLogonType == LOGON32_LOGON_NETWORK_CLEARTEXT. + + It grants the process ability for second-hop. + #> + Import-DscNativeMethods + + [PSDesiredStateConfiguration.NativeMethods]::CreateProcessAsUser( "$Path $Arguments", $splitCredentialResult.Domain, + $splitCredentialResult.UserName, $Credential.Password, + $false, [Ref]$null ) +} + +<# + .SYNOPSIS + Splits a credential into a username and domain without calling GetNetworkCredential. + Calls to GetNetworkCredential expose the password as plain text in memory. + + .PARAMETER Credential + The credential to pull the username and domain out of. + + .NOTES + Supported formats: DOMAIN\username, username@domain +#> +function Split-Credential +{ + [OutputType([Hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + $wrongFormat = $false + + if ($Credential.UserName.Contains('\')) + { + $credentialSegments = $Credential.UserName.Split('\') + + if ($credentialSegments.Length -gt 2) + { + # i.e. domain\user\foo + $wrongFormat = $true + } + else + { + $domain = $credentialSegments[0] + $userName = $credentialSegments[1] + } + } + elseif ($Credential.UserName.Contains('@')) + { + $credentialSegments = $Credential.UserName.Split('@') + + if ($credentialSegments.Length -gt 2) + { + # i.e. user@domain@foo + $wrongFormat = $true + } + else + { + $UserName = $credentialSegments[0] + $Domain = $credentialSegments[1] + } + } + else + { + # support for default domain (localhost) + $domain = $env:computerName + $userName = $Credential.UserName + } + + if ($wrongFormat) + { + $message = $script:localizedData.ErrorInvalidUserName -f $Credential.UserName + + New-InvalidArgumentException -ArgumentName 'Credential' -Message $message + } + + return @{ + Domain = $domain + UserName = $userName + } +} + +<# + .SYNOPSIS + Asserts that the PsDscContext is not run as user. + Throws an invalid argument exception if DSC is running as a specific user + (the PsDscRunAsCredential parameter was provided to DSC). + + .NOTES + Strict mode is turned off for this function since it does not recognize $PsDscContext +#> +function Assert-PsDscContextNotRunAsUser +{ + [CmdletBinding()] + param + () + + Set-StrictMode -Off + + if ($null -ne $PsDscContext.RunAsUser) + { + $newInvalidArgumentExceptionParams = @{ + ArgumentName = 'PsDscRunAsCredential' + Message = ($script:localizedData.ErrorRunAsCredentialParameterNotSupported -f $PsDscContext.RunAsUser) + } + New-InvalidArgumentException @newInvalidArgumentExceptionParams + } +} + +<# + .SYNOPSIS + Imports the DSC native methods so that a process can be started with a credential + for a user from the local system. + Currently Start-Process, which is the command used otherwise, cannot do this. +#> +function Import-DscNativeMethods +{ +$dscNativeMethodsSource = @" + +using System; +using System.Collections.Generic; +using System.Text; +using System.Security; +using System.Runtime.InteropServices; +using System.Diagnostics; +using System.Security.Principal; +#if !CORECLR +using System.ComponentModel; +#endif +using System.IO; + +namespace PSDesiredStateConfiguration +{ +#if !CORECLR + [SuppressUnmanagedCodeSecurity] +#endif + public static class NativeMethods + { + //The following structs and enums are used by the various Win32 API's that are used in the code below + + [StructLayout(LayoutKind.Sequential)] + public struct STARTUPINFO + { + public Int32 cb; + public string lpReserved; + public string lpDesktop; + public string lpTitle; + public Int32 dwX; + public Int32 dwY; + public Int32 dwXSize; + public Int32 dwXCountChars; + public Int32 dwYCountChars; + public Int32 dwFillAttribute; + public Int32 dwFlags; + public Int16 wShowWindow; + public Int16 cbReserved2; + public IntPtr lpReserved2; + public IntPtr hStdInput; + public IntPtr hStdOutput; + public IntPtr hStdError; + } + + [StructLayout(LayoutKind.Sequential)] + public struct PROCESS_INFORMATION + { + public IntPtr hProcess; + public IntPtr hThread; + public Int32 dwProcessID; + public Int32 dwThreadID; + } + + [Flags] + public enum LogonType + { + LOGON32_LOGON_INTERACTIVE = 2, + LOGON32_LOGON_NETWORK = 3, + LOGON32_LOGON_BATCH = 4, + LOGON32_LOGON_SERVICE = 5, + LOGON32_LOGON_UNLOCK = 7, + LOGON32_LOGON_NETWORK_CLEARTEXT = 8, + LOGON32_LOGON_NEW_CREDENTIALS = 9 + } + + [Flags] + public enum LogonProvider + { + LOGON32_PROVIDER_DEFAULT = 0, + LOGON32_PROVIDER_WINNT35, + LOGON32_PROVIDER_WINNT40, + LOGON32_PROVIDER_WINNT50 + } + [StructLayout(LayoutKind.Sequential)] + public struct SECURITY_ATTRIBUTES + { + public Int32 Length; + public IntPtr lpSecurityDescriptor; + public bool bInheritHandle; + } + + public enum SECURITY_IMPERSONATION_LEVEL + { + SecurityAnonymous, + SecurityIdentification, + SecurityImpersonation, + SecurityDelegation + } + + public enum TOKEN_TYPE + { + TokenPrimary = 1, + TokenImpersonation + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal struct TokPriv1Luid + { + public int Count; + public long Luid; + public int Attr; + } + + public const int GENERIC_ALL_ACCESS = 0x10000000; + public const int CREATE_NO_WINDOW = 0x08000000; + internal const int SE_PRIVILEGE_ENABLED = 0x00000002; + internal const int TOKEN_QUERY = 0x00000008; + internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020; + internal const string SE_INCRASE_QUOTA = "SeIncreaseQuotaPrivilege"; + +#if CORECLR + [DllImport("api-ms-win-core-handle-l1-1-0.dll", +#else + [DllImport("kernel32.dll", +#endif + EntryPoint = "CloseHandle", SetLastError = true, + CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] + public static extern bool CloseHandle(IntPtr handle); + +#if CORECLR + [DllImport("api-ms-win-core-processthreads-l1-1-2.dll", +#else + [DllImport("advapi32.dll", +#endif + EntryPoint = "CreateProcessAsUser", SetLastError = true, + CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)] + public static extern bool CreateProcessAsUser( + IntPtr hToken, + string lpApplicationName, + string lpCommandLine, + ref SECURITY_ATTRIBUTES lpProcessAttributes, + ref SECURITY_ATTRIBUTES lpThreadAttributes, + bool bInheritHandle, + Int32 dwCreationFlags, + IntPtr lpEnvrionment, + string lpCurrentDirectory, + ref STARTUPINFO lpStartupInfo, + ref PROCESS_INFORMATION lpProcessInformation + ); + +#if CORECLR + [DllImport("api-ms-win-security-base-l1-1-0.dll", EntryPoint = "DuplicateTokenEx")] +#else + [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")] +#endif + public static extern bool DuplicateTokenEx( + IntPtr hExistingToken, + Int32 dwDesiredAccess, + ref SECURITY_ATTRIBUTES lpThreadAttributes, + Int32 ImpersonationLevel, + Int32 dwTokenType, + ref IntPtr phNewToken + ); + +#if CORECLR + [DllImport("api-ms-win-security-logon-l1-1-1.dll", CharSet = CharSet.Unicode, SetLastError = true)] +#else + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] +#endif + public static extern Boolean LogonUser( + String lpszUserName, + String lpszDomain, + IntPtr lpszPassword, + LogonType dwLogonType, + LogonProvider dwLogonProvider, + out IntPtr phToken + ); + +#if CORECLR + [DllImport("api-ms-win-security-base-l1-1-0.dll", ExactSpelling = true, SetLastError = true)] +#else + [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] +#endif + internal static extern bool AdjustTokenPrivileges( + IntPtr htok, + bool disall, + ref TokPriv1Luid newst, + int len, + IntPtr prev, + IntPtr relen + ); + +#if CORECLR + [DllImport("api-ms-win-downlevel-kernel32-l1-1-0.dll", ExactSpelling = true)] +#else + [DllImport("kernel32.dll", ExactSpelling = true)] +#endif + internal static extern IntPtr GetCurrentProcess(); + +#if CORECLR + [DllImport("api-ms-win-downlevel-advapi32-l1-1-1.dll", ExactSpelling = true, SetLastError = true)] +#else + [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] +#endif + internal static extern bool OpenProcessToken( + IntPtr h, + int acc, + ref IntPtr phtok + ); + +#if CORECLR + [DllImport("api-ms-win-downlevel-kernel32-l1-1-0.dll", ExactSpelling = true)] +#else + [DllImport("kernel32.dll", ExactSpelling = true)] +#endif + internal static extern int WaitForSingleObject( + IntPtr h, + int milliseconds + ); + +#if CORECLR + [DllImport("api-ms-win-downlevel-kernel32-l1-1-0.dll", ExactSpelling = true)] +#else + [DllImport("kernel32.dll", ExactSpelling = true)] +#endif + internal static extern bool GetExitCodeProcess( + IntPtr h, + out int exitcode + ); + +#if CORECLR + [DllImport("api-ms-win-downlevel-advapi32-l4-1-0.dll", SetLastError = true)] +#else + [DllImport("advapi32.dll", SetLastError = true)] +#endif + internal static extern bool LookupPrivilegeValue( + string host, + string name, + ref long pluid + ); + + internal static void ThrowException( + string message + ) + { +#if CORECLR + throw new Exception(message); +#else + throw new Win32Exception(message); +#endif + } + + public static void CreateProcessAsUser(string strCommand, string strDomain, string strName, SecureString secureStringPassword, bool waitForExit, ref int ExitCode) + { + var hToken = IntPtr.Zero; + var hDupedToken = IntPtr.Zero; + TokPriv1Luid tp; + var pi = new PROCESS_INFORMATION(); + var sa = new SECURITY_ATTRIBUTES(); + sa.Length = Marshal.SizeOf(sa); + Boolean bResult = false; + try + { + IntPtr unmanagedPassword = IntPtr.Zero; + try + { +#if CORECLR + unmanagedPassword = SecureStringMarshal.SecureStringToCoTaskMemUnicode(secureStringPassword); +#else + unmanagedPassword = Marshal.SecureStringToGlobalAllocUnicode(secureStringPassword); +#endif + bResult = LogonUser( + strName, + strDomain, + unmanagedPassword, + LogonType.LOGON32_LOGON_NETWORK_CLEARTEXT, + LogonProvider.LOGON32_PROVIDER_DEFAULT, + out hToken + ); + } + finally + { + Marshal.ZeroFreeGlobalAllocUnicode(unmanagedPassword); + } + if (!bResult) + { + ThrowException("$($script:localizedData.UserCouldNotBeLoggedError)" + Marshal.GetLastWin32Error().ToString()); + } + IntPtr hproc = GetCurrentProcess(); + IntPtr htok = IntPtr.Zero; + bResult = OpenProcessToken( + hproc, + TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, + ref htok + ); + if (!bResult) + { + ThrowException("$($script:localizedData.OpenProcessTokenError)" + Marshal.GetLastWin32Error().ToString()); + } + tp.Count = 1; + tp.Luid = 0; + tp.Attr = SE_PRIVILEGE_ENABLED; + bResult = LookupPrivilegeValue( + null, + SE_INCRASE_QUOTA, + ref tp.Luid + ); + if (!bResult) + { + ThrowException("$($script:localizedData.PrivilegeLookingUpError)" + Marshal.GetLastWin32Error().ToString()); + } + bResult = AdjustTokenPrivileges( + htok, + false, + ref tp, + 0, + IntPtr.Zero, + IntPtr.Zero + ); + if (!bResult) + { + ThrowException("$($script:localizedData.TokenElevationError)" + Marshal.GetLastWin32Error().ToString()); + } + + bResult = DuplicateTokenEx( + hToken, + GENERIC_ALL_ACCESS, + ref sa, + (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, + (int)TOKEN_TYPE.TokenPrimary, + ref hDupedToken + ); + if (!bResult) + { + ThrowException("$($script:localizedData.DuplicateTokenError)" + Marshal.GetLastWin32Error().ToString()); + } + var si = new STARTUPINFO(); + si.cb = Marshal.SizeOf(si); + si.lpDesktop = ""; + bResult = CreateProcessAsUser( + hDupedToken, + null, + strCommand, + ref sa, + ref sa, + false, + 0, + IntPtr.Zero, + null, + ref si, + ref pi + ); + if (!bResult) + { + ThrowException("$($script:localizedData.CouldNotCreateProcessError)" + Marshal.GetLastWin32Error().ToString()); + } + if (waitForExit) { + int status = WaitForSingleObject(pi.hProcess, -1); + if(status == -1) + { + ThrowException("$($script:localizedData.WaitFailedError)" + Marshal.GetLastWin32Error().ToString()); + } + + bResult = GetExitCodeProcess(pi.hProcess, out ExitCode); + if(!bResult) + { + ThrowException("$($script:localizedData.RetriveStatusError)" + Marshal.GetLastWin32Error().ToString()); + } + } + } + finally + { + if (pi.hThread != IntPtr.Zero) + { + CloseHandle(pi.hThread); + } + if (pi.hProcess != IntPtr.Zero) + { + CloseHandle(pi.hProcess); + } + if (hDupedToken != IntPtr.Zero) + { + CloseHandle(hDupedToken); + } + } + } + } +} + +"@ + # if not on Nano: + Add-Type -TypeDefinition $dscNativeMethodsSource -ReferencedAssemblies 'System.ServiceProcess' +} + +Export-ModuleMember -Function *-TargetResource diff --git a/DscResources/MSFT_WindowsProcess/MSFT_WindowsProcess.schema.mof b/DscResources/MSFT_WindowsProcess/MSFT_WindowsProcess.schema.mof new file mode 100644 index 0000000..b04bad5 --- /dev/null +++ b/DscResources/MSFT_WindowsProcess/MSFT_WindowsProcess.schema.mof @@ -0,0 +1,18 @@ +[ClassVersion("1.0.0"), FriendlyName("WindowsProcess")] +class MSFT_WindowsProcess : OMI_BaseResource +{ + [Key, Description("The full path or file name to the process executable to start or stop.")] String Path; + [Key, Description("A string of arguments to pass to the process executable. Pass in an empty string if no arguments are needed.")] String Arguments; + [Write, EmbeddedInstance("MSFT_Credential"), Description("The credential to run the process under.")] String Credential; + [Write, ValueMap{"Present", "Absent"}, Values{"Present", "Absent"}, Description("Indicates whether the process is present (running) or absent (not running).")] String Ensure; + [Write, Description("The path to write the standard output stream to.")] String StandardOutputPath; + [Write, Description("The path to write the standard error stream to.")] String StandardErrorPath; + [Write, Description("The path to receive standard input from.")] String StandardInputPath; + [Write, Description("The directory to run the processes under.")] String WorkingDirectory; + [Read, Description("The amount of paged memory, in bytes, allocated for the process.")] UInt64 PagedMemorySize; + [Read, Description("The amount of nonpaged memory, in bytes, allocated for the process.")] UInt64 NonPagedMemorySize; + [Read, Description("The amount of virtual memory, in bytes, allocated for the process.")] UInt64 VirtualMemorySize; + [Read, Description("The number of handles opened by the process.")] SInt32 HandleCount; + [Read, Description("The unique identifier of the process.")] SInt32 ProcessId; + [Read, Description("The number of instances of the given process that are currently running.")] SInt32 ProcessCount; +}; diff --git a/DscResources/MSFT_WindowsProcess/en-US/MSFT_WindowsProcess.schema.mfl b/DscResources/MSFT_WindowsProcess/en-US/MSFT_WindowsProcess.schema.mfl new file mode 100644 index 0000000..a6ff25f Binary files /dev/null and b/DscResources/MSFT_WindowsProcess/en-US/MSFT_WindowsProcess.schema.mfl differ diff --git a/DscResources/MSFT_WindowsProcess/en-US/MSFT_WindowsProcess.strings.psd1 b/DscResources/MSFT_WindowsProcess/en-US/MSFT_WindowsProcess.strings.psd1 new file mode 100644 index 0000000..32df073 --- /dev/null +++ b/DscResources/MSFT_WindowsProcess/en-US/MSFT_WindowsProcess.strings.psd1 @@ -0,0 +1,36 @@ +# Localized resources for MSFT_WindowsProcess + +ConvertFrom-StringData @' + CouldNotCreateProcessError = Could not create process. Error code: + DuplicateTokenError = Duplicate token. Error code: + FileNotFound = File '{0}' not found in the environment path. + ErrorInvalidUserName = Invalid username: {0}. Username cannot contain multiple '@' or multiple '\' + ErrorParametersNotSupportedWithCredential = Can't specify StandardOutputPath, StandardInputPath or WorkingDirectory when trying to run a process under a local user. + ErrorRunAsCredentialParameterNotSupported = The PsDscRunAsCredential parameter is not supported by the Process resource. To start the process with user '{0}', add the Credential parameter. + ErrorStarting = Failure starting process matching path '{0}'. Message: {1}. + ErrorStopping = Failure stopping processes matching path '{0}' with IDs '({1})'. Message: {2}. + FailureWaitingForProcessesToStart = Failed to wait for processes to start. + FailureWaitingForProcessesToStop = Failed to wait for processes to stop. + GetTargetResourceStartMessage = Begin executing Get functionality for the process {0}. + GetTargetResourceEndMessage = End executing Get functionality for the process {0}. + OpenProcessTokenError = Error while opening process token. Error code: + ParameterShouldNotBeSpecified = Parameter {0} should not be specified. + PathShouldBeAbsolute = The path '{0}' should be absolute for argument '{1}'. + PathShouldExist = The path '{0}' should exist for argument '{1}'. + PrivilegeLookingUpError = Error while looking up privilege. Error code: + ProcessAlreadyStarted = Process matching path '{0}' found running. No action required. + ProcessAlreadyStopped = Process matching path '{0}' not found running. No action required. + ProcessesStarted = Processes matching path '{0}' started. + ProcessesStopped = Processes matching path '{0}' with IDs '({1})' stopped. + RetriveStatusError = Failed to retrieve status. Error code: + SetTargetResourceStartMessage = Begin executing Set functionality for the process {0}. + SetTargetResourceEndMessage = End executing Set functionality for the process {0}. + StartingProcessWhatif = Start-Process. + StoppingProcessWhatIf = Stop-Process. + TestTargetResourceStartMessage = Begin executing Test functionality for the process {0}. + TestTargetResourceEndMessage = End executing Test functionality for the process {0}. + TokenElevationError = Error while getting token elevation. Error code: + UserCouldNotBeLoggedError = User could not be logged. Error code: + WaitFailedError = Failed while waiting for process. Error code: + VerboseInProcessHandle = In process handle {0}. +'@ diff --git a/Examples/Sample_WindowsProcess_Start.ps1 b/Examples/Sample_WindowsProcess_Start.ps1 new file mode 100644 index 0000000..4c58b42 --- /dev/null +++ b/Examples/Sample_WindowsProcess_Start.ps1 @@ -0,0 +1,25 @@ +<# + .SYNOPSIS + Starts the gpresult process which generates a log about the group policy. + The path to the log is provided in 'Arguments'. +#> +Configuration Sample_WindowsProcess_Start +{ + param + () + + Import-DSCResource -ModuleName 'PSDscResources' + + Node localhost + { + WindowsProcess GPresult + { + Path = 'C:\Windows\System32\gpresult.exe' + Arguments = '/h C:\gp2.htm' + Ensure = 'Present' + } + } +} + +Sample_WindowsProcess_Start + diff --git a/Examples/Sample_WindowsProcess_StartUnderUser.ps1 b/Examples/Sample_WindowsProcess_StartUnderUser.ps1 new file mode 100644 index 0000000..1d96883 --- /dev/null +++ b/Examples/Sample_WindowsProcess_StartUnderUser.ps1 @@ -0,0 +1,37 @@ +<# + .SYNOPSIS + Starts the gpresult process under the given credential which generates a log + about the group policy. The path to the log is provided in 'Arguments'. + + .PARAMETER Credential + Credential to start the process under. +#> +Configuration Sample_WindowsProcess_StartUnderUser +{ + [CmdletBinding()] + param + ( + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential = (Get-Credential) + ) + + Import-DSCResource -ModuleName 'PSDscResources' + + Node localhost + { + WindowsProcess GPresult + { + Path = 'C:\Windows\System32\gpresult.exe' + Arguments = '/h C:\gp2.htm' + Credential = $Credential + Ensure = 'Present' + } + } +} + +<# + To use the sample(s) with credentials, see blog at: + http://blogs.msdn.com/b/powershell/archive/2014/01/31/want-to-secure-credentials-in-windows-powershell-desired-state-configuration.aspx +#> + diff --git a/Examples/Sample_WindowsProcess_Stop.ps1 b/Examples/Sample_WindowsProcess_Stop.ps1 new file mode 100644 index 0000000..89577b2 --- /dev/null +++ b/Examples/Sample_WindowsProcess_Stop.ps1 @@ -0,0 +1,26 @@ +<# + .SYNOPSIS + Stops the gpresult process if it is running. + Since the Arguments parameter isn't needed to stop the process, + an empty string is passed in. +#> +Configuration Sample_WindowsProcess_Stop +{ + param + () + + Import-DSCResource -ModuleName 'PSDscResources' + + Node localhost + { + WindowsProcess GPresult + { + Path = 'C:\Windows\System32\gpresult.exe' + Arguments = '' + Ensure = 'Absent' + } + } +} + +Sample_WindowsProcess_Stop + diff --git a/Examples/Sample_WindowsProcess_StopUnderUser.ps1 b/Examples/Sample_WindowsProcess_StopUnderUser.ps1 new file mode 100644 index 0000000..e084b87 --- /dev/null +++ b/Examples/Sample_WindowsProcess_StopUnderUser.ps1 @@ -0,0 +1,39 @@ +<# + .SYNOPSIS + Stops the gpresult process running under the given credential if it is running. + Since the Arguments parameter isn't needed to stop the process, + an empty string is passed in. + + .PARAMETER Credential + Credential that the process is running under. +#> +Configuration Sample_WindowsProcess_StopUnderUser +{ + [CmdletBinding()] + param + ( + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential = (Get-Credential) + ) + Import-DSCResource -ModuleName 'PSDscResources' + + Node localhost + { + WindowsProcess GPresult + { + Path = 'C:\Windows\System32\gpresult.exe' + Arguments = '' + Credential = $Credential + Ensure = 'Absent' + } + } +} + +<# + To use the sample(s) with credentials, see blog at: + http://blogs.msdn.com/b/powershell/archive/2014/01/31/want-to-secure-credentials-in-windows-powershell-desired-state-configuration.aspx +#> + + diff --git a/README.md b/README.md index 9e1450a..9c5786b 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Please check out the common DSC Resources [contributing guidelines](https://gith * [WindowsFeature](#windowsfeature): Provides a mechanism to install or uninstall windows roles or features on a target node. * [WindowsOptionalFeature](#windowsoptionalfeature): Provides a mechanism to enable or disable optional features on a target node. * [WindowsPackageCab](#windowspackagecab): Provides a mechanism to install or uninstall a package from a windows cabinet (cab) file on a target node. +* [WindowsProcess](#windowsprocess): Provides a mechanism to start and stop a Windows process. ### Resources that Work on Nano Server @@ -221,6 +222,39 @@ None * [Install a cab file with the given name from the given path](https://github.com/PowerShell/PSDesResources/blob/master/Examples/Sample_WindowsPackageCab.ps1) +### WindowsProcess +Provides a mechanism to start and stop a Windows process. + +#### Requirements + +None + +#### Parameters +* **[String] Path** _(Key)_: The executable file of the process. This can be defined as either the full path to the file or as the name of the file if it is accessible through the environment path. Relative paths are not supported. +* **[String] Arguments** _(Key)_: A single string containing all the arguments to pass to the process. Pass in an empty string if no arguments are needed. +* **[PSCredential] Credential** _(Write)_: The credential of the user account to run the process under. If this user is from the local system, the StandardOutputPath, StandardInputPath, and WorkingDirectory parameters cannot be provided at the same time. +* **[String] Ensure** _(Write)_: Specifies whether or not the process should be running. To start the process, specify this property as Present. To stop the process, specify this property as Absent. { *Present* | Absent }. +* **[String] StandardOutputPath** _(Write)_: The file path to which to write the standard output from the process. Any existing file at this file path will be overwritten. This property cannot be specified at the same time as Credential when running the process as a local user. +* **[String] StandardErrorPath** _(Write)_: The file path to which to write the standard error output from the process. Any existing file at this file path will be overwritten. +* **[String] StandardInputPath** _(Write)_: The file path from which to receive standard input for the process. This property cannot be specified at the same time as Credential when running the process as a local user. +* **[String] WorkingDirectory** _(Write)_: The file path to the working directory under which to run the file. This property cannot be specified at the same time as Credential when running the process as a local user. + +#### Read-Only Properties from Get-TargetResource +* **[UInt64] PagedMemorySize** _(Read)_: The amount of paged memory, in bytes, allocated for the process. +* **[UInt64] NonPagedMemorySize** _(Read)_: The amount of nonpaged memory, in bytes, allocated for the process. +* **[UInt64] VirtualMemorySize** _(Read)_: The amount of virtual memory, in bytes, allocated for the process. +* **[SInt32] HandleCount** _(Read)_: The number of handles opened by the process. +* **[SInt32] ProcessId** _(Read)_: The unique identifier of the process. +* **[SInt32] ProcessCount** _(Read)_: The number of instances of the given process that are currently running. + +#### Examples + +* [Start a process](https://github.com/PowerShell/PSDesResources/blob/master/Examples/Sample_WindowsProcess_Start.ps1) +* [Stop a process](https://github.com/PowerShell/PSDesResources/blob/master/Examples/Sample_WindowsProcess_Stop.ps1) +* [Start a process under a user](https://github.com/PowerShell/PSDesResources/blob/master/Examples/Sample_WindowsProcess_StartUnderUser.ps1) +* [Stop a process under a user](https://github.com/PowerShell/PSDesResources/blob/master/Examples/Sample_WindowsProcess_StopUnderUser.ps1) + + ## Versions ### Unreleased @@ -228,6 +262,8 @@ None * WindowsFeature: * Added Catch to ignore RuntimeException when importing ServerManager module. This solves the issue described [here](https://social.technet.microsoft.com/Forums/en-US/9fc314e1-27bf-4f03-ab78-5e0f7a662b8f/importmodule-servermanager-some-or-all-identity-references-could-not-be-translated?forum=winserverpowershell). * Updated unit tests. + +* Added WindowsProcess ### 2.1.0.0 diff --git a/Tests/TestHelpers/MSFT_WindowsProcess.TestHelper.psm1 b/Tests/TestHelpers/MSFT_WindowsProcess.TestHelper.psm1 new file mode 100644 index 0000000..0fe4b33 --- /dev/null +++ b/Tests/TestHelpers/MSFT_WindowsProcess.TestHelper.psm1 @@ -0,0 +1,26 @@ +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +<# + .SYNOPSIS + Stops all instances of the process with the given name. + + .PARAMETER ProcessName + The name of the process to stop. +#> +function Stop-ProcessByName +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [String] + $ProcessName + ) + + Stop-Process -Name $ProcessName -ErrorAction 'SilentlyContinue' -Force + Wait-ScriptBlockReturnTrue -ScriptBlock { return $null -eq (Get-Process -Name $ProcessName -ErrorAction 'SilentlyContinue') } ` + -TimeoutSeconds 15 +} + +Export-ModuleMember -Function Stop-ProcessByName diff --git a/tests/Integration/MSFT_WindowsProcess.Integration.Tests.ps1 b/tests/Integration/MSFT_WindowsProcess.Integration.Tests.ps1 new file mode 100644 index 0000000..d0a71dc --- /dev/null +++ b/tests/Integration/MSFT_WindowsProcess.Integration.Tests.ps1 @@ -0,0 +1,540 @@ +<# + These tests should only be run in AppVeyor since the second half of the tests require + the AppVeyor administrator account credential to run. + + Also please note that some of these tests depend on each other. + They must be run in the order given - if one test fails, subsequent tests may + also fail. +#> +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +$script:moduleRootPath = Split-Path -Path $PSScriptRoot -Parent +$script:testHelpersPath = Join-Path -Path $script:moduleRootPath -ChildPath 'TestHelpers' +Import-Module -Name (Join-Path -Path $script:testHelpersPath -ChildPath 'CommonTestHelper.psm1') + +$script:testEnvironment = Enter-DscResourceTestEnvironment ` + -DscResourceModuleName 'PSDscResources' ` + -DscResourceName 'MSFT_WindowsProcess' ` + -TestType 'Integration' + +try +{ + Describe 'WindowsProcess Integration Tests without Credential' { + $testProcessPath = Join-Path -Path $script:testHelpersPath -ChildPath 'WindowsProcessTestProcess.exe' + $logFilePath = Join-Path -Path $script:testHelpersPath -ChildPath 'processTestLog.txt' + + $configFile = Join-Path -Path $PSScriptRoot -ChildPath 'MSFT_WindowsProcess.config.ps1' + + Context 'Should stop any current instances of the testProcess running' { + $configurationName = 'MSFT_WindowsProcess_Setup' + $configurationPath = Join-Path -Path $TestDrive -ChildPath $configurationName + + It 'Should compile without throwing' { + { + if (Test-Path -Path $logFilePath) + { + Remove-Item -Path $logFilePath + } + + .$configFile -ConfigurationName $configurationName + & $configurationName -Path $testProcessPath ` + -Arguments $logFilePath ` + -Ensure 'Absent' ` + -ErrorAction 'Stop' ` + -OutputPath $configurationPath + Start-DscConfiguration -Path $configurationPath -ErrorAction 'Stop' -Wait -Force + } | Should Not Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction 'Stop' } | Should Not Throw + } + + It 'Should return the correct configuration' { + $currentConfig = Get-DscConfiguration -Verbose -ErrorAction 'Stop' + $currentConfig.Path | Should Be $testProcessPath + $currentConfig.Arguments | Should Be $logFilePath + $currentConfig.Ensure | Should Be 'Absent' + } + + It 'Should not create a logfile' { + $pathResult = Test-Path $logFilePath + $pathResult | Should Be $false + } + } + + Context 'Should start a new testProcess instance as running' { + $configurationName = 'MSFT_WindowsProcess_StartProcess' + $configurationPath = Join-Path -Path $TestDrive -ChildPath $configurationName + + It 'Should compile without throwing' { + { + if (Test-Path -Path $logFilePath) + { + Remove-Item -Path $logFilePath + } + + .$configFile -ConfigurationName $configurationName + & $configurationName -Path $testProcessPath ` + -Arguments $logFilePath ` + -Ensure 'Present' ` + -ErrorAction 'Stop' ` + -OutputPath $configurationPath + Start-DscConfiguration -Path $configurationPath -ErrorAction 'Stop' -Wait -Force + } | Should Not Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction 'Stop' } | Should Not Throw + } + + It 'Should return the correct configuration' { + $currentConfig = Get-DscConfiguration -Verbose -ErrorAction 'Stop' + $currentConfig.Path | Should Be $testProcessPath + $currentConfig.Arguments | Should Be $logFilePath + $currentConfig.Ensure | Should Be 'Present' + $currentConfig.ProcessCount | Should Be 1 + } + + It 'Should create a logfile' { + $pathResult = Test-Path $logFilePath + $pathResult | Should Be $true + } + } + + Context 'Should not start a second new testProcess instance when one is already running' { + $configurationName = 'MSFT_WindowsProcess_StartSecondProcess' + $configurationPath = Join-Path -Path $TestDrive -ChildPath $configurationName + + It 'Should compile without throwing' { + { + if (Test-Path -Path $logFilePath) + { + Remove-Item -Path $logFilePath + } + + .$configFile -ConfigurationName $configurationName + & $configurationName -Path $testProcessPath ` + -Arguments $logFilePath ` + -Ensure 'Present' ` + -ErrorAction 'Stop' ` + -OutputPath $configurationPath + Start-DscConfiguration -Path $configurationPath -ErrorAction 'Stop' -Wait -Force + } | Should Not Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction 'Stop' } | Should Not Throw + } + + It 'Should return the correct configuration' { + $currentConfig = Get-DscConfiguration -Verbose -ErrorAction 'Stop' + $currentConfig.Path | Should Be $testProcessPath + $currentConfig.Arguments | Should Be $logFilePath + $currentConfig.Ensure | Should Be 'Present' + $currentConfig.ProcessCount | Should Be 1 + } + + It 'Should not create a logfile' { + $pathResult = Test-Path $logFilePath + $pathResult | Should Be $false + } + } + + Context 'Should stop the testProcess instance from running' { + $configurationName = 'MSFT_WindowsProcess_StopProcesses' + $configurationPath = Join-Path -Path $TestDrive -ChildPath $configurationName + + It 'Should compile without throwing' { + { + if (Test-Path -Path $logFilePath) + { + Remove-Item -Path $logFilePath + } + + .$configFile -ConfigurationName $configurationName + & $configurationName -Path $testProcessPath ` + -Arguments $logFilePath ` + -Ensure 'Absent' ` + -ErrorAction 'Stop' ` + -OutputPath $configurationPath + Start-DscConfiguration -Path $configurationPath -ErrorAction 'Stop' -Wait -Force + } | Should Not Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction 'Stop' } | Should Not Throw + } + + It 'Should return the correct configuration' { + $currentConfig = Get-DscConfiguration -Verbose -ErrorAction 'Stop' + $currentConfig.Path | Should Be $testProcessPath + $currentConfig.Arguments | Should Be $logFilePath + $currentConfig.Ensure | Should Be 'Absent' + } + + It 'Should not create a logfile' { + $pathResult = Test-Path $logFilePath + $pathResult | Should Be $false + } + } + + Context 'Should return correct amount of processes running when more than 1 are running' { + $configurationName = 'MSFT_WindowsProcess_StartMultipleProcesses' + $configurationPath = Join-Path -Path $TestDrive -ChildPath $configurationName + + It 'Should compile without throwing' { + { + if (Test-Path -Path $logFilePath) + { + Remove-Item -Path $logFilePath + } + + .$configFile -ConfigurationName $configurationName + & $configurationName -Path $testProcessPath ` + -Arguments $logFilePath ` + -Ensure 'Present' ` + -ErrorAction 'Stop' ` + -OutputPath $configurationPath + Start-DscConfiguration -Path $configurationPath -ErrorAction 'Stop' -Wait -Force + } | Should Not Throw + } + + It 'Should start another process running' { + Start-Process -FilePath $testProcessPath -ArgumentList @($logFilePath) + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction 'Stop' } | Should Not Throw + } + + It 'Should return the correct configuration' { + $currentConfig = Get-DscConfiguration -Verbose -ErrorAction 'Stop' + $currentConfig.Path | Should Be $testProcessPath + $currentConfig.Arguments | Should Be $logFilePath + $currentConfig.Ensure | Should Be 'Present' + $currentConfig.ProcessCount | Should Be 2 + } + + It 'Should create a logfile' { + $pathResult = Test-Path $logFilePath + $pathResult | Should Be $true + } + + + } + + Context 'Should stop all of the testProcess instances from running' { + $configurationName = 'MSFT_WindowsProcess_StopAllProcesses' + $configurationPath = Join-Path -Path $TestDrive -ChildPath $configurationName + + It 'Should compile without throwing' { + { + if (Test-Path -Path $logFilePath) + { + Remove-Item -Path $logFilePath + } + + .$configFile -ConfigurationName $configurationName + & $configurationName -Path $testProcessPath ` + -Arguments $logFilePath ` + -Ensure 'Absent' ` + -ErrorAction 'Stop' ` + -OutputPath $configurationPath + Start-DscConfiguration -Path $configurationPath -ErrorAction 'Stop' -Wait -Force + } | Should Not Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction 'Stop' } | Should Not Throw + } + + It 'Should return the correct configuration' { + $currentConfig = Get-DscConfiguration -Verbose -ErrorAction 'Stop' + $currentConfig.Path | Should Be $testProcessPath + $currentConfig.Arguments | Should Be $logFilePath + $currentConfig.Ensure | Should Be 'Absent' + } + + It 'Should not create a logfile' { + $pathResult = Test-Path $logFilePath + $pathResult | Should Be $false + } + } + } + + Describe 'WindowsProcess Integration Tests with Credential' { + $ConfigData = @{ + AllNodes = @( + @{ + NodeName = '*' + PSDscAllowPlainTextPassword = $true + } + @{ + NodeName = 'localhost' + } + ) + } + + $testCredential = Get-AppVeyorAdministratorCredential + + $testProcessPath = Join-Path -Path $script:testHelpersPath -ChildPath 'WindowsProcessTestProcess.exe' + $logFilePath = Join-Path -Path $script:testHelpersPath -ChildPath 'processTestLog.txt' + + $configFile = Join-Path -Path $PSScriptRoot -ChildPath 'MSFT_WindowsProcessWithCredential.config.ps1' + + Context 'Should stop any current instances of the testProcess running' { + $configurationName = 'MSFT_WindowsProcess_SetupWithCredential' + $configurationPath = Join-Path -Path $TestDrive -ChildPath $configurationName + + It 'Should compile without throwing' { + { + if (Test-Path -Path $logFilePath) + { + Remove-Item -Path $logFilePath + } + + .$configFile -ConfigurationName $configurationName + & $configurationName -Path $testProcessPath ` + -Arguments $logFilePath ` + -Ensure 'Absent' ` + -Credential $testCredential ` + -ErrorAction 'Stop' ` + -OutputPath $configurationPath ` + -ConfigurationData $ConfigData + Start-DscConfiguration -Path $configurationPath -ErrorAction 'Stop' -Wait -Force + } | Should Not Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction 'Stop' } | Should Not Throw + } + + It 'Should return the correct configuration' { + $currentConfig = Get-DscConfiguration -Verbose -ErrorAction 'Stop' + $currentConfig.Path | Should Be $testProcessPath + $currentConfig.Arguments | Should Be $logFilePath + $currentConfig.Ensure | Should Be 'Absent' + } + + It 'Should not create a logfile' { + $pathResult = Test-Path $logFilePath + $pathResult | Should Be $false + } + } + + Context 'Should start a new testProcess instance as running' { + $configurationName = 'MSFT_WindowsProcess_StartProcessWithCredential' + $configurationPath = Join-Path -Path $TestDrive -ChildPath $configurationName + + It 'Should compile without throwing' { + { + if (Test-Path -Path $logFilePath) + { + Remove-Item -Path $logFilePath + } + + .$configFile -ConfigurationName $configurationName + & $configurationName -Path $testProcessPath ` + -Arguments $logFilePath ` + -Ensure 'Present' ` + -Credential $testCredential ` + -ErrorAction 'Stop' ` + -OutputPath $configurationPath ` + -ConfigurationData $ConfigData + Start-DscConfiguration -Path $configurationPath -ErrorAction 'Stop' -Wait -Force + } | Should Not Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction 'Stop' } | Should Not Throw + } + + It 'Should return the correct configuration' { + $currentConfig = Get-DscConfiguration -Verbose -ErrorAction 'Stop' + $currentConfig.Path | Should Be $testProcessPath + $currentConfig.Arguments | Should Be $logFilePath + $currentConfig.Ensure | Should Be 'Present' + $currentConfig.ProcessCount | Should Be 1 + } + + It 'Should create a logfile' { + $pathResult = Test-Path $logFilePath + $pathResult | Should Be $true + } + } + + Context 'Should not start a second new testProcess instance when one is already running' { + $configurationName = 'MSFT_WindowsProcess_StartSecondProcessWithCredential' + $configurationPath = Join-Path -Path $TestDrive -ChildPath $configurationName + + It 'Should compile without throwing' { + { + if (Test-Path -Path $logFilePath) + { + Remove-Item -Path $logFilePath + } + + .$configFile -ConfigurationName $configurationName + & $configurationName -Path $testProcessPath ` + -Arguments $logFilePath ` + -Ensure 'Present' ` + -Credential $testCredential ` + -ErrorAction 'Stop' ` + -OutputPath $configurationPath ` + -ConfigurationData $ConfigData + Start-DscConfiguration -Path $configurationPath -ErrorAction 'Stop' -Wait -Force + } | Should Not Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction 'Stop' } | Should Not Throw + } + + It 'Should return the correct configuration' { + $currentConfig = Get-DscConfiguration -Verbose -ErrorAction 'Stop' + $currentConfig.Path | Should Be $testProcessPath + $currentConfig.Arguments | Should Be $logFilePath + $currentConfig.Ensure | Should Be 'Present' + $currentConfig.ProcessCount | Should Be 1 + } + + It 'Should not create a logfile' { + $pathResult = Test-Path $logFilePath + $pathResult | Should Be $false + } + } + + Context 'Should stop the testProcess instance from running' { + $configurationName = 'MSFT_WindowsProcess_StopProcessesWithCredential' + $configurationPath = Join-Path -Path $TestDrive -ChildPath $configurationName + + It 'Should compile without throwing' { + { + if (Test-Path -Path $logFilePath) + { + Remove-Item -Path $logFilePath + } + + .$configFile -ConfigurationName $configurationName + & $configurationName -Path $testProcessPath ` + -Arguments $logFilePath ` + -Ensure 'Absent' ` + -Credential $testCredential ` + -ErrorAction 'Stop' ` + -OutputPath $configurationPath ` + -ConfigurationData $ConfigData + Start-DscConfiguration -Path $configurationPath -ErrorAction 'Stop' -Wait -Force + } | Should Not Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction 'Stop' } | Should Not Throw + } + + It 'Should return the correct configuration' { + $currentConfig = Get-DscConfiguration -Verbose -ErrorAction 'Stop' + $currentConfig.Path | Should Be $testProcessPath + $currentConfig.Arguments | Should Be $logFilePath + $currentConfig.Ensure | Should Be 'Absent' + } + + It 'Should not create a logfile' { + $pathResult = Test-Path $logFilePath + $pathResult | Should Be $false + } + } + + Context 'Should return correct amount of processes running when more than 1 are running' { + $configurationName = 'MSFT_WindowsProcess_StartMultipleProcessesWithCredential' + $configurationPath = Join-Path -Path $TestDrive -ChildPath $configurationName + + It 'Should compile without throwing' { + { + if (Test-Path -Path $logFilePath) + { + Remove-Item -Path $logFilePath + } + + .$configFile -ConfigurationName $configurationName + & $configurationName -Path $testProcessPath ` + -Arguments $logFilePath ` + -Ensure 'Present' ` + -ErrorAction 'Stop' ` + -Credential $testCredential ` + -OutputPath $configurationPath ` + -ConfigurationData $ConfigData + Start-DscConfiguration -Path $configurationPath -ErrorAction 'Stop' -Wait -Force + } | Should Not Throw + } + + It 'Should start another process running' { + Start-Process -FilePath $testProcessPath -ArgumentList @($logFilePath) + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction 'Stop' } | Should Not Throw + } + + It 'Should return the correct configuration' { + $currentConfig = Get-DscConfiguration -Verbose -ErrorAction 'Stop' + $currentConfig.Path | Should Be $testProcessPath + $currentConfig.Arguments | Should Be $logFilePath + $currentConfig.Ensure | Should Be 'Present' + $currentConfig.ProcessCount | Should Be 2 + } + + It 'Should create a logfile' { + $pathResult = Test-Path $logFilePath + $pathResult | Should Be $true + } + + + } + + Context 'Should stop all of the testProcess instances from running' { + $configurationName = 'MSFT_WindowsProcess_StopAllProcessesWithCredential' + $configurationPath = Join-Path -Path $TestDrive -ChildPath $configurationName + + It 'Should compile without throwing' { + { + if (Test-Path -Path $logFilePath) + { + Remove-Item -Path $logFilePath + } + + .$configFile -ConfigurationName $configurationName + & $configurationName -Path $testProcessPath ` + -Arguments $logFilePath ` + -Ensure 'Absent' ` + -Credential $testCredential ` + -ErrorAction 'Stop' ` + -OutputPath $configurationPath ` + -ConfigurationData $ConfigData + Start-DscConfiguration -Path $configurationPath -ErrorAction 'Stop' -Wait -Force + } | Should Not Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -Verbose -ErrorAction 'Stop' } | Should Not Throw + } + + It 'Should return the correct configuration' { + $currentConfig = Get-DscConfiguration -Verbose -ErrorAction 'Stop' + $currentConfig.Path | Should Be $testProcessPath + $currentConfig.Arguments | Should Be $logFilePath + $currentConfig.Ensure | Should Be 'Absent' + } + + It 'Should not create a logfile' { + $pathResult = Test-Path $logFilePath + $pathResult | Should Be $false + } + } + } +} +finally +{ + Exit-DscResourceTestEnvironment -TestEnvironment $script:testEnvironment +} diff --git a/tests/Integration/MSFT_WindowsProcess.config.ps1 b/tests/Integration/MSFT_WindowsProcess.config.ps1 new file mode 100644 index 0000000..f3e2b9a --- /dev/null +++ b/tests/Integration/MSFT_WindowsProcess.config.ps1 @@ -0,0 +1,35 @@ +param +( + [Parameter(Mandatory = $true)] + [String] + $ConfigurationName +) + +Configuration $ConfigurationName +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Path, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $Arguments, + + [ValidateSet('Present', 'Absent')] + [String] + $Ensure = 'Present' + ) + + Import-DscResource -ModuleName 'PSDscResources' + + WindowsProcess Process1 + { + Path = $Path + Arguments = $Arguments + Ensure = $Ensure + } +} diff --git a/tests/Integration/MSFT_WindowsProcessWithCredential.config.ps1 b/tests/Integration/MSFT_WindowsProcessWithCredential.config.ps1 new file mode 100644 index 0000000..38e5b07 --- /dev/null +++ b/tests/Integration/MSFT_WindowsProcessWithCredential.config.ps1 @@ -0,0 +1,44 @@ +param +( + [Parameter(Mandatory = $true)] + [String] + $ConfigurationName +) + +Configuration $ConfigurationName +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Path, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [String] + $Arguments, + + [ValidateSet('Present', 'Absent')] + [String] + $Ensure = 'Present', + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential = (Get-Credential) + ) + + Import-DscResource -ModuleName 'PSDscResources' + + Node $AllNodes.NodeName + { + WindowsProcess Process1 + { + Path = $Path + Arguments = $Arguments + Credential = $Credential + Ensure = $Ensure + } + } +} diff --git a/tests/TestHelpers/WindowsProcessTestProcess.cs b/tests/TestHelpers/WindowsProcessTestProcess.cs new file mode 100644 index 0000000..04e68fc --- /dev/null +++ b/tests/TestHelpers/WindowsProcessTestProcess.cs @@ -0,0 +1,37 @@ +using System.IO; +using System.Threading; + +// This is a test process used for testing configuring running and stopping a process on a machine +namespace WindowsProcessTestProcess +{ + class Program + { + static void Main(string[] args) + { + string[] lines = { "Test line1", "Test line2", "Test line3" }; + + if (args.Length > 0) + { + // and that it is a path + + // a second argument for infinite wait + string filePath = args[0]; + + using (StreamWriter outputFile = new StreamWriter(filePath)) + { + // Write to a log file so that we can see if the process ran + foreach (var line in lines) + { + outputFile.WriteLine(line); + } + } + if (args.Length == 1 || args[1] != "Stop Running") + { + // Sleep so that the process stays running until it is killed + Thread.Sleep(Timeout.Infinite); + } + } + } + } +} + diff --git a/tests/TestHelpers/WindowsProcessTestProcess.exe b/tests/TestHelpers/WindowsProcessTestProcess.exe new file mode 100644 index 0000000..cd9e857 Binary files /dev/null and b/tests/TestHelpers/WindowsProcessTestProcess.exe differ diff --git a/tests/Unit/MSFT_WindowsProcess.Tests.ps1 b/tests/Unit/MSFT_WindowsProcess.Tests.ps1 new file mode 100644 index 0000000..c8e3868 --- /dev/null +++ b/tests/Unit/MSFT_WindowsProcess.Tests.ps1 @@ -0,0 +1,781 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +param () + +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +$script:moduleRootPath = Split-Path -Path $PSScriptRoot -Parent +$script:testHelpersPath = Join-Path -Path $script:moduleRootPath -ChildPath 'TestHelpers' +Import-Module -Name (Join-Path -Path $script:testHelpersPath -ChildPath 'CommonTestHelper.psm1') + +$script:testEnvironment = Enter-DscResourceTestEnvironment ` + -DscResourceModuleName 'PSDscResources' ` + -DscResourceName 'MSFT_WindowsProcess' ` + -TestType 'Unit' + +try +{ + InModuleScope 'MSFT_WindowsProcess' { + + # Mock objects + $script:validPath1 = 'ValidPath1' + $script:validPath2 = 'ValidPath2' + $script:validPath3 = 'ValidPath3' + $script:invalidPath = 'InvalidPath' + $script:testUserName = 'TestUserName12345' + $testPassword = 'StrongOne7.' + $testSecurePassword = ConvertTo-SecureString -String $testPassword -AsPlainText -Force + $script:testCredential = New-Object PSCredential ($script:testUserName, $testSecurePassword) + $script:exceptionMessage = 'Test Invalid Operation Exception' + + $script:mockProcess1 = @{ + Path = $script:validPath1 + CommandLine = 'C:\temp\test.exe argument1 argument2 argument3' + Arguments = 'argument1 argument2 argument3' + ProcessId = 12345 + Id = 12345 + PagedMemorySize64 = 1048 + NonpagedSystemMemorySize64 = 16 + VirtualMemorySize64 = 256 + HandleCount = 50 + } + + $script:mockProcess2 = @{ + Path = $script:validPath2 + CommandLine = '' + Arguments = '' + ProcessId = 54321 + Id = 54321 + PagedMemorySize64 = 2096 + NonpagedSystemMemorySize64 = 8 + VirtualMemorySize64 = 512 + HandleCount = 5 + } + + $script:mockProcess3 = @{ + Path = $script:validPath1 + CommandLine = 'C:\test.exe arg6' + Arguments = 'arg6' + ProcessId = 1111101 + Id = 1111101 + PagedMemorySize64 = 512 + NonpagedSystemMemorySize64 = 32 + VirtualMemorySize64 = 64 + HandleCount = 0 + } + + $script:mockProcess4 = @{ + Path = $script:validPath1 + CommandLine = 'C:\test.exe arg6' + Arguments = 'arg6' + ProcessId = 1111101 + Id = 1111101 + PagedMemorySize64 = 510 + NonpagedSystemMemorySize64 = 16 + VirtualMemorySize64 = 8 + HandleCount = 8 + } + + $script:errorProcess = @{ + Path = $script:validPath3 + CommandLine = '' + Arguments = '' + ProcessId = 77777 + Id = 77777 + PagedMemorySize64 = 0 + NonpagedSystemMemorySize64 = 0 + VirtualMemorySize64 = 0 + HandleCount = 0 + } + + Describe 'WindowsProcess\Get-TargetResource' { + Mock -CommandName Expand-Path -MockWith { return $Path } + Mock -CommandName Get-ProcessCimInstance -MockWith { + if ($Path -eq $script:validPath1) + { + return @($script:mockProcess1, $script:mockProcess3) + } + elseif ($Path -eq $script:validPath2) + { + return @($script:mockProcess2) + } + elseif ($Path -eq $script:validPath3) + { + return @($script:errorProcess) + } + else + { + return @() + } + } + Mock -CommandName Get-Process -MockWith { + if ($ID -eq $script:mockProcess1.Id) + { + return $script:mockProcess1 + } + elseif ($script:mockProcess2.Id) + { + return $script:mockProcess2 + } + elseif ($script:mockProcess3.Id) + { + return $script:mockProcess3 + } + else + { + return $script:errorProcess + } + } + Mock -CommandName New-InvalidOperationException -MockWith { Throw $script:exceptionMessage } + Mock -CommandName New-InvalidArgumentException -MockWith { Throw $script:exceptionMessage } + + It 'Should return the correct properties for a process that is Absent' { + $processArguments = 'TestGetProperties' + + $getTargetResourceResult = Get-TargetResource -Path $invalidPath ` + -Arguments $processArguments + + $getTargetResourceResult.Arguments | Should Be $processArguments + $getTargetResourceResult.Ensure | Should Be 'Absent' + $getTargetResourceResult.Path | Should Be $invalidPath + + Assert-MockCalled -CommandName Expand-Path -Exactly 1 -Scope It + Assert-MockCalled -CommandName Get-ProcessCimInstance -Exactly 1 -Scope It + } + + It 'Should return the correct properties for one process with a credential' { + + $getTargetResourceResult = Get-TargetResource -Path $script:validPath2 ` + -Arguments $script:mockProcess2.Arguments ` + -Credential $script:testCredential + + $getTargetResourceResult.VirtualMemorySize | Should Be $script:mockProcess2.VirtualMemorySize64 + $getTargetResourceResult.Arguments | Should Be $script:mockProcess2.Arguments + $getTargetResourceResult.Ensure | Should Be 'Present' + $getTargetResourceResult.PagedMemorySize | Should Be $script:mockProcess2.PagedMemorySize64 + $getTargetResourceResult.Path | Should Be $script:mockProcess2.Path + $getTargetResourceResult.NonPagedMemorySize | Should Be $script:mockProcess2.NonpagedSystemMemorySize64 + $getTargetResourceResult.HandleCount | Should Be $script:mockProcess2.HandleCount + $getTargetResourceResult.ProcessId | Should Be $script:mockProcess2.ProcessId + + Assert-MockCalled -CommandName Expand-Path -Exactly 1 -Scope It + Assert-MockCalled -CommandName Get-ProcessCimInstance -Exactly 1 -Scope It + Assert-MockCalled -CommandName Get-Process -Exactly 1 -Scope It + } + + It 'Should return the correct properties when there are multiple processes' { + + $getTargetResourceResult = Get-TargetResource -Path $script:validPath1 ` + -Arguments $script:mockProcess1.Arguments + + $getTargetResourceResult.VirtualMemorySize | Should Be $script:mockProcess1.VirtualMemorySize64 + $getTargetResourceResult.Arguments | Should Be $script:mockProcess1.Arguments + $getTargetResourceResult.Ensure | Should Be 'Present' + $getTargetResourceResult.PagedMemorySize | Should Be $script:mockProcess1.PagedMemorySize64 + $getTargetResourceResult.Path | Should Be $script:mockProcess1.Path + $getTargetResourceResult.NonPagedMemorySize | Should Be $script:mockProcess1.NonpagedSystemMemorySize64 + $getTargetResourceResult.HandleCount | Should Be $script:mockProcess1.HandleCount + $getTargetResourceResult.ProcessId | Should Be $script:mockProcess1.ProcessId + + Assert-MockCalled -CommandName Expand-Path -Exactly 1 -Scope It + Assert-MockCalled -CommandName Get-ProcessCimInstance -Exactly 1 -Scope It + Assert-MockCalled -CommandName Get-Process -Exactly 1 -Scope It + } + } + + Describe 'WindowsProcess\Set-TargetResource' { + Mock -CommandName Expand-Path -MockWith { return $Path } + Mock -CommandName Get-ProcessCimInstance -MockWith { + if ($Path -eq $script:validPath1) + { + return @($script:mockProcess1, $script:mockProcess3) + } + elseif ($Path -eq $script:validPath2) + { + return @($script:mockProcess2) + } + elseif ($Path -eq $script:validPath3) + { + return @($script:errorProcess) + } + else + { + return @() + } + } + Mock -CommandName New-InvalidOperationException -MockWith { Throw $script:exceptionMessage } + Mock -CommandName New-InvalidArgumentException -MockWith { Throw $script:exceptionMessage } + Mock -CommandName Stop-Process -MockWith { return $null } ` + -ParameterFilter { ($Id -contains $script:mockProcess1.ProcessId) -or ` + ($Id -contains $script:mockProcess2.ProcessId) -or ` + ($Id -contains $script:mockProcess3.ProcessId) } + Mock -CommandName Stop-Process -MockWith { return 'error' } ` + -ParameterFilter { $Id -contains $script:errorProcess.ProcessId} + Mock -CommandName Test-IsRunFromLocalSystemUser -MockWith { return $true } + Mock -CommandName Wait-ProcessCount -MockWith { return $true } + + It 'Should not throw when Ensure set to Absent and processes are running' { + { Set-TargetResource -Path $script:validPath1 ` + -Arguments $script:mockProcess1.Arguments ` + -Credential $script:testCredential ` + -Ensure 'Absent' + } | Should Not Throw + + Assert-MockCalled -CommandName Expand-Path -Exactly 1 -Scope It + Assert-MockCalled -CommandName Get-ProcessCimInstance -Exactly 1 -Scope It + Assert-MockCalled -CommandName Stop-Process -Exactly 1 -Scope It + Assert-MockCalled -CommandName Wait-ProcessCount -Exactly 1 -Scope It + } + + It 'Should not throw when Ensure set to Absent and processes are not running' { + { Set-TargetResource -Path $script:invalidPath ` + -Arguments '' ` + -Ensure 'Absent' + } | Should Not Throw + + Assert-MockCalled -CommandName Expand-Path -Exactly 1 -Scope It + Assert-MockCalled -CommandName Get-ProcessCimInstance -Exactly 1 -Scope It + Assert-MockCalled -CommandName Stop-Process -Exactly 0 -Scope It + Assert-MockCalled -CommandName Wait-ProcessCount -Exactly 0 -Scope It + } + + It 'Should throw an invalid operation exception when Stop-Process throws an error' { + { Set-TargetResource -Path $script:errorProcess.Path ` + -Arguments '' ` + -Ensure 'Absent' + } | Should Throw $script:exceptionMessage + + Assert-MockCalled -CommandName Expand-Path -Exactly 1 -Scope It + Assert-MockCalled -CommandName Get-ProcessCimInstance -Exactly 1 -Scope It + Assert-MockCalled -CommandName Stop-Process -Exactly 1 -Scope It + Assert-MockCalled -CommandName New-InvalidOperationException -Exactly 1 -Scope It + Assert-MockCalled -CommandName Wait-ProcessCount -Exactly 0 -Scope It + } + + Mock -CommandName Wait-ProcessCount -MockWith { return $false } + + It 'Should throw an invalid operation exception when there is a problem waiting for the processes' { + { Set-TargetResource -Path $script:validPath1 ` + -Arguments $script:mockProcess1.Arguments ` + -Ensure 'Absent' + } | Should Throw $script:exceptionMessage + + Assert-MockCalled -CommandName Expand-Path -Exactly 1 -Scope It + Assert-MockCalled -CommandName Get-ProcessCimInstance -Exactly 1 -Scope It + Assert-MockCalled -CommandName Stop-Process -Exactly 1 -Scope It + Assert-MockCalled -CommandName Wait-ProcessCount -Exactly 1 -Scope It + Assert-MockCalled -CommandName New-InvalidOperationException -Exactly 1 -Scope It + } + + Mock -CommandName Wait-ProcessCount -MockWith { return $true } + Mock -CommandName Start-ProcessAsLocalSystemUser -MockWith {} + + It 'Should not throw when Ensure set to Present and processes are not running and credential passed in' { + { Set-TargetResource -Path $script:invalidPath ` + -Arguments $script:mockProcess1.Arguments ` + -Credential $script:testCredential ` + -Ensure 'Present' + } | Should Not Throw + + Assert-MockCalled -CommandName Expand-Path -Exactly 1 -Scope It + Assert-MockCalled -CommandName Get-ProcessCimInstance -Exactly 1 -Scope It + Assert-MockCalled -CommandName Test-IsRunFromLocalSystemUser -Exactly 1 -Scope It + Assert-MockCalled -CommandName Start-ProcessAsLocalSystemUser -Exactly 1 -Scope It + Assert-MockCalled -CommandName Wait-ProcessCount -Exactly 1 -Scope It + } + + Mock -CommandName Start-ProcessAsLocalSystemUser -MockWith {} + Mock -CommandName Assert-PathArgumentRooted -MockWith {} + Mock -CommandName Assert-PathArgumentValid -MockWith {} + + It 'Should throw when Ensure set to Present, processes not running and credential and WorkingDirectory passed' { + { Set-TargetResource -Path $script:invalidPath ` + -Arguments $script:mockProcess1.Arguments ` + -Credential $script:testCredential ` + -WorkingDirectory 'test working directory' ` + -Ensure 'Present' + } | Should Throw $script:exceptionMessage + + Assert-MockCalled -CommandName Expand-Path -Exactly 1 -Scope It + Assert-MockCalled -CommandName Get-ProcessCimInstance -Exactly 1 -Scope It + Assert-MockCalled -CommandName Test-IsRunFromLocalSystemUser -Exactly 1 -Scope It + Assert-MockCalled -CommandName Start-ProcessAsLocalSystemUser -Exactly 0 -Scope It + Assert-MockCalled -CommandName Wait-ProcessCount -Exactly 0 -Scope It + Assert-MockCalled -CommandName Assert-PathArgumentRooted -Exactly 1 -Scope It + Assert-MockCalled -CommandName Assert-PathArgumentValid -Exactly 1 -Scope It + Assert-MockCalled -CommandName New-InvalidArgumentException -Exactly 1 -Scope It + } + + $testErrorRecord = 'test Start-ProcessAsLocalSystemUser error record' + Mock -CommandName Start-ProcessAsLocalSystemUser -MockWith { Throw $testErrorRecord } + + It 'Should throw when Ensure set to Present and Start-processAsLocalSystemUser fails' { + { Set-TargetResource -Path $script:invalidPath ` + -Arguments $script:mockProcess1.Arguments ` + -Credential $script:testCredential ` + -Ensure 'Present' + } | Should Throw $testErrorRecord + + Assert-MockCalled -CommandName Expand-Path -Exactly 1 -Scope It + Assert-MockCalled -CommandName Get-ProcessCimInstance -Exactly 1 -Scope It + Assert-MockCalled -CommandName Test-IsRunFromLocalSystemUser -Exactly 1 -Scope It + Assert-MockCalled -CommandName Start-ProcessAsLocalSystemUser -Exactly 1 -Scope It + Assert-MockCalled -CommandName Wait-ProcessCount -Exactly 0 -Scope It + } + + Mock -CommandName Start-Process -MockWith {} + + It 'Should not throw when Ensure set to Present and processes are not running and no credential passed' { + { Set-TargetResource -Path $script:invalidPath ` + -Arguments $script:mockProcess1.Arguments ` + -Ensure 'Present' + } | Should Not Throw + + Assert-MockCalled -CommandName Expand-Path -Exactly 1 -Scope It + Assert-MockCalled -CommandName Get-ProcessCimInstance -Exactly 1 -Scope It + Assert-MockCalled -CommandName Start-Process -Exactly 1 -Scope It + Assert-MockCalled -CommandName Wait-ProcessCount -Exactly 1 -Scope It + } + + $mockStartProcessException = New-Object -TypeName 'InvalidOperationException' ` + -ArgumentList @('Start-Process test exception') + Mock -CommandName Start-Process -MockWith { Throw $mockStartProcessException } + + It 'Should throw when Ensure set to Present and Start-Process fails' { + { Set-TargetResource -Path $script:invalidPath ` + -Arguments $script:mockProcess1.Arguments ` + -Ensure 'Present' + } | Should Throw $script:exceptionMessage + + Assert-MockCalled -CommandName Expand-Path -Exactly 1 -Scope It + Assert-MockCalled -CommandName Get-ProcessCimInstance -Exactly 1 -Scope It + Assert-MockCalled -CommandName Start-Process -Exactly 1 -Scope It + Assert-MockCalled -CommandName Wait-ProcessCount -Exactly 0 -Scope It + Assert-MockCalled -CommandName New-InvalidOperationException -Exactly 1 -Scope It + } + + Mock -CommandName Wait-ProcessCount -MockWith { return $false } + Mock -CommandName Start-Process -MockWith {} + + It 'Should throw when there is a failure waiting for the process to start' { + { Set-TargetResource -Path $script:invalidPath ` + -Arguments $script:mockProcess1.Arguments ` + -Ensure 'Present' + } | Should Throw $script:exceptionMessage + + Assert-MockCalled -CommandName Expand-Path -Exactly 1 -Scope It + Assert-MockCalled -CommandName Get-ProcessCimInstance -Exactly 1 -Scope It + Assert-MockCalled -CommandName Start-Process -Exactly 1 -Scope It + Assert-MockCalled -CommandName Wait-ProcessCount -Exactly 1 -Scope It + } + + Mock -CommandName Wait-ProcessCount -MockWith { return $true } + + It 'Should not throw when Ensure set to Present and processes are already running' { + { Set-TargetResource -Path $script:validPath1 ` + -Arguments $script:mockProcess1.Arguments ` + -Ensure 'Present' + } | Should Not Throw + + Assert-MockCalled -CommandName Expand-Path -Exactly 1 -Scope It + Assert-MockCalled -CommandName Get-ProcessCimInstance -Exactly 1 -Scope It + Assert-MockCalled -CommandName Start-Process -Exactly 0 -Scope It + Assert-MockCalled -CommandName Wait-ProcessCount -Exactly 0 -Scope It + } + } + + Describe 'WindowsProcess\Test-TargetResource' { + Mock -CommandName Expand-Path -MockWith { return $Path } + Mock -CommandName Get-ProcessCimInstance -MockWith { + if ($Path -eq $script:validPath1) + { + return @($script:mockProcess1, $script:mockProcess3) + } + elseif ($Path -eq $script:validPath2) + { + return @($script:mockProcess2) + } + elseif ($Path -eq $script:validPath3) + { + return @($script:errorProcess) + } + else + { + return @() + } + } + + It 'Should return true when Ensure set to Present and process is running' { + $testTargetResourceResult = Test-TargetResource -Path $script:validPath1 ` + -Arguments $script:mockProcess1.Arguments ` + -Ensure 'Present' + $testTargetResourceResult | Should Be $true + } + + It 'Should return false when Ensure set to Present and process is not running' { + $testTargetResourceResult = Test-TargetResource -Path $script:invalidPath ` + -Arguments $script:mockProcess1.Arguments ` + -Ensure 'Present' + $testTargetResourceResult | Should Be $false + } + + It 'Should return true when Ensure set to Absent and process is not running and Credential passed' { + $testTargetResourceResult = Test-TargetResource -Path $script:invalidPath ` + -Arguments $script:mockProcess1.Arguments ` + -Credential $script:testCredential ` + -Ensure 'Absent' + $testTargetResourceResult | Should Be $true + } + + It 'Should return false when Ensure set to Absent and process is running' { + $testTargetResourceResult = Test-TargetResource -Path $script:validPath1 ` + -Arguments $script:mockProcess1.Arguments ` + -Ensure 'Absent' + $testTargetResourceResult | Should Be $false + } + + } + + Describe 'WindowsProcess\Expand-Path' { + Mock -CommandName New-InvalidArgumentException -MockWith { Throw $script:exceptionMessage } + Mock -CommandName Test-Path -MockWith { return $true } + + It 'Should return the original path when path is rooted' { + $rootedPath = 'C:\testProcess.exe' + + $expandPathResult = Expand-Path -Path $rootedPath + $expandPathResult | Should Be $rootedPath + } + + Mock -CommandName Test-Path -MockWith { return $false } + + It 'Should throw an invalid argument exception when Path is rooted and does not exist' { + $rootedPath = 'C:\invalidProcess.exe' + + { Expand-Path -Path $rootedPath} | Should Throw $script:exceptionMessage + + Assert-MockCalled -CommandName New-InvalidArgumentException -Exactly 1 -Scope It + } + + It 'Should throw an invalid argument exception when Path is unrooted and does not exist' { + $unrootedPath = 'invalidfile.txt' + + { Expand-Path -Path $unrootedPath} | Should Throw $script:exceptionMessage + + Assert-MockCalled -CommandName New-InvalidArgumentException -Exactly 1 -Scope It + } + } + + Describe 'WindowsProcess\Get-ProcessCimInstance' { + Mock -CommandName Get-Process -MockWith { return @($script:mockProcess2) } + Mock -CommandName Get-CimInstance -MockWith { return $script:mockProcess2 } + + It 'Should return the correct process when it exists and no arguments passed' { + $resultProcess = Get-ProcessCimInstance -Path $script:mockProcess2.Path + $resultProcess | Should Be @($script:mockProcess2) + + Assert-MockCalled -CommandName Get-Process -Exactly 1 -Scope It + Assert-MockCalled -CommandName Get-CimInstance -Exactly 1 -Scope It + } + + Mock -CommandName Get-Process -MockWith { return @($script:mockProcess1) } + Mock -CommandName Get-CimInstance -MockWith { return $script:mockProcess1 } + + It 'Should return the correct process when it exists and arguments are passed' { + $resultProcess = Get-ProcessCimInstance -Path $script:mockProcess1.Path ` + -Arguments $script:mockProcess1.Arguments + $resultProcess | Should Be @($script:mockProcess1) + + Assert-MockCalled -CommandName Get-Process -Exactly 1 -Scope It + Assert-MockCalled -CommandName Get-CimInstance -Exactly 1 -Scope It + } + + Mock -CommandName Get-Process -MockWith { return @($script:mockProcess1, $script:mockProcess1, $script:mockProcess1) } + Mock -CommandName Get-CimInstance -MockWith { return @($script:mockProcess1, $script:mockProcess1, $script:mockProcess1) } + + It 'Should return the correct processes when multiple exist' { + $resultProcess = Get-ProcessCimInstance -Path $script:mockProcess1.Path ` + -Arguments $script:mockProcess1.Arguments + $resultProcess | Should Be @($script:mockProcess1, $script:mockProcess1, $script:mockProcess1) + + Assert-MockCalled -CommandName Get-Process -Exactly 1 -Scope It + Assert-MockCalled -CommandName Get-CimInstance -Exactly 3 -Scope It + } + + Mock -CommandName Get-Process -MockWith { return @($script:mockProcess2, $script:mockProcess2) } + Mock -CommandName Get-CimInstance -MockWith { return @($script:mockProcess2, $script:mockProcess2) } + + It 'Should return the correct processes when they exists and cim instance threshold is lower than number of processes found' { + $resultProcess = Get-ProcessCimInstance -Path $script:mockProcess2.Path ` + -Arguments $script:mockProcess2.Arguments ` + -UseGetCimInstanceThreshold 1 + $resultProcess | Should Be @($script:mockProcess2, $script:mockProcess2) + + Assert-MockCalled -CommandName Get-Process -Exactly 1 -Scope It + Assert-MockCalled -CommandName Get-CimInstance -Exactly 1 -Scope It + } + + Mock -CommandName Get-Process -MockWith { return @($script:mockProcess2) } + Mock -CommandName Get-CimInstance -MockWith { return $script:mockProcess2 } + Mock -CommandName Get-ProcessOwner -MockWith { return ($env:computerName + '\' + $script:testUsername) } ` + -ParameterFilter { ($Process -eq $script:mockProcess2) } + + It 'Should return the correct process when it exists and Credential is passed in' { + $resultProcess = Get-ProcessCimInstance -Path $script:mockProcess2.Path ` + -Credential $script:testCredential + $resultProcess | Should Be @($script:mockProcess2) + + Assert-MockCalled -CommandName Get-Process -Exactly 1 -Scope It + Assert-MockCalled -CommandName Get-CimInstance -Exactly 1 -Scope It + Assert-MockCalled -CommandName Get-ProcessOwner -Exactly 1 -Scope It + } + + Mock -CommandName Get-Process -MockWith { return @($script:mockProcess3, $script:mockProcess3, $script:mockProcess4, $script:mockProcess2) } + Mock -CommandName Get-CimInstance -MockWith { return @($script:mockProcess3, $script:mockProcess3, $script:mockProcess4, $script:mockProcess2) } + Mock -CommandName Get-ProcessOwner -MockWith { return ($env:computerName + '\' + $script:testUsername) } ` + -ParameterFilter { ($Process -eq $script:mockProcess3) } + Mock -CommandName Get-ProcessOwner -MockWith { return ('wrongDomain' + '\' + $script:testUsername) } ` + -ParameterFilter { ($Process -eq $script:mockProcess4) -or ($Process -eq $script:mockProcess2) } + + It 'Should return only processes that match Credential' { + $resultProcess = Get-ProcessCimInstance -Path $script:mockProcess3.Path ` + -Credential $script:testCredential ` + -Arguments $script:mockProcess3.Arguments ` + -UseGetCimInstanceThreshold 1 + $resultProcess | Should Be @($script:mockProcess3, $script:mockProcess3) + + Assert-MockCalled -CommandName Get-Process -Exactly 1 -Scope It + Assert-MockCalled -CommandName Get-CimInstance -Exactly 1 -Scope It + } + + Mock -CommandName Get-ProcessOwner -MockWith { return ($env:computerName + '\' + $script:testUsername) } ` + -ParameterFilter { ($Process -eq $script:mockProcess3) -or ($Process -eq $script:mockProcess2) } + Mock -CommandName Get-ProcessOwner -MockWith { return ('wrongDomain' + '\' + $script:testUsername) } ` + -ParameterFilter { ($Process -eq $script:mockProcess4) } + + It 'Should return only processes that match Credential and Arguments' { + $resultProcess = Get-ProcessCimInstance -Path $script:mockProcess3.Path ` + -Credential $script:testCredential ` + -Arguments $script:mockProcess3.Arguments ` + -UseGetCimInstanceThreshold 1 + $resultProcess | Should Be @($script:mockProcess3, $script:mockProcess3) + + Assert-MockCalled -CommandName Get-Process -Exactly 1 -Scope It + Assert-MockCalled -CommandName Get-CimInstance -Exactly 1 -Scope It + } + } + + Describe 'WindowsProcess\ConvertTo-EscapedStringForWqlFilter' { + It 'Should return the same string when there are no escaped characters' { + $inputString = 'testString%$.@123' + $convertedString = ConvertTo-EscapedStringForWqlFilter -FilterString $inputString + $convertedString | Should Be $inputString + } + + It 'Should return a string with escaped characters: ("\)' { + $inputString = '\test"string"\123' + $expectedString = '\\test\"string\"\\123' + $convertedString = ConvertTo-EscapedStringForWqlFilter -FilterString $inputString + $convertedString | Should Be $expectedString + } + + It "Should return a string with escaped characters: ('\)" { + $inputString = "\test'string'\123" + $expectedString = "\\test\'string\'\\123" + $convertedString = ConvertTo-EscapedStringForWqlFilter -FilterString $inputString + $convertedString | Should Be $expectedString + } + } + + Describe 'WindowsProcess\Get-ProcessOwner' { + $mockOwner = @{ + Domain = 'Mock Domain' + User = 'Mock User' + } + Mock -CommandName Get-ProcessOwnerCimInstance -MockWith { return $mockOwner } + + It 'Should return the correct string with domain\user' { + $owner = Get-ProcessOwner -Process $script:mockProcess1 + $owner | Should Be ($mockOwner.Domain + '\' + $mockOwner.User) + } + + It 'Should return the correct string with default-domain\user when domain is not there' { + $mockOwner.Domain = $null + $owner = Get-ProcessOwner -Process $script:mockProcess1 + $owner | Should Be ($env:computerName + '\' + $mockOwner.User) + } + + } + + Describe 'WindowsProcess\Get-ArgumentsFromCommandLineInput' { + It 'Should return the correct arguments when single quotes are used' { + $inputString = 'test.txt a b c' + $argumentsReturned = Get-ArgumentsFromCommandLineInput -CommandLineInput $inputString + $argumentsReturned | Should Be 'a b c' + } + + It 'Should return the correct arguments when double quotes are used' { + $inputString = '"test file test" a b c' + $argumentsReturned = Get-ArgumentsFromCommandLineInput -CommandLineInput $inputString + $argumentsReturned | Should Be 'a b c' + } + + It 'Should return an empty string when an empty string is passed in' { + $inputString = $null + $resultString = [String]::Empty + $argumentsReturned = Get-ArgumentsFromCommandLineInput -CommandLineInput $inputString + $argumentsReturned | Should Be $resultString + } + + It 'Should return an empty string when there are no arguments' { + $inputString = 'test.txt' + $resultString = [String]::Empty + $argumentsReturned = Get-ArgumentsFromCommandLineInput -CommandLineInput $inputString + $argumentsReturned | Should Be $resultString + } + } + + Describe 'WindowsProcess\Assert-HashtableDoesNotContainKey' { + $mockHashtable = @{ + Key1 = 'test key1' + Key2 = 'test key2' + Key3 = 'test key3' + } + Mock -CommandName New-InvalidArgumentException -MockWith { Throw $script:exceptionMessage } + + It 'Should not throw an exception if the hashtable does not contain a key' { + $mockKey = @('k1', 'k2', 'k3', 'k4', 'k5') + { Assert-HashTableDoesNotContainKey -Hashtable $mockHashtable -Key $mockKey } | Should Not Throw + } + + It 'Should throw an exception if the hashtable contains a key' { + $mockKey = @('k1', 'k2', 'Key3', 'k4', 'k5') + { Assert-HashTableDoesNotContainKey -Hashtable $mockHashtable -Key $mockKey } | Should Throw $script:exceptionMessage + + Assert-MockCalled -CommandName New-InvalidArgumentException -Exactly 1 -Scope It + } + } + + Describe 'WindowsProcess\Wait-ProcessCount' { + $mockProcessSettings = @{ + Path = 'mockPath' + Arguments = 'mockArguments' + } + Mock -CommandName Get-ProcessCimInstance -MockWith { return @($script:mockProcess1, $script:mockProcess3) } + + It 'Should return true when all processes are returned' { + $processCountResult = Wait-ProcessCount -ProcessSettings $mockProcessSettings -ProcessCount 2 + $processCountResult | Should Be $true + } + + It 'Should return false when not all processes are returned' { + $processCountResult = Wait-ProcessCount -ProcessSettings $mockProcessSettings ` + -ProcessCount 3 ` + -WaitTime 10 + $processCountResult | Should Be $false + } + } + + Describe 'WindowsProcess\Assert-PathArgumentRooted' { + Mock -CommandName New-InvalidArgumentException -MockWith { Throw $script:exceptionMessage } + + It 'Should not throw when path is rooted' { + $rootedPath = 'C:\testProcess.exe' + + { Assert-PathArgumentRooted -PathArgumentName 'mock test name' ` + -PathArgument $rootedPath } | Should Not Throw + } + + It 'Should throw an invalid argument exception when Path is unrooted' { + $unrootedPath = 'invalidfile.txt' + + + { Assert-PathArgumentRooted -PathArgumentName 'mock test name' ` + -PathArgument $unrootedPath } | Should Throw $script:exceptionMessage + + Assert-MockCalled -CommandName New-InvalidArgumentException -Exactly 1 -Scope It + } + } + + Describe 'WindowsProcess\Assert-PathArgumentValid' { + Mock -CommandName New-InvalidArgumentException -MockWith { Throw $script:exceptionMessage } + + It 'Should not throw when path is valid' { + Mock -CommandName Test-Path -MockWith { return $true } + + { Assert-PathArgumentValid -PathArgumentName 'test name' ` + -PathArgument 'validPath' } | Should Not Throw + } + + It 'Should throw an invalid argument exception when Path is not valid' { + Mock -CommandName Test-Path -MockWith { return $false } + + { Assert-PathArgumentValid -PathArgumentName 'test name' ` + -PathArgument 'invalidPath' } | Should Throw $script:exceptionMessage + + Assert-MockCalled -CommandName New-InvalidArgumentException -Exactly 1 -Scope It + } + } + + Describe 'WindowsProcess\Split-Credential' { + Mock -CommandName New-InvalidArgumentException -MockWith { Throw $script:exceptionMessage } + + It 'Should return correct domain and username with @ seperator' { + $testUsername = 'user@domain' + $testPassword = ConvertTo-SecureString -String 'dummy' -AsPlainText -Force + $testCredential = New-Object -TypeName 'PSCredential' -ArgumentList @($testUsername, $testPassword) + + $splitCredentialResult = Split-Credential -Credential $testCredential + + $splitCredentialResult.Domain | Should Be 'domain' + $splitCredentialResult.Username | Should Be 'user' + } + + It 'Should return correct domain and username with \ seperator' { + $testUsername = 'domain\user' + $testPassword = ConvertTo-SecureString -String 'dummy' -AsPlainText -Force + $testCredential = New-Object -TypeName 'PSCredential' -ArgumentList @($testUsername, $testPassword) + + $splitCredentialResult = Split-Credential -Credential $testCredential + + $splitCredentialResult.Domain | Should Be 'domain' + $splitCredentialResult.Username | Should Be 'user' + } + + It 'Should return correct domain and username with a local user' { + $testUsername = 'localuser' + $testPassword = ConvertTo-SecureString -String 'dummy' -AsPlainText -Force + $testCredential = New-Object -TypeName 'PSCredential' -ArgumentList @($testUsername, $testPassword) + + $splitCredentialResult = Split-Credential -Credential $testCredential + + $splitCredentialResult.Domain | Should Be $env:computerName + $splitCredentialResult.Username | Should Be 'localuser' + } + + It 'Should throw an invalid argument exception when more than one \ in username' { + $testUsername = 'user\domain\foo' + $testPassword = ConvertTo-SecureString -String 'dummy' -AsPlainText -Force + $testCredential = New-Object -TypeName 'PSCredential' -ArgumentList @($testUsername, $testPassword) + + { $splitCredentialResult = Split-Credential -Credential $testCredential } | Should Throw $script:exceptionMessage + + Assert-MockCalled -CommandName New-InvalidArgumentException -Exactly 1 -Scope It + } + + It 'Should throw an invalid argument exception when more than one @ in username' { + $testUsername = 'user@domain@foo' + $testPassword = ConvertTo-SecureString -String 'dummy' -AsPlainText -Force + $testCredential = New-Object -TypeName 'PSCredential' -ArgumentList @($testUsername, $testPassword) + + { $splitCredentialResult = Split-Credential -Credential $testCredential } | Should Throw $script:exceptionMessage + + Assert-MockCalled -CommandName New-InvalidArgumentException -Exactly 1 -Scope It + } + } + } +} +finally +{ + Exit-DscResourceTestEnvironment -TestEnvironment $script:testEnvironment +}