Detect application close based on app user's name - powershell

been trying to create script to detect user's application crash. (Assume the computer is used by multiple users)
so far only managed to come out below code to query the application (based on user name) only but not app close or crash
gwmi -query "select * from win32_process where name='calc.exe'" | %{if($_.GetOwner().User -eq 'myUser'){
#do something when app crash
}}

You can use the Register-WmiEvent cmdlet to register an event with the Win32_ProcessStopTrace event class.
The Win32_ProcessStopTrace doesn't have a GetOwner() method, but you can use your current code to gather the Process ID's of the processes you're interested in, and use those in your event query:
$UserName = 'myUser'
$ProcessName = 'calc.exe'
$PIDFilters = Get-WmiObject -Query "SELECT * FROM Win32_Process WHERE Name='$ProcessName'" |Where-Object {
$_.GetOwner().User -eq $UserName
} |Select-Object -ExpandProperty ProcessId |ForEach-Object {
"ProcessId={0}" -f $_
}
$WmiFilter = $PIDFilters -join " OR "
Now, you have a $WmiFilter that looks something like this:
ProcessId=2468 OR ProcessId=11576 OR ProcessId=5426
You can use that in a WMI query:
$WmiQuery = "SELECT * FROM Win32_ProcessStopTrace WHERE ($WmiFilter)"
And finally register the event with Register-WmiEvent:
Register-WmiEvent -Query $WmiQuery -SourceIdentifier CalcStopEvent -Action {
$TraceEvent = $Event.SourceEventArgs.NewEvent
if($TraceEvent.ExitStatus -ne 0){
# The process didn't exit with success/noerror
# Send many emails!
# Sound the klaxon!
# Call the fire brigade!
# or, whatever you feel like ...
}
}

Related

Powersell - Remotely Query if User Exists Across Domain [Fastest]

