Remote Powershell to retrieve specific registry value from lots of servers - powershell

I have the following..
$output = #()
$servers =Get-Content "C:\Windows\System32\List3.txt"
foreach ($server in $servers)
{
trap [Exception] {continue}
Import-Module PSRemoteRegistry
$key="SOFTWARE\Microsoft\'Microsoft Antimalware'\'Signature Updates'"
$regkey=Get-RegBinary -ComputerName $server -Key $Key -Value SignatuesLastUpdated
#$regkey=(Get-Item HKLM:\SOFTWARE\Microsoft\'Microsoft Antimalware'\'Signature Updates').getValue('SignaturesLastUpdated')
#$regkey=[datetime]::ParseExact("01/02/03", "dd/MM/yy", $null) | Export-csv -path c:\temp\avinfo.csv -append
#$regkey
}
$output | Select $server , $Regkey | Export-Csv c:\temp\avinfo.csv -NoTypeInformation
I think it's pretty close but doesn't work as needed - can anyone tell me what I am doing wrong here - been reading a lot and managed to get this far, just need the help to finalise.
Thanks

Ok... so there is alot that needed to be changed to get this to work. I will update the answer frequently after this is posted.
$servers = Get-Content "C:\Windows\System32\List3.txt"
$key="SOFTWARE\Microsoft\Microsoft Antimalware\Signature Updates"
$servers | ForEach-Object{
$server = $_
Try{
Get-RegBinary -ComputerName $server -Key $Key -Value SignatuesLastUpdated -ErrorAction Stop
} Catch [exception]{
[pscustomobject]#{
ComputerName = $server
Data = "Unable to retrieve data"
}
}
} | Select ComputerName,#{Label=$value;Expression={If(!($_.Data -is [string])){[System.Text.Encoding]::Ascii.GetBytes($_.data)}Else{$_.Data}}} | Export-Csv c:\temp\avinfo.csv -NoTypeInformation
What the above code will do is more in line with your intentions. Take the list and for each item get the key data from that server. If there is an issue getting that data then we output a custom object stating that so we can tell in the output if there was an issue. The part that is up in the air is how you want to export the binary data to file. As it stands it should create a space delimited string of the bytes.
The issues that you did have that should be highlighted are
No need to import the module for every server. Moved that call out of the loop
You have declared the variable $output but do not populate it during your loop process. This is important for the foreach construct. You were, in the end, sending and empty array to you csv. My answer does not need it as it just uses standard output.
As #Meatspace pointed out you had a typo here: SignatuesLastUpdated
Get-RegBinary does not by default create terminating errors which are needed by try/catch blocks. Added -ErrorAction Stop. Don't think your code trap [Exception] {continue} would have caught anything.
The single quotes you have in your $key might have prevented the path from being parsed. You were trying to escape spaces and just need to enclose the whole string in a set of quotes to achieve that.
While Select can use variables they are there, in a basic form, to select property names. In short what you had was wrong.

Related

How to modify local variable within Invoke-Command

