I appreciate any advice from Powershell wizards out there.
I am trying to setup an email alert that will be triggered on event but having problems passing values as a variable so I can use it later in the body and subject of an email.
I have a few lines of script that pulls an event and stores it as XML but have no luck converting the value to variable to call it later.
$event = wevtutil qe 'Microsoft-Windows-Base-Filtering-Engine-Connections/Operational' /rd:true /c:1 /q:"*[System[(EventID=2000)]]" /f:renderedxml
$xmlresults = [xml]$event
$eventDate = $xmlresults.event.system.timecreated.systemtime
$eventTime = get-date $eventDate -Format ('dd-MM-yyyy hh:mm:ss')
Now when calling $xmlresults.event.eventdata.data I can see all values in a table:
PS C:\Windows\system32> $xmlresults.Event.EventData.Data
Name #text
---- -----
ConnectionId 1324000000003942
MachineAuthenticationMethod 2
RemoteMachineAccount DOMAIN\COMPUTERNAME$
UserAuthenticationMethod 2
RemoteUserAcount DOMAIN\USERNAME$
RemoteIPAddress XXXX:XXXXX:XXXX:XXXX:XXXXX:XXX:XXXX
LocalIPAddress XXXXX:XXXX:XXXX:XXXX::1
TechnologyProviderKey {XXXXXX-XXXX-XXXXX-XXXX-XXXXXXXXXX}
IPsecTrafficMode 1
DHGroup 0
StartTime 2021-05-17T13:52:38.103Z
How do I store the values of RemoteMachineAccount and RemoteUserAccount as a variable so I can call upon it later in the script when composing email alert and log input by simply using something along the lines of $computer and $user?
Any help greatly appreciated.
If you just need a few specifically named properties, you can use the .Where({}) method to filter the nodes returned by the XML provider and grab the text from that specific property:
$RemoteMachineAccount = $xmlresults.Event.EventData.Data.Where({$_.Name -eq 'RemoteMachineAccount'}).InnerText
$RemoteUserAccount = $xmlresults.Event.EventData.Data.Where({$_.Name -eq 'RemoteUserAccount'}).InnerText
If you need to discover/use them dynamically, another option is to convert the node list into a hashtable:
$eventData = #{}
$xmlresults.Event.EventData.Data.ForEach({ $eventData[$_.Name] = $_.InnerText })
At which point you can dereference the individual property values by name:
$eventData["RemoteUserAccount"]
Maybe I am trying to use AWS EC2 incorrectly, please help me out. I would like to make a base ami via a user data script, this is no problem, it works. However, the next step is to make an image, however since the object is untagged its kind of a pain to filter for it, I can add criteria for region, vpc, security group and state, this would find the object and I could build the image.
However I do not want to overwrite the existing image, so ideally i need to tag this with a name and version, no problem. But then I need the child images to find that image, and i would like to find via name and version, but dynamically, i.e. latest. In docker it is pretty straight forward as long as the container is tagged, to use latest the version can be omitted and it will auto pull the latest. Is there a similar technique here? What do you guys use? Am I possibly using this wrong?
Here is what I did:
Note: although this is for is tagging an instance, it can easily be modified to work with images.
Note: I am not a powershell power user, so if you see a glaring inefficiency, please let me know.
I use Jenkins to build the machine, as such it has environment variables that I use for the tagging, however it calls a powershell script that has this signature, so you could manually call or call it via another script:
param(
...
[Parameter(Mandatory=$true)][string]$Tag_Name,
[Parameter(Mandatory=$true)][string]$Tag_Version
)
Inside of this script, I set the instance tags like so:
#Get metadata from ec2 service
$identityDocument = (Invoke-WebRequest http://169.254.169.254/latest/dynamic/instance-identity/document/).Content | ConvertFrom-Json
$tags = #(
#{Key = "Name"; Value = $Tag_Name},
#{Key = "Version"; Value = $Tag_Version}
)
New-EC2Tag -Resource $identityDocument.instanceId -Tag $tags
In another script, I can query by name, find all instances, parse the result into a hash table of [InstanceId, Version], sort by Version and get the top one.
$instanceName = "hello-world"
$instances = GetHashTableOfFilteredInstances $instanceName
$instanceId = GetNewestInstance($instances)
Write-Host 'Information for ' $instanceName
Write-Host '================='
Write-Host 'The newest instance is ' $instanceId
Write-Host '================='
function GetHashTableOfFilteredInstances($tagName){
$instances = Get-EC2Instance -Filter #( `
#{name='tag:Name'; values=$tagName};`
) | Select-Object -ExpandProperty instances
$actInstances= #{}
foreach($instance in $instances){
foreach($tag in $instance.Tag){
if ($tag.Key -ne "Version") {
Continue;
}
$actInstances.Add($tag.Value, $instance.InstanceId)
}
}
return $actInstances
}
function GetNewestInstance($instances){
return ($instances.GetEnumerator() | Sort-Object Key -descending)[0].Value
}
I am trying to get a PowerShell script running that will display a list of all users who have been inactive (or not logged in) in x days. That part was easy enough to find and modify a script for, but I am having trouble setting it so I can specify only certain OUs and sub OUs within the domain. This is what I have so far, though I think I might have to use another method to accomplish this:
#Import Ad Module
Import-Module ActiveDirectory
#SearchBase
$searchB = (Get-Content -Path C:\scripts\ous.txt)
#Time accounts have been inactive
$tSpan = "145"
Search-ADAccount -SearchBase $searchB -AccountInactive -UserOnly -Timepsan $tSpan |
Where {($_.DistinguishedName -notlike "specific sub-ou I don't want to check")} |
FT name,ObjectClass -A
The text file is in the format:
OU=first ou,OU=Parent OU,DC= thisDC,DC=dc,DC=DC
OU=third ou,OU=Parent OU,DC= this DC,DC=dc,DC=DC
OU=fourthou,OU=Parent OU,DC= thisDC,DC=dc,DC=DC
When I run this I get an error
Search-ADAccount : Directory object not found
Looks like you have a type mismatch in your -SearchBase parameter.
See Get-Help Search-ADAccount
Note that the value type for -SearchBase is string. You have three OUs in your text file, so Get-Content on that file is going to produce a string array (string[]).
Since the -SearchBase parameter will only accept a single value, you'll need to foreach through the OU list, giving on one OU at a time:
foreach ($OU in $SearchB)
{
search-adaccount -searchbase $OU -accountinactive -useronly -timepsan $tSpan.....
}
First off I apologize for the extremely long, wordy post. It’s an interesting issue and I wanted to be as detailed as possible. I’ve tried looking through any related PowerShell posts on the site but I couldn’t find anything that helped me with troubleshooting this problem.
I've been working on a PowerShell script with a team that can send Wake-On-Lan packets to a group of computers. It works by reading a .csv file that has the hostnames and MAC’s in two columns, then it creates the WOL packets for each computer and broadcasts them out on the network. After the WOL packets are sent, it waits a minute and then pings the computers to verify they are online, and if any don’t respond it will display a window with what machines didn’t respond to a ping. Up until the final If/Else statement works fine, so I won't be going into too much detail on that part of the script (but of course if you want/need further details please feel free to ask).
The problem I’m having is with the final If/Else statement. The way the script is supposed to work is that in the ForEach loop in the middle of the script, the value of variable $PingResult is true or false depending on whether or not the computer responds to a ping. If the ping fails, $PingResult is $false, and then it adds the hostname to the $PingResult2 variable.
In theory if all of the machines respond, the If statement fires and the message box displays that it was a success and then the script stops. If any machines failed to respond, the Else statement runs and it joins all of the items together from the $PingResult2 variable and displays the list in a window.
What actually happens is that even if all of the machines respond to a ping, the If statement is completely skipped and the Else statement runs instead. However, at that point the $PingResult2 variable is blank and hence it doesn’t display any computer names of machines that failed to respond. In my testing I’ve never seen a case where the script fails to wake a computer up (assuming it’s plugged in, etc.), but the Else statement still runs regardless. In situations where the Else statement runs, I’ve checked the value of the $PingResult2 variable and confirmed that it is blank, and typing $PingResult2 –eq “” returns $true.
To add another wrinkle to the problem, I want to return to the $PingResult2 variable. I had to create the variable as a generic list so that it would support the Add method to allow the variable to grow as needed. As a test, we modified the script to concatenate the results together by using the += operator instead of making $PingResult2 a list, and while that didn’t give a very readable visual result in the final display window if machines failed, it did actually work properly occasionally. If all of the computers responded successfully the If statement would run as expected and display the success message. Like I said, it would sometimes work and sometimes not, with no other changes making a difference in the results. One other thing that we tried was taking out all of the references to the Visual Basic assembly and other GUI elements (besides the Out-GridView window) and that didn’t work either.
Any idea of what could be causing this problem? Me and my team are completely tapped out of ideas at this point and we’d love to figure out what’s causing the issue. We’ve tried it on Windows 7, 8.1, and the latest preview release of Windows 10 with no success. Thanks in advance for any assistance.
P.S Extra brownie points if you can explain what the regular expression on line 29 is called and how it exactly works. I found out about it on a web posting that resolved the issue of adding a colon between every two characters, but the posting didn’t explain what it was called. (Original link http://powershell.org/wp/forums/topic/add-colon-between-every-2-characters/)
Original WOL Script we built the rest of the script around was by John Savill (link http://windowsitpro.com/networking/q-how-can-i-easily-send-magic-packet-wake-machine-my-subnet)
Script
Add-Type -AssemblyName Microsoft.VisualBasic,System.Windows.Forms
$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$OpenFileDialog.ShowDialog() | Out-Null
$FileVerify = Get-Content -Path $OpenFileDialog.FileName -TotalCount 1
$FileVerify = ($FileVerify -split ',')
If($FileVerify[0] -ne "Machine Name" -or $FileVerify[1] -ne "MAC")
{
$MsgBox = [System.Windows.Forms.MessageBox]::Show("The CSV File's headers must be Machine Name and MAC.",'Invalid CSV File headers!',0,48)
Break
}
$ComputerList = Import-Csv -Path $OpenFileDialog.FileName |
Out-GridView -PassThru -Title "Select Computers to Wake up"
ForEach($Computer in $ComputerList)
{
If($Computer.'MAC' -notmatch '([:]|[-])')
{
$Computer.'MAC' = $Computer.'MAC' -replace '(..(?!$))','$1:'
}
$MACAddr = $Computer.'MAC'.split('([:]|[-])') | %{ [byte]('0x' + $_) }
$UDPclient = new-Object System.Net.Sockets.UdpClient
$UDPclient.Connect(([System.Net.IPAddress]::Broadcast),4000)
$packet = [byte[]](,0xFF * 6)
$packet += $MACAddr * 16
[void] $UDPclient.Send($packet, $packet.Length)
write "Wake-On-Lan magic packet sent to $($Computer.'Machine Name'.ToUpper())"
}
Write-Host "Pausing for sixty seconds before verifying connectivity."
Start-Sleep -Seconds 60
$PingResult2 = New-Object System.Collections.Generic.List[System.String]
ForEach($Computer in $ComputerList)
{
Write-Host "Pinging $($Computer.'Machine Name')"
$PingResult = Test-Connection -ComputerName $Computer.'Machine Name' -Quiet
If ($PingResult -eq $false)
{
$PingResult2.Add($Computer.'Machine Name')
}
}
If($PingResult2 -eq "")
{
[System.Windows.Forms.MessageBox]::Show("All machines selected are online.",'Success',0,48)
Break
}
Else
{
$PingResult2 = ($PingResult2 -join ', ')
[System.Windows.Forms.MessageBox]::Show("The following machines did not respond to a ping: $PingResult2",'Unreachable Machines',0,48)
}
The comparison in your If statement is incorrect because you are comparing $PingResult2, a List<string>, to a string. Instead, try
If ($PingResult2.Count -eq 0)
{
# Show the message box
}
Else
{
# Show the other message box
}
or one of countless other variations on this theme.
The regular expression in question uses a backreference to replace exactly two characters with the same two characters plus a colon character. I am unsure what exactly you are attempting to "define," though.
You are checking if a list has a value of a null string, rather than checking the number of items in the list.
If you change the if statement to the following it should work fine:
If($PingResult2.count -eq 0)
I'm guessing the regex is trying to insert a colon between every two characters of a string to represent 0123456789ab as 01:23:45:67:89:ab.
The code means if there is no hyphen or colon in the MAC, put in a colon every the characters, then split the address using colon as delimiter then represent each as a byte:
If($Computer.'MAC' -notmatch '([:]|[-])')
{
$Computer.'MAC' = $Computer.'MAC' -replace '(..(?!$))','$1:'
}
$MACAddr = $Computer.'MAC'.split('([:]|[-])') | %{ [byte]('0x' + $_) }
The other answer have explained quite well why your code does not work. I'm not going there. Instead I'll give some suggestions that I think would improve your script, and explain why I think so. Let's start with functions. Some of the things you do are functions I keep on hand because, well, they work well and are used often enough that I like having them handy.
First, your dialog to get the CSV file path. It works, don't get me wrong, but it could probably be better... As it is you pop up an Open File dialog with no parameters. This function allows you to use a few different parameters as wanted, or none for a very generic Open File dialog, but I think it's a slight improvement here:
Function Get-FilePath{
[CmdletBinding()]
Param(
[String]$Filter = "|*.*",
[String]$InitialDirectory = "C:\")
[void][System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms")
$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$OpenFileDialog.initialDirectory = $InitialDirectory
$OpenFileDialog.filter = $Filter
[void]$OpenFileDialog.ShowDialog()
$OpenFileDialog.filename
}
Then just call it as such:
$CSVFile = Get-FilePath -Filter "Comma Separated Value (.CSV)|*.CSV" -InitialDirectory "$env:USERPROFILE\Desktop"
That opens the dialog filtering for only CSV files, and starts them looking at their desktop (I find that a lot of people save things to their desktop). That only gets the path, so you would run your validation like you were. Actually, not like you were. You really seem to have over complicated that whole bit. Bit I'll get to that in a moment, first, another function! You call message boxes fairly often, and type out a bunch of options, and call the type, and everything every single time. If you're going to do it more than once, make it easy on yourself, make a function. Here, check this out:
Function Show-MsgBox ($Text,$Title="",[Windows.Forms.MessageBoxButtons]$Button = "OK",[Windows.Forms.MessageBoxIcon]$Icon="Information"){
[Windows.Forms.MessageBox]::Show("$Text", "$Title", [Windows.Forms.MessageBoxButtons]::$Button, $Icon) | ?{(!($_ -eq "OK"))}
}
Then you can specify as much or as little as you want for it. Plus it uses Type'd parameters, so tab completion works, or in the ISE (if that's where you're writing your script, like I do) it will pop up valid options and you just pick from a list for the buttons or icon to show. Plus it doesn't return anything if it's a simple 'OK' response, to keep things clean, but will return Yes/No/Cancel or whatever other option you choose for buttons.
Ok, that's the functions, let's get to the meat of the script. Your file validation... Ok, you pull the first line of the file, so that should just be a string, I'm not sure why you're splitting it and verifying each header individually. Just match the string as a whole. I would suggest doing it case insensitive, since we don't really care about case here. Also, depending on how the CSV file was generated, there could be quotes around headers, which you may want to account for. Using -Match will perform a RegEx match that is a bit more forgiving.
If((Get-Content $CSVFile -TotalCount 1) -match '^"?machine name"?,"?mac"?$'){
Show-MsgBox "The CSV File's headers must be Machine Name and MAC." 'Invalid CSV File headers!' -Icon Warning
break
}
So now we have two functions, and 5 lines of code. Yes, the functions take up more space than what you previously had, but they're friendlier to work with, and IMO more functional. Your MAC address correction, and WOL sending part are all aces so far as I'm concerned. There's no reason to change that part. Now, for validating that computers came back up... here we could use some improvement. Instead of making a [List] just add a member to each object, then filter against that below. The script as a whole would be a little longer, but better off for it I think.
Add-Type -AssemblyName Microsoft.VisualBasic,System.Windows.Forms
Function Get-FilePath{
[CmdletBinding()]
Param(
[String]$Filter = "|*.*",
[String]$InitialDirectory = "C:\")
[void][System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms")
$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$OpenFileDialog.initialDirectory = $InitialDirectory
$OpenFileDialog.filter = $Filter
[void]$OpenFileDialog.ShowDialog()
$OpenFileDialog.filename
}
Function Show-MsgBox ($Text,$Title="",[Windows.Forms.MessageBoxButtons]$Button = "OK",[Windows.Forms.MessageBoxIcon]$Icon="Information"){
[Windows.Forms.MessageBox]::Show("$Text", "$Title", [Windows.Forms.MessageBoxButtons]::$Button, $Icon) | ?{(!($_ -eq "OK"))}
}
#Get File Path
$CSVFile = Get-FilePath -Filter "Comma Separated Value (.CSV)|*.CSV" -InitialDirectory "$env:USERPROFILE\Desktop"
#Validate Header
If((Get-Content $CSVFile -TotalCount 1) -match '^"?machine name"?,"?mac"?$'){
Show-MsgBox "The CSV File's headers must be Machine Name and MAC." 'Invalid CSV File headers!' -Icon Warning
break
}
$ComputerList = Import-Csv -Path $CSVFile |
Out-GridView -PassThru -Title "Select Computers to Wake up"
ForEach($Computer in $ComputerList)
{
If($Computer.'MAC' -notmatch '([:]|[-])')
{
$Computer.'MAC' = $Computer.'MAC' -replace '(..(?!$))','$1:'
}
$MACAddr = $Computer.'MAC'.split('([:]|[-])') | %{ [byte]('0x' + $_) }
$UDPclient = new-Object System.Net.Sockets.UdpClient
$UDPclient.Connect(([System.Net.IPAddress]::Broadcast),4000)
$packet = [byte[]](,0xFF * 6)
$packet += $MACAddr * 16
[void] $UDPclient.Send($packet, $packet.Length)
write "Wake-On-Lan magic packet sent to $($Computer.'Machine Name'.ToUpper())"
}
Write-Host "Pausing for sixty seconds before verifying connectivity."
Start-Sleep -Seconds 60
$ComputerList|ForEach
{
Write-Host "Pinging $($_.'Machine Name')"
Add-Member -InputObject $_ -NotePropertyName "PingResult" -NotePropertyValue (Test-Connection -ComputerName $Computer.'Machine Name' -Quiet)
}
If(($ComputerList|Where{!($_.PingResult)}).Count -gt 0)
{
Show-MsgBox "All machines selected are online." 'Success'
}
Else
{
Show-MsgBox "The following machines did not respond to a ping: $(($ComputerList|?{!($_.PingResult)}) -join ", ")" 'Unreachable Machines' -Icon Asterisk
}
Ok, I'm going to get off my soap box and go home, my shift's over and it's time for a cold one.
Regarding service's recovery tab properties that can be seen here:
Is there an API to get the following property values:
First failure for example value: "Take no action"
Second failure
Subsequent failures
Reset fail count
I prefer a way to do so in PowerShell but would like to know about other options as well.
I am not familiar with PowerShell, but there is a Win32 API available: QueryServiceConfig2(). Set the dwInfoLevel parameter to SERVICE_CONFIG_FAILURE_ACTIONS, and pass a pointer to a buffer in the lpBuffer parameter that is large enough to receive a SERVICE_FAILURE_ACTIONS struct.
One needs to modify the services reg key, under
HKLM\System\CurrentControlSet\services\<service name>\
Adding a value of type binary with the name FailureActions. I don't know how it's structured you'd have to play around with that, but as it relates to powershell it would simply be grabbing to real name of the service (maybe using get-service if all you have is the display name), and navigating to that regkey and creating a new value, for example:
PS C:\Users\*\Desktop> $ByteArray = 0,0,0,144,10,23,253,33
PS C:\Users\*\Desktop> Set-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\services\AdobeARMservice -Name FailureActions -Type Binary -Value $ByteArray -Force
PS C:\Users\*\Desktop> Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\services\AdobeARMservice -Name FailureActions
FailureActions : {0, 0, 0, 144...}
PSPath : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\AdobeARMservice
PSParentPath : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services
PSChildName : AdobeARMservice
PSDrive : HKLM
PSProvider : Microsoft.PowerShell.Core\Registry
Adding a byte[ ], but like I mentioned you'd have to either reverse engineer the meaning of the array, or just copy an existing one or something similar.
You can control it for instance with cs.exe
Get-Service -DisplayName YourService | % { sc.exe failure $_.Name actions= /0 reset= 0 }