Abstract
So I work for a company that has roughly 10k computer assets on my domain. My issue is the time it takes to query if a user exists on a computer to see if they've ever logged into said computer. We need this functionality for audits in case they've done something they shouldn't have.
I have two methods in mind I've researched to complete this task, and a third alternative solution I have not thought of;
-Method A: Querying every computer for the "C:\Users<USER>" to see if LocalPath exists
-Method B: Checking every computer registry for the "HKU:<SID>" to see if the SID exists
-Method C: You are all smarter than me and have a better way? XD
Method A Function
$AllCompFound = #()
$AllADComputer = Get-ADComputer -Properties Name -SearchBase "WhatsItToYa" -filter 'Name -like "*"' | Select-Object Name
ForEach($Computer in $AllADComputers) {
$CName = $Computer.Name
if (Get-CimInstance -ComputerName "$CName" -ClassName Win32_Profile | ? {"C:\Users\'$EDIPI'" -contains $_.LocalPath}) {
$AllCompFound += $CName
} else {
#DOOTHERSTUFF
}
}
NOTE: I have another function that prompts me to enter a username to check for. Where I work they are numbers so case sensitivity is not an issue. My issue with this function is I believe it is the 'if' statement returns true every time because it ran rather than because it matched the username.
Method B Function
$AllCompFound = #()
$AllADComputer = Get-ADComputer -Properties Name -SearchBase "WhatsItToYa" -filter 'Name -like "*"' | Select-Object Name
$hive = [Microsoft:Win32.RegistryHive]::Users
ForEach($Computer in $AllADComputers) {
try {
$base = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($hive, $Computer.Name)
$key = &base.OpenSubKey($strSID)
if ($!key) {
#DOSTUFF
} else {
$AllCompFound += $Computer.Name
#DOOTHERSTUFF
}
} catch {
#IDONTTHROWBECAUSEIWANTITTOCONTINUE
} finally {
if($key) {
$key.Close()
}
if ($base) {
$base.Close()
}
}
}
NOTE: I have another function that converts the username into a SID prior to this function. It works.
Where my eyes start to glaze over is using Invoke-Command and actually return a value back, and whether or not to run all of these queries as their own PS-Session or not. My Method A returns false positives and my Method B seems to hang up on some computers.
Neither of these methods are really fast enough to get through 10k results, I've been using smaller pools of computers in order to get test these results when requested. I'm by no means an expert, but I think I have a good understanding, so any help is appreciated!
First, use WMI Win32_UserProfile, not C:\Users or registry.
Second, use reports from pc to some database, not from server to pc. This is much better usually.
About GPO: If you get access, you can Add\Remove scheduled task for such reports through GPP (not GPO) from time to time.
Third: Use PoshRSJob to make parallel queries.
Get-WmiObject -Class 'Win32_USerProfile' |
Select #(
'SID',
#{
Name = 'LastUseTime';
Expression = {$_.ConvertToDateTime($_.LastUseTime)}}
#{
Name = 'NTAccount';
Expression = { [System.Security.Principal.SecurityIdentifier]::new($_.SID).Translate([System.Security.Principal.NTAccount])}}
)
Be careful with translating to NTAccount: if SID does not translates, it will cause error, so, maybe, it's better not to collect NTAccount from user space.
If you have no other variants, parallel jobs using PoshRSJob
Example for paralleling ( maybe there are some typos )
$ToDo = [System.Collections.Concurrent.ConcurrentQueue[string]]::new() # This is Queue (list) of computers that SHOULD be processed
<# Some loop through your computers #>
<#...#> $ToDo.Enqueue($computerName)
<#LoopEnd#>
$result = [System.Collections.Concurrent.ConcurrentBag[Object]]::new() # This is Bag (list) of processing results
# This function has ComputerName on input, and outputs some single value (object) as a result of processing this computer
Function Get-MySpecialComputerStats
{
Param(
[String]$ComputerName
)
<#Some magic#>
# Here we make KSCustomObject form Hashtable. This is result object
return [PSCustomObject]#{
ComputerName = $ComputerName;
Result = 'OK'
SomeAdditionalInfo1 = 'whateverYouWant'
SomeAdditionalInfo2 = 42 # Because 42
}
}
# This is script that runs on background. It can not output anything.
# It takes 2 args: 1st is Input queue, 2nd is output queue
$JobScript = [scriptblock]{
$inQueue = [System.Collections.Concurrent.ConcurrentQueue[string]]$args[0]
$outBag = [System.Collections.Concurrent.ConcurrentBag[Object]]$args[1]
$compName = $null
# Logging inside, if you need it
$log = [System.Text.StringBuilder]::new()
# we work until inQueue is empty ( then TryDequeue will return false )
while($inQueue.TryDequeue([ref] $compName) -eq $true)
{
$r= $null
try
{
$r = Get-MySpecialComputerStats -ComputerName $compName -EA Stop
[void]$log.AppendLine("[_]: $($compName) : OK!")
[void]$outBag.Add($r) # We append result to outBag
}
catch
{
[void]$log.AppendLine("[E]: $($compName) : $($_.Exception.Message)")
}
}
# we return log.
return $log.ToString()
}
# Some progress counters
$i_max = $ToDo.Count
$i_cur = $i_max
# We start 20 jobs. Dont forget to say about our functions be available inside job
$jobs = #(1..20) <# Run 20 threads #> | % { Start-RSJob -ScriptBlock $JobScript -ArgumentList #($ToDo, $result) -FunctionsToImport 'Get-MySpecialComputerStats' }
# And once per 3 seconds we check, how much entries left in Queue ($todo)
while ($i_cur -gt 0)
{
Write-Progress -Activity 'Working' -Status "$($i_cur) left of $($i_max) computers" -PercentComplete (100 - ($i_cur / $i_max * 100))
Start-Sleep -Seconds 3
$i_cur = $ToDo.Count
}
# When there is zero, we shall wait for jobs to complete last items and return logs, and we collect logs
$logs = $jobs | % { Wait-RSJob -Job $_ } | % { Receive-RSJob -Job $_ }
# Logs is LOGS, not result
# Result is in the result variable.
$result | Export-Clixml -Path 'P:/ath/to/file.clixml' # Exporting result to CliXML file, or whatever you want
Please be careful: there is no output inside $JobScript done, so it must be perfectly done, and function Get-MySpecialComputerStats must be tested on unusual ways to return value that can be interpreted.

Store the output of command ran on remote system and list in proper format and select the option from that list in powershell?