I am trying to modify a variable within Invoke-Command in order to get out of a loop, however I'm having trouble doing that.
In the sample script below, I'm connecting to a host, grabbing information from NICs that are Up and saving the output to a file (Baseline). Then on my next iteration I will keep grabbing the same info and then compare Test file to Baseline file.
From a different shell, I've connected to the same server and disabled one of the NICs to force Compare-Object to find a difference.
Once a difference is found, I need to get out of the loop, however I cannot find a way to update the local variable $test_condition. I've tried multiple things, from Break, Return, $variable:global, $variable:script, but nothing worked so far.
$hostname = "server1"
$test_condition = $false
do {
Invoke-Command -ComputerName $hostname -Credential $credential -ScriptBlock{
$path = Test-Path -LiteralPath C:\Temp\"network_list_$using:hostname-Baseline.txt"
if ($path -eq $false) {
Get-NetAdapter | Where-Object Status -EQ "Up" | Out-File -FilePath (New-Item C:\Temp\"network_list_$using:hostname-Baseline.txt" -Force)
} else {
Get-NetAdapter | Where-Object Status -EQ "Up" | Out-File -FilePath C:\Temp\"network_list_$using:hostname-Test.txt"
$objects = #{
ReferenceObject = (Get-Content C:\Temp\"network_list_$using:hostname-Baseline.txt")
DifferenceObject = (Get-Content C:\Temp\"network_list_$using:hostname-Test.txt")
}
$test_condition = (Compare-Object #objects).SideIndicator -ccontains "<="
$test_condition #this is returning True <-----
}
}
} until ($test_condition -eq $true)
Any tips? What am I doing wrong?
TIA,
ftex
You can pass variables into a remote script block with the $Using:VarName scope modifier, but you can't use typical $Global: or $Script to modify anything in the calling scope. In this scenario the calling scope isn't the parent scope. The code is technically running in a new session on the remote system and $Global: would refer to that session's global scope.
For example:
$var = "something"
Invoke-Command -ComputerName MyComuter -ScriptBlock { $Global:var = "else"; $var}
The remote session will output "else". However, after return in the calling session $var will output "something" remaining unchanged despite the assignment in the remote session.
Based on #SantiagoSquarzon's comment place the assignment inside the Do loop with a few other modifications:
$hostname = "server1"
do {
$test_condition = $false
$test_condition =
Invoke-Command -ComputerName $hostname -Credential $credential -ScriptBlock{
$path = Test-Path -LiteralPath C:\Temp\"network_list_$using:hostname-Baseline.txt"
if ($path -eq $false) {
Get-NetAdapter | Where-Object Status -eq "Up" | Out-File -FilePath (New-Item C:\Temp\"network_list_$using:hostname-Baseline.txt" -Force)
} else {
Get-NetAdapter | Where-Object Status -eq "Up" | Out-File -FilePath C:\Temp\"network_list_$using:hostname-Test.txt"
$objects = #{
ReferenceObject = (Get-Content C:\Temp\"network_list_$using:hostname-Baseline.txt")
DifferenceObject = (Get-Content C:\Temp\"network_list_$using:hostname-Test.txt")
}
(Compare-Object #objects).SideIndicator -contains "<=" # this is returning True <-----
}
}
} until ($test_condition -eq $true)
I don't know why you were using -ccontains considering "<=" has no casing implications. Also it's very unusual to capitalize operators.
Notice there's no explicit return or assignment. PowerShell will emit the Boolean result of the comparison and that will be returned from the remote session and end up assigned to the $test_condition variable.
An aside:
I'm not sure why we want to use -contains at all. Admittedly it'll work fine in this case, however, it may lead you astray elsewhere. -contains is a collection containment operator and not really meant for testing the presence of one string within another. The literal meaning of "contains" makes for an implicitly attractive hazard, as demonstrated in this recent question.
In short it's easy to confuse the meaning, purpose and behavior on -contains.
This "<=" -contains "<=" will return "true" as expected, however "<==" -contains "<=" will return "false" even though the left string literally does contain the right string.
The answer, to the aforementioned question says much the same. My addendum answer offers a some additional insight for the particular problem and how different operators can be circumstantially applied.
So, as a matter of practice for this case wrap the Compare-Object command in the array sub-expression operator like:
#( (Compare-Object #objects).SideIndicator ) -contains "<="
Given the particulars, this strikes me as the least intrusive way to implement such a loosely stated best practice.

Script to scan computers from a list and identify which ones have a software title installed

I have narrowed down a little more what exactly my end game is.
I have pre-created the file that I want the results to write to.
Here is a rough script of what I want to do:
$computers = Get-content "C:\users\nicholas.j.nedrow\desktop\scripts\lists\ComputerList.txt"
# Ping all computers in ComputerList.txt.
# Need WinEst_Computers.csv file created with 3 Columns; Computer Name | Online (Y/N) | Does File Exist (Y/N)
$output = foreach ($comp in $computers) {
$TestConn = Test-connection -cn $comp -BufferSize 16 -Count 1 -ea 0 -quiet
if ($TestConn -match "False")
{
#Write "N" to "Online (Y/N)" Column in primary output .csv file
}
if ($TestConn -match "True")
{
#Write "Y" to "Online (Y/N)" Column in primary output .csv file
}
#For computers that return a "True" ping value:
#Search for WinEst.exe application on C:\
Get-ChildItem -Path "\\$comp\c$\program files (x86)\WinEst.exe" -ErrorAction SilentlyContinue |
if ("\\$comp\c$\program files (x86)\WinEst.exe" -match "False")
{
#Write "N" to "Does File Exist (Y/N)" Column in primary output .csv file
}
if ("\\$comp\c$\program files (x86)\WinEst.exe" -match "True")
{
#Write "Y" to "Does File Exist (Y/N)" Column in primary output .csv file
}
Select #{n='ComputerName';e={$comp}},Name
}
$output | Out-file "C:\users\nicholas.j.nedrow\desktop\scripts\results\CSV Files\WinEst_Computers.csv"
What I need help with is the following:
How to get each result to either write to the appropriate line (I.e. computername, online, file exist?) or would it be easier to do one column at a time;
--Write all PC's to Column A
--Ping each machine and record results in Column B
--Search each machine for the .exe and record results.
Any suggestions? Sorry I keep changing things. Just trying to figure out the best way to do this.
You are using the foreach command, which has a syntax foreach ($itemVariable in $collectionVariable) { }. If $computer is your collection, then your current item cannot also be $computer inside your foreach.
Get-Item does not return a property computerName. Therefore you cannot explicitly select it with Select-Object. However, you can use a calculated property to add a new property to the custom object that Select-Object outputs.
If your CSV file has a row of header(s), it is simpler to use Import-Csv to read the file. If it is just a list of computer names, then Get-Content works well.
If you are searching for a single file and you know the exact path, then just stick with -Path or -LiteralPath and forget -Include. -Include is not intuitive and isn't explained well in the online documentation.
If you are piping output to Export-Csv using a single pipeline, there's no need for -Append unless you already have an existing CSV with data you want to retain. However, if you choose to pipe to Export-Csv during each loop iteration, -Append would be necessary to retain the output.
Here is some updated code using the recommendations:
$computers = Get-content "C:\users\nicholas.j.nedrow\desktop\scripts\lists\ComputerList.txt"
$output = foreach ($comp in $computers) {
Get-Item -Path "\\$comp\c$\program files (x86)\WinEst.exe" -ErrorAction SilentlyContinue |
Select #{n='ComputerName';e={$comp}},Name
}
$output | Export-Csv -Path "C:\users\nicholas.j.nedrow\desktop\scripts\results\CSV Files\WinEst_Computers.csv" -NoType