I am running the below command in one of my powershell function to get the list of services from remote computer
Get-WmiObject -Class Win32_Service -filter "name like '$envlist%'" -Impersonation 3 -Credential abcdomain\PXXXX -ComputerName $server | Format-List -Property Name
The above command gives me the below output and I want to store it in variable and list it in proper format
Name : ABC_xx02_TT_xcedf_1.0.00.0101
Name : ABC_xx02_TT_nghk_2.1.0.99999
Name : ABC_xx02_TT_nmk_3_1.0.3.7890
Name : ABC_xx02_TT_pnp_4.0.0.123
I am expecting the output in below way:(the below services could be more so need to use something like counter).Once I select any choice from below I want to store it in variable for e.g if I select "3" then it should store in variable $serivcename = ABC_LA02_TT_nmk_3_1.0.3.7890
1. Press 1 to select ABC_xx02_TT_xcedf_1.0.00.0101
2. Press 2 select ABC_xx02_TT_nghk_2.1.0.99999
3. Press 3 to select ABC_xx02_TT_nmk_3_1.0.3.7890
4. Press 4 to select ABC_xx02_TT_pnp_4.0.0.123
Please make a selection:
I have no idea of course what could be in '$envlist%', but apparently that gives you the result you need.
Right now, your code uses Format-List, but that does not display a usable console menu.
I would do it like this:
$collection = (Get-WmiObject -Class Win32_Service -Filter "name like '$envlist%'" -Impersonation 3 -Credential abcdomain\PXXXX -ComputerName $server).Name
while ($true) {
Clear-Host
for ($i = 1; $i -le $collection.Count; $i++) {
# build your menu
"{0}. Press {0} to select {1}" -f $i, $collection[$i - 1]
}
$answer = Read-Host "Please make a selection. Press Q to quit"
# test if the (string) answer is in range, or if the user wants to quit
# if so, break the while loop
if ($answer -match "[1-$($collection.Count)Q]") { break }
# when you reach this point, the user made an invalid choice
Write-Host "Invalid input, please try again" -ForegroundColor Red
Start-Sleep -Seconds 3
}
# do whatever you need with the selected option
if ($answer -ne 'Q') {
Write-Host "User selected '{0}'" -f $collection[[int]$answer - 1]
# DoStuff $collection[[int]$answer - 1]
}
Try this:
$i = 0
Get-WmiObject -Class Win32_Service -filter "name like '$envlist%'" -Impersonation 3 -Credential abcdomain\PXXXX -ComputerName $server | Foreach {
$i++
Write-host "$($i). Press $($i) to select $($_.name.split('=')[1])"
}
$input = read-host "Please make a selection"
Now selection will be stored in $input.

Get-WmiObject not functioning properly in foreach loop

I am currently trying to write a script that takes a list of Computers joined to our domain, iterate through them one at a time to check if they exist in an Access DB that I created, run WMI queries on them collecting their system info, and add that data to the DB if they aren't already in it. I am successfully able to do so on most of the computers (around half), but some of them say RPC server not found.
I know that some of these errors are due to computers being offline (the firewall is disabled and WMI querying is enabled). The problem is that some of the computers are online, and when I run the Get-WmiObject command on them in the script I get that RPC server error, but when I run the command separately outside of the script I am able to successfully query the information. I have posted the function that is causing the weird behavior and was hoping someone with more programming knowledge would find what noob mistake I am making.
The second problem is that after the first iteration I get the error below saying blank CompName field? The first two iterations work as expected then it just throws a bunch of these errors with the "Computer already exists after".
function Update-Systems {
$PSCredential = Get-Credential
$Comp = (Get-ADComputer -Filter * | select -ExpandProperty Name)
foreach ($Computer in $Comp) {
$RecordSet.MoveFirst()
$RecordSet.Find("CompName = '$Computer'")
$RecordCheck = $RecordSet.Fields.Item("CompName").Value
if (!$RecordCheck) {
"Collecting Data for $Record"
$SystemProp = Get-WmiObject -Class Win32_ComputerSystem -Credential $PSCredential -ComputerName: $Computer -ErrorAction SilentlyContinue
$RecordSet.Addnew()
$RecordSet.Fields.Item("DateRan") = Get-Date
$RecordSet.Fields.Item("Domain") = $SystemProp.Domain
$RecordSet.Fields.Item("CompName") = $SystemProp.Name
$RecordSet.Fields.Item("Model") = $SystemProp.Model
$RecordSet.Fields.Item("Manufacturer") = $SystemProp.Manufacturer
$RecordSet.Update()
} else {
"Computer already exists"
}
}
}
Most likely Get-WmiObject fails to query information from a remote computer. Since you instructed the cmdlet to just carry on in case of an error (-ErrorAction SilentlyContinue) the variable $SystemProp ends up empty when an error occurs, because of which $SystemProp.Name evaluates to $null as well.
You could work around the issue by assigning $Computer rather than $SystemProp.Name to the recordset, at least as a fallback like this:
$RecordSet.Fields.Item("CompName") = if (-not $SystemProp) {
$Computer
} else {
$SystemProp.Name
}
However, a better approach would be to do proper error handling:
$ErrorActionPreference = 'Stop'
try {
$SystemProp = Get-WmiObject -Class Win32_ComputerSystem -Credential $PSCredential -ComputerName $Computer
$RecordSet.AddNew()
$RecordSet.Fields.Item("DateRan") = Get-Date
$RecordSet.Fields.Item("Domain") = $SystemProp.Domain
$RecordSet.Fields.Item("CompName") = $SystemProp.Name
$RecordSet.Fields.Item("Model") = $SystemProp.Model
$RecordSet.Fields.Item("Manufacturer") = $SystemProp.Manufacturer
} catch {
Write-Error $_ -ErrorAction Continue
}
You could also retry a couple times before giving up.