Powershell problem with values comparison in ARS - false positive

I am updating mass info about users. The script is getting data from a file, comparing with the current data in ARS and changing if necessary.
Unfortunately for two parameters - "st" and "postOfficeBox" - it is updating data all the time altho the data is the same in the file and in AD.
first one is empty, the second one is not
I have checked directly -
PS> $user.$parameters.postofficebox -eq $userQuery.$parameters.postofficebox
True
How can I handle this? It is not an error, but it is annoying and not efficient updating the same data all the time.
#Internal Accounts
$Parameters = #("SamAccountName", "co", "company", "department", "departmentNumber","physicalDeliveryOfficeName","streetAddress","l","st","postalCode","employeeType","manager", "division", "title", "edsvaEmployedByCountry", "extensionAttribute4", "EmployeeID", "postOfficeBox")
#import of users
$users = Import-csv -Path C:\ps\krbatch.csv -Delimiter "," -Encoding UTF8
Connect-QADService -Proxy
#Headers compliance
$fileHeaders = $users[0].psobject.Properties | foreach { $_.Name }
$c = Compare-Object -ReferenceObject $fileHeaders -DifferenceObject $Parameters -PassThru
if ($c -ne $null) {Write-Host "headers do not fit"
break}
#Check if account is enabled
foreach ($user in $users) {
$checkEnable = Get-ADUser $user.SamAccountName | select enabled
if (-not $checkEnable.enabled) {
Write-Host $user.SamAccountName -ForegroundColor Red
}
}
#Main loop
$result = #()
foreach ($user in $users) {
$userQuery = Get-QADUser $user.sAMaccountName -IncludedProperties $Parameters | select $Parameters
Write-Host "...updating $($user.samaccountname)..." -ForegroundColor white
foreach ($param in $Parameters) {
if ($user.$param -eq $userQuery.$param) {
Write-Host "$($user.samaccountname) has correct $param" -ForegroundColor Yellow
}
else {
try {
Write-Host "Updating $param for $($user.samaccountname)" -ForegroundColor Green
Set-QADUser -Identity $user.SamAccountName -ObjectAttributes #{$param=$user.$param} -ErrorVariable ProcessError -ErrorAction SilentlyContinue | Out-Null
If ($ProcessError) {
Write-Host "cannot update $param for $($user.samaccountname) $($error[0])" -ForegroundColor Red
$problem = #{}
$problem.samaccountname = $($user.samaccountname)
$problem.param = $param
$problem.value = $($user.$param)
$problem.error = $($error[0])
$result +=[pscustomobject]$problem
}
}
catch { Write-Host "fail, check if the user account is enabled?" -ForegroundColor Red}
}
}
}
$result | Select samaccountname, param, value, error | Export-Csv -Path c:\ps\krfail.csv -NoTypeInformation -Encoding UTF8 -Append
And also any suggestions to my code, where I can make it better will be appreciated.
Similar to what Mathias R. Jessen was suggesting, the way you are testing the comparison doesn't look right. As debugging approaches either add the suggested Write-Host command or a break point such that you can test at run time.
Withstanding the comparison aspect of the question there's a loosely defined advisory request that I'll try to address.
Why are you you using QAD instead of the native AD module. QAD is awesome and still outshines the native tools in a few areas. But, (without a deep investigation) it looks like you can get by with the native tools here.
I'd point out there's an instance capability in AD cmdlets that allows for incremental updates even without comparison... ie you can run the Set-ADUser cmdlet and it will only write the attributes if they different.
Check out the help file for Set-ADUser
It would be inappropriate and time consuming for me to rewrite this. I'd suggest you check out those concepts for a rev 2.0 ... However, I can offer some advice bounded by the current approach.
The way the code is structured it'll run Set-QADUser for each attribute that needs updating rather than setting all the attributes at once on a per/user basis. Instead you could collect all the changes and apply in a single run of Set-QADUser per each user. That would be faster and likely have more compact logging etc...
When you're checking if the account is enabled you aren't doing anything other than Write-Host. If you wanted to skip that user, maybe move that logic into the main loop and add a Continue statement. That would also save you from looping twice.
Avoid using +=, you can use an [ArrayList] instead. Performance & scalability issues with += are well documented, so you can Google for more info. [ArrayList] might look something like:
$result = [Collections.ArrayList]#()
# ...
[Void]$result.Add( [PSCustomObject]$problem )
I'm also not sure how the catch block is supposed to fire if you've set -ErrorAction SilentlyContinue. You can probably remove If($ProcessError)... and and move population of $Result to the Catch{} block.

powershell use CSV to create variables

I am a total novice when it comes to powershell. I would like to create a little script to save me a lot of time and after a little research, I am sure it can be achieved using Import-CSV command.
Basically I need to run a command on multiple PC's but the variable is different for each command. I wish to pull that variable from a comparision in a CSV file. So find current Hostname, then use that hostname to find the corresponding asset number in the CSV and then use that asset number as a variable in the final comamnd.
Looking at other examples on here, I have this so far:
$Asset = #()
$Host = #()
Import-Csv -Path "C:\hostnametoasset.csv" |`
ForEach-Object {
$Asset += $_.Asset
$Host += $_.Host
}
$Hostname = (Get-WmiObject -Class Win32_ComputerSystem -Property Name).Name
if ($Host -contains $Hostname)
{
C:\BiosConfigUtility64.exe /setvalue:"Asset Tracking Number","$Asset"
}
Section of the CSV:
Asset,Host
10756,PCD001
10324,PCD002
10620,PCD003
Any help would be greatly appreciated.
Couple of different points...
Importing a CSV results in an array of objects that you can filter on.
$Lines = Import-Csv -Path "C:\hostnametoasset.csv"
$Line = $Lines | ?{$_.host -match $ENV:COMPUTERNAME}
You can then use the filter results directly by accessing the member you need.
C:\BiosConfigUtility64.exe /setvalue:"Asset Tracking Number","$($Line.Asset)"
NOTE: I cannot test this directly right now so hopefully I got the syntax right.

Local Groups and Members

I have a requirement to report the local groups and members from a specific list of servers. I have the following script that I have pieced together from other scripts. When run the script it writes the name of the server it is querying and the server's local group names and the members of those groups. I would like to output the text to a file, but where ever I add the | Out-File command I get an error "An empty pipe element is not allowed". My secondary concern with this script is, will the method I've chosen the report the server being queried work when outputting to a file. Will you please help correct this newbies script errors please?
$server=Get-Content "C:\Powershell\Local Groups\Test.txt"
Foreach ($server in $server)
{
$computer = [ADSI]"WinNT://$server,computer"
"
"
write-host "==========================="
write-host "Server: $server"
write-host "==========================="
"
"
$computer.psbase.children | where { $_.psbase.schemaClassName -eq 'group' } | foreach {
write-host $_.name
write-host "------"
$group =[ADSI]$_.psbase.Path
$group.psbase.Invoke("Members") | foreach {$_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null)}
write-host **
write-host
}
}
Thanks,
Kevin
You say that you are using Out-File and getting that error. You don't show_where_ in your code that is being called from.
Given the code you have my best guess is that you were trying something like this
Foreach ($server in $server){
# All the code in this block
} | Out-File c:\pathto.txt
I wish I had a technical reference for this interpretation but alas I have not found one (Think it has to do with older PowerShell versions). In my experience there is not standard output passed from that construct. As an aside ($server in $server) is misleading even if it works. Might I suggest this small change an let me know if that works.
$servers=Get-Content "C:\Powershell\Local Groups\Test.txt"
$servers | ForEach-Object{
$server = $_
# Rest of code inside block stays the same
} | Out-File c:\pathto.txt
If that is not your speed then I would also consider building an empty array outside the block and populate is for each loop pass.
# Declare empty array to hold results
$results = #()
Foreach ($server in $server){
# Code before this line
$results += $group.psbase.Invoke("Members") | foreach {$_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null)}
# Code after this line
}
$results | Set-Content c:\pathto.txt
Worthy Note
You are mixing Console output with standard output. depending on what you want to do with the script you will not get the same output you expect. If you want the lines like write-host "Server: $server" to be in the output file then you need to use Write-Output