Using variable reference with get-WMIObject and interrogateservice method

I am using Get-WMIObject with class Win32_Service to get a reference to a service. I am storing that reference in a variable. My question is, can I just use the reference variable to get an update on the state of the service, or do I need to perform another Get-WMIObject?
Currently I use this:
$parameters = #{
Class = 'Win32_Service'
ComputerName = $server
Credential = $script:credentials
Filter = "Name='$ServiceName'"
}
$targetservice = Get-WMIObject #parameters
$results = $targetservice.startservice()
if($results.returnvalue -eq 0)
{
do
{
start-sleep -milliseconds 100
} until((Get-WMIObject #parameters).state -eq 'Running')
}
Do I need to make that second call to Get-WMIObject, or can I do it like this?
$parameters = #{
Class = 'Win32_Service'
ComputerName = $server
Credential = $script:credentials
Filter = "Name='$ServiceName'"
}
$targetservice = Get-WMIObject #parameters
$results = $targetservice.startservice()
if($results.returnvalue -eq 0)
{
do
{
start-sleep -milliseconds 100
$targetservice.interrogateservice()
} until(($targetservice).state -eq 'Running')
}
I know the interrogateservice is suppose to update the state of the service but for some reason it is not working as you would think and gets stuck in the loop.
WMI results only reflect the status at the time the query was performed. You need to re-run Get-WmiObject to get the current status.
You were close.. unfortunately InterrogateService does not update the $targetservice object but it does have its own return code that you can use to determine the state.
so you can have a if statement like:
if ($targetservice .InterrogateService().ReturnValue -ne 6) {"Failed to stop"}
For the return codes look at the below Microsoft reference:
https://learn.microsoft.com/en-us/windows/desktop/cimwin32prov/interrogateservice-method-in-class-win32-service
... but yea I would just make another WMI call as the return codes are not that straight forward... I believe it gives a 0 if the state isn't changed from the original. So you would have to account for that.

powershell output multiple variables derived from processlist

I have the below bit of code, it is designed to simply ask for a Windows ProcessName, and then return a list of all instances of that process that are running.
$processName = Read-Host 'Please Enter the Process Name, as shown in Task Manager (not inc *32)'
if (!$processName)
{
"No Process Given"
}
else
{
"You Entered "+$processName
$filter = "name like '%"+$processName+"'"
$result = Get-WmiObject win32_process -Filter $filter | select CommandLine
$counter=1
foreach($process in $result )
{
write-host "$counter) $process"
$counter++
}
}
It works fine up until the point where it outputs the list.
If I do
echo $process
then I get what I am after e.g.
"C:\folder\AppName.exe"
"C:\folder\AppName.exe instance1"
If however I try to concatenate the $counter in front of it I get:
1) #{CommandLine="C:\folder\AppName.exe" }
2) #{CommandLine="C:\folder\AppName.exe instance1" }
I've tried write-host, write-output, echo, various combinations of "", +, but I can't get rid of the #{CommandLine= xxx } when I try to combine it with another variable
Is there a way to get what I am after? e.g.:
1) "C:\folder\AppName.exe"
2) "C:\folder\AppName.exe instance1"
try write-host "$counter) $($process.commandline)"
OR modify your selection :
$result = Get-WmiObject win32_process -Filter $filter | select -expandproperty CommandLine
explanation : without expandproperty you get a psobject with expandproperty you have a string