Changing CSV report entry if duplicates exist with powershell - powershell

I'm pretty new to powershell, so I'll do my best to explain myself. I'm currently working with a script which creates a csv report of access levels for all users and user groups on a server. Breaking it down between Admin and User privileges. As is, it currently out puts two entries for groups which have both Admin and User access. Resembling the following image. (posted as an image due to some trouble with creating a table on stackoverflow)
I was hoping for some suggestions about how to consolidate users/groups which repeat in the report into one entry with an X in both fields. Kind of like the following:
Here is my current script:
[CmdletBinding()]
Param(
[Parameter( ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true
)]
[string[]]
$ComputerName = $env:ComputerName,
[Parameter()]
[array] $LocalGroupNames = #("Administrators", "Remote Desktop Users"),
[Parameter()]
[string]
$LocalGroupName,
[Parameter()]
[string]
$OutputDir = "c:\temp"
)
Begin {
$OutputFile = Join-Path $OutputDir "OSUsers $(get-date -f yyyy-MM-dd).csv"
Add-Content -Path $OutPutFile -Value "ServerName, User\Group, Administrator, User"
}
Process {
ForEach($Computer in $ComputerName) {
foreach ($LocalGroupName in $LocalGroupNames) {
Write-host "Working on $Computer"
If(!(Test-Connection -ComputerName $Computer -Count 1 -Quiet)) {
Add-Content -Path $OutputFile -Value "$Computer,Offline,Offline,Offline"
Continue
} else {
Write-Verbose "Working on $computer"
try {
$group = [ADSI]"WinNT://$Computer/$LocalGroupName"
$members = #($group.Invoke("Members"))
Write-Verbose "Successfully queries the members of $computer"
if(!$members) {
Add-Content -Path $OutputFile -Value "$Computer,NoMembersFound"
Write-Verbose "No members found in the group"
continue
}
}
catch {
Add-Content -Path $OutputFile -Value "$Computer,FailedToQuery"
Continue
}
foreach($member in $members) {
try {
$MemberName = $member.GetType().Invokemember("Name","GetProperty",$null,$member,$null)
$MemberType = $member.GetType().Invokemember("Class","GetProperty",$null,$member,$null)
$MemberPath = $member.GetType().Invokemember("ADSPath","GetProperty",$null,$member,$null)
$MemberDomain = $null
if($MemberPath -match "^Winnt\:\/\/(?<domainName>\S+)\/(?<CompName>\S+)\/") {
if($MemberType -eq "User") {
$MemberType = "LocalUser"
} elseif($MemberType -eq "Group"){
$MemberType = "LocalGroup"
}
$MemberDomain = $matches["CompName"]
} elseif($MemberPath -match "^WinNT\:\/\/(?<domainname>\S+)/") {
if($MemberType -eq "User") {
$MemberType = "DomainUser"
} elseif($MemberType -eq "Group"){
$MemberType = "DomainGroup"
}
$MemberDomain = $matches["domainname"]
} else {
$MemberType = "Unknown"
$MemberDomain = "Unknown"
}
if ($MemberType -ne "Unknown") ##Exclude unresolved users
{
if ($LocalGroupName -eq "Administrators")
{Add-Content -Path $OutPutFile -Value "$Computer, $MemberName, X, ,"}
if ($LocalGroupName -eq "Remote Desktop Users")
{Add-Content -Path $OutPutFile -Value "$Computer, $MemberName, , X,"}
}
} catch
{
Add-Content -Path $OutputFile -Value "$Computer, ,FailedQueryMember"
}
}
}
}
}
}
End {}

What you may want to do here is get the members of both the Administrators and Remote Desktop Users, and then with a foreach statement, check the results for each user's presence in both groups or not.
Compare-Object might be your friend here as well, as you can determine how to handle what you write to your file. That command will output "<=", "=>", or "==" if you use the -IncludeEqual parameter.
Ex:
Powershell - find items which are in array1 but NOT in array2

Related

How to enumerate available properties of member

In the code below there are a number of properties of "member" I've added "description" property successfully but I can't find out if the account is enabled. I've tried "status" or "enabled" or "disabled" all to no avail. I realize it's a member of a group of an ADSI call but, I really need to know if the account is enabled or not.
Thanks in advance!
Full script available at https://github.com/JDogHerman/Powershell_Scripts/blob/master/get-localgroupmembers.ps1
Process {
ForEach($Computer in $ComputerName) {
Write-host "Working on $Computer"
If(!(Test-Connection -ComputerName $Computer -Count 1 -Quiet)) {
Write-Verbose "$Computer is offline. Proceeding with next computer"
Add-Content -Path $OutputFile -Value "$Computer,$LocalGroupName,Offline"
Continue
} else {
Write-Verbose "Working on $computer"
try {
$group = [ADSI]"WinNT://$Computer/$LocalGroupName"
$members = #($group.Invoke("Members"))
Write-Verbose "Successfully queries the members of $computer"
if(!$members) {
Add-Content -Path $OutputFile -Value "$Computer,$LocalGroupName,NoMembersFound"
Write-Verbose "No members found in the group"
continue
}
}
catch {
Write-Verbose "Failed to query the members of $computer"
Add-Content -Path $OutputFile -Value "$Computer,,FailedToQuery"
Continue
}
foreach($member in $members) {
try {
$MemberName = $member.GetType().Invokemember("Name","GetProperty",$null,$member,$null)
$MemberType = $member.GetType().Invokemember("Class","GetProperty",$null,$member,$null)
$MemberPath = $member.GetType().Invokemember("ADSPath","GetProperty",$null,$member,$null)
$MemberDomain = $null
Based on this answer, you can change this part of your code:
foreach($member in $members) {
try {
$MemberName = $member.GetType().Invokemember("Name","GetProperty",$null,$member,$null)
$MemberType = $member.GetType().Invokemember("Class","GetProperty",$null,$member,$null)
....
For this:
$members.ForEach([adsi]).ForEach({
$enabled = switch ($_.class) {
User { ('Enabled', 'Disabled')[[bool]($_.UserFlags.Value -band 2)] }
Default { 'Not Applicable'}
}
[pscustomobject]#{
Name = $_.Name.Value
Class = $_.Class
ADSPath = $_.ADSPath
Enabled = $enabled
}
})
You can add a try / catch logic if you believe it's needed. As also stated in comments, the built-in cmdlet Get-LocalUser already does this for you.

Powershell: Delete class from JAR file with invoke-command

I am trying to remove vulnerable classes from the log4j jar file with powershell.
I am able to remove the file using the script locally on the server, however, I want to remove the class from many paths on many servers and trying to use invoke-command.
The script can open and read the JAR file but doesnt seem to action the delete() method. Is there a way to get powershell to delete the class "remotely"?
Here is my script:
$servers = #(
"server"
)
$class_to_delete = "JMSSink"
$unable_to_connect = #()
Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.Filesystem
write-host "`nTesting connection to:" -ForegroundColor Yellow
$servers | ForEach-Object {Write-Host "$_"}
$servers | ForEach-Object {
$server = $_
try {
write-host "`nTesting $($server)"
Invoke-Command -ComputerName $server -ScriptBlock {
Write-Host "Connection successful to $($env:computername)" -ForegroundColor Green
} -ErrorAction Stop
} catch {
write-host "`nConnection failed to $($server)"
$unable_to_connect += $server
}
}
Write-Host "`nStarting script to remove $($class_to_delete) class from log4j" -ForegroundColor Yellow
$objects_skipped = #()
$servers | ForEach-Object {
$server_node = $_
write-host "`nPut in the file paths for $($server_node)" -ForegroundColor Yellow
$file_locations = (#(While($l=(Read-Host).Trim()){$l}) -join("`n"))
$file_locations | Out-File C:\temp\output.txt #Change this path to the temp folder and file on the server you execute from
$file_objects = Get-Content -Path C:\temp\output.txt #Change this path to the temp folder and file on the server you execute from
$stats_x = foreach ($file_object in $file_objects) {
$stats = Invoke-Command -ComputerName $server_node -ScriptBlock {
Write-Host "`nStarting on $($env:COMPUTERNAME)"
$class = $using:class_to_delete
Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.Filesystem
$ful_path = $using:file_object
$fn = Resolve-Path $ful_path
try {
$zip = [System.io.Compression.ZipFile]::Open("$fn", "Read")
Write-Host "Backing up $($fn) to $($fn).bak"
$zip.Dispose()
Copy-Item "$fn" "$($fn).bak"
$zip = [System.io.Compression.ZipFile]::Open($fn, "Update")
$files = $zip.Entries | Where-Object { $_.name -eq "$($class).class" }
if (!$files) {
write-host "`nNo $($class) class found on $($env:COMPUTERNAME) for path: $($ful_path)"
$files.dispose()
$not_found = #()
$not_found += New-Object -TypeName PSObject -Property #{
Server = $env:COMPUTERNAME;
Path = $ful_path;
Result = "$($class) class NOT FOUND"
}
Write-Output $not_found
} else {
foreach ($file in $files) {
write-host "`n$($class) class found on $($env:COMPUTERNAME) for path: $($ful_path)"
write-host "`nDeleting file $($file)" -ForegroundColor Green
#delete class
$file.delete()
#check if class was successfully deleted
$confirm_delete = $zip.Entries | Where-Object { $_.name -eq "$($class).class" }
write-host $confirm_delete
if ($confirm_delete.name -match "$class.class") {
$deleted_status = "$($class) !!NOT REMOVED!!"
} else {
$deleted_status = "$($class) REMOVED"
}
$Output = #()
$Output += New-Object -TypeName PSObject -Property #{
Server = $env:COMPUTERNAME;
Path = $ful_path;
Result = $deleted_status
}
Write-Output $Output
}
}
} catch {
Write-Host "Cannot open $($ful_path) as a Zip File. $($Error[0])"
}
}
Write-Output $stats
}
$objects_skipped += $stats_x
}
#result
write-host "`nEnd result"
$objects_skipped | select Server,Result,Path | ft -AutoSize
You need to explicitly call Dispose() on the containing archive to persist the updates to the file on disk:
# this goes immediately after the `catch` block:
finally {
if($zip -is [IDisposable]){ $zip.Dispose() }
}
By placing the call to $zip.Dispose() inside a finally block, we ensure that it's always disposed regardless of whether an exception was thrown in the preceding try block.

Adding PSCustomObject to Array gives Error, but works fine when debugging the code in Powershell

I know this question is already answered, but that question didn't answered my question.
The link is Adding PSCustomObject to Array gives Error, but works fine when debugging the code in Visual Studio Code
When I am trying to run the script its executing fine in debug mode but its failing if I am executing directly.
$path = Split-Path $script:MyInvocation.MyCommand.Path
$output_File = $path + "\$outputFile_Name"
$report = #()
function Folder-Access
{
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$true, Position=0)]
[string]$prod_path,
[Parameter(Mandatory=$true, Position=1)]
[string]$non_prod_path
)
Write-Host "`$prod_path value: $prod_path"
Write-Host "`$non_prod_path value: $non_prod_path"
try {
$prod_acl = get-acl $prod_path
$nonProd_acl = Get-Acl $non_prod_path
$arr = #()
foreach ($non_prod_usr in ($nonProd_acl.access) ) {
$x = $non_prod_usr.IdentityReference.Value
$arr += $x
}
foreach ($usr in ($prod_acl.Access)){
$dirObj = New-Object psobject
if ($usr.IdentityReference.Value -in $arr) {
echo "Do Nothing."
} else {
if ($usr.IdentityReference.Value -ne 'BUILTIN\Administrators') {
if ($usr.IdentityReference.Value -ne 'S-1-5-21-2554127390-2720852439-1129525235-1959308') {
$properties = [ordered]#{'Foldername'=$non_prod_path; 'AD Group'=$usr.IdentityReference.Value;
'Permissions'=$usr.Filesystemrights}
echo "Do Something."
$usr.IdentityReference.Value
#$dirObj | Add-Member -MemberType NoteProperty -Name "Foldername" -Value $non_prod_path
#$dirObj | Add-Member -MemberType NoteProperty -Name 'AD Group' -Value $usr.IdentityReference.Value
#$dirObj | Add-Member -MemberType NoteProperty -Name 'Permissions' -Value $usr.Filesystemrights
#$report += $dirObj
$report += New-Object -TypeName psobject -Property $properties
}
}
}
#$report += New-Object -TypeName psobject -Property $properties
} $report | Export-Csv -Path 'C:\Powershell\output.csv' -Append -NoTypeInformation -Encoding UTF8
}
catch {
Write-Host "Error occured " + $_ -ForegroundColor Red
}
}
function executionTimeForSourcePath {
try {
$Prod_Folder_Path = Get-ChildItem -Directory -Path $prod_path -Recurse -Force
$Prod_Folder_Path | ForEach-Object {
if (-not ([string]::IsNullOrEmpty($_.FullName))){
$path = $_.FullName
$folderName = $path.Replace($prod_path + '\',"")
if (-Not (Test-Path "$non_prod_path\$folderName")){
try {
New-Item -ItemType Dir -Path "$non_prod_path\$folderName" -ErrorAction Stop
if (Test-Path "$non_prod_path\$folderName"){
Folder-Access -prod_path $path -non_prod_path "$non_prod_path\$folderName"
}
}
catch {
Write-Host $_ -ForegroundColor Red
}
}
ElseIf (Test-Path "$non_prod_path\$folderName") {
Folder-Access -prod_path $path -non_prod_path "$non_prod_path\$folderName"
}
} }
} catch { $_ }
}
echo "Script started.."
executionTimeForSourcePath
Would be great if someone can guide me here please. I tried all the possible ways but none worked for this.
The error is
Method invocation failed because [System.Management.Automation.
PSObject] does not contain a method named 'op_Addition'.

Powershell Error handling not not working as expected with functions

Looking for advice on error handling in Powershell. I think I understand the concept behind using Try/Catch but I'm struggling on where to utilize this in my scripts or how granular I need to be.
For example, should I use the try/catch inside my functions and if so, should I insert the actions of my function inside the try or do I need to break it
down further? OR, should I try to handle the error when I call my function? Doing something like this:
Try{
Get-MyFunction
} catch{ Do Something"
}
Here's an example of a script I wrote which is checking for some indicators of compromise on a device. I have an application that will launch this script and capture the final output. The application requires the final output to be in the following format so any failure should generate this.
[output]
result=<0 or 1>
msg= <string>
Which I'm doing like this:
Write-Host "[output]"
Write-Host "result=0"
Write-Host "msg = $VariableContainingOutput -NoNewline
Two of my functions create custom objects and then combine these for the final output so I'd like to capture any errors in this same format. If one function generates an error, it should record these and continue.
If I just run the code by itself (not using function) this works but with the function my errors are not captured.
This needs to work on PowerShell 2 and up. The Add-RegMember and Get-RegValue functions called by this script are not shown.
function Get-ChangedRunKey {
[CmdletBinding()]
param()
process
{
$days = '-365'
$Run = #()
$AutoRunOutput = #()
$RunKeyValues = #("HKLM:\Software\Microsoft\Windows\CurrentVersion\Run",
"HKLM:\Software\Wow6432node\Microsoft\Windows\CurrentVersion\Run",
"HKU:\S-1-5-21-*\Software\Microsoft\Windows\CurrentVersion\Run",
"HKU:\S-1-5-21-*\Software\Wow6432node\Microsoft\Windows\CurrentVersion\Run"
)
Try{
$Run += $RunKeyValues |
ForEach-Object {
Get-Item $_ -ErrorAction SilentlyContinue |
Add-RegKeyMember -ErrorAction SilentlyContinue |
Where-Object {
$_.lastwritetime -gt (Get-Date).AddDays($days)
} |
Select-Object Name,LastWriteTime,property
}
if ($Run -ne $Null)
{
$AutoRunPath = ( $Run |
ForEach-Object {
$_.name
}
) -replace "HKEY_LOCAL_MACHINE", "HKLM:" -replace "HKEY_Users", "HKU:"
$AutoRunValue = $AutoRunPath |
Where-Object {
$_ -and $_.Trim()
} |
ForEach-Object {
Get-RegValue -path $_ -Name '*' -ErrorAction SilentlyContinue
}
}
#Build Custom Object if modified Run keys are found
if($AutorunValue -ne $null)
{
foreach ($Value in $AutoRunValue) {
$AutoRunOutput += New-Object PSObject -Property #{
Description = "Autorun"
path = $Value.path
value = $Value.value
}
}
}
Write-Output $AutoRunOutput
}catch{
$AutoRunOutput += New-Object PSObject -Property #{
Description = "Autorun"
path = "N/A"
value = "Error accessing Autorun data. $($Error[0])"
}
}
}
}
function Get-ShellIOC {
[CmdletBinding()]
param()
process
{
$ShellIOCOutput = #()
$ShellIOCPath = 'HKU:\' + '*' + '_Classes\*\shell\open\command'
Try{
$ShellIOCValue = (Get-Item $ShellIOCPath -ErrorAction SilentlyContinue |
Select-Object name,property |
ForEach-Object {
$_.name
}
) -replace "HKEY_LOCAL_MACHINE", "HKLM:" -replace "HKEY_Users", "HKU:"
$ShellIOCDetected = $ShellIOCValue |
ForEach-Object {
Get-RegValue -path $_ -Name '*' -ErrorAction SilentlyContinue
} |
Where-Object {
$_.value -like "*cmd.exe*" -or
$_.value -like "*mshta.exe*"
}
if($ShellIOCDetected -ne $null)
{
foreach ($ShellIOC in $ShellIOCDetected) {
$ShellIOCOutput += New-Object PSObject -Property #{
Description = "Shell_IOC_Detected"
path = $ShellIOC.path
value = $ShellIOC.value
}
}
}
Write-Output $ShellIOCOutput
}catch{
$ShellIOCOutput += New-Object PSObject -Property #{
Description = "Shell_IOC_Detected"
path = "N/A"
value = "Error accessing ShellIOC data. $($Error[0])"
}
}
}
}
function Set-OutputFormat {
[CmdletBinding()]
param()
process
{
$FormattedOutput = $AutoRunOutput + $ShellIOCOutput |
ForEach-Object {
"Description:" + $_.description + ',' + "Path:" + $_.path + ',' + "Value:" + $_.value + "|"
}
Write-Output $FormattedOutput
}
}
if (!(Test-Path "HKU:\")){
try{
New-PSDrive -PSProvider Registry -Root HKEY_USERS -Name HKU -ErrorAction Stop | Out-Null
}catch{
Write-Output "[output]"
Write-Output "result=0"
Write-Host "msg = Unable to Connect HKU drive" -NoNewline
}
}
$AutoRunOutput = Get-ChangedRunKey
$ShellIOCOutput = Get-ShellIOC
$FormattedOutput = Set-OutputFormat
Write-Output "[output]"
if ($FormattedOutput -eq $Null)
{
Write-Output "result=0"
Write-Host "msg= No Items Detected" -NoNewline
}
else
{
Write-Output "result=1"
Write-Host "msg=Items Detected: $($FormattedOutput)" -NoNewline
}
You have to know that there are 2 error types in PowerShell:
Terminating Errors: Those get caught automatically in the catch block
Non-Terminating Error: If you want to catch them then the command in question needs to be execution using -ErrorAction Stop. If it is not a PowerShell command but an executable, then you need to check stuff like the exit code or $?. Therefore I suggest wrapping your entire action in an advanced function on which you then call using -ErrorAction Stop.
Apart from that I would like to remark that PowerShell version 2 has already been deprecated. The reason for why non-terminating errors exists is because there are cases like for example processing multiple objects from the pipeline where you might not want it to stop just because it did not work for one object. And please do not use Write-Host, use Write-Verbose or Write-Output depending on the use case.

PSObject bug with specific parameter

I have a powershell script that does some simple auditing of group membership on remote servers. The output is as expected except in the case of one group.
There are two parameters two this script, an OU to check in AD and a Group name to check. The OU parameter returns a list of server names, the Group name is the group to return members on. This all works fine except in one case, Backup Operators.
param([parameter(mandatory=$true)][string]$region,[string]$group)
### Debug flag for viewing output when running the script.
$DEBUG = 1
$self = $myinvocation.mycommand.name
function cmdopts {
if ($DEBUG) {
write-host "$self running with options"
write-host "Region: $region"
write-host "Group: $group"
}
}
### Function to handle custom messages to the user.
function usageRegion {
# Removed for brevity
}
function usageGroups {
# Removed for brevity
}
### Cleanup from previous run of the script.
function cleanup {
# Removed for brevity
}
#### Function to load powershell modules at runtime
function loadmod {
param([string]$name)
if ( -not(get-module -name $name)) {
if (get-module -listavailable| where-object { $_.name -eq $name}) {
import-module -name $name
$true
} else {
$false
}
} else {
$true
}
}
### Main()
cmdopts
#### Validate commandline options
if ( "cnr","nwr","swr","ner","ser","emr","lar","apr" -notcontains $region ) {
usageRegion
exit
}
if ( "Administrators","Backup Operators","Event Log Readers","Hyper-V Administrators","Power Users",
"Print Operators","Remote Desktop Users" -notcontains $group) {
usageGroups
exit
} else {
### We are creating three files for each run, previous runs need to be cleaned up before we start.
cleanup
### The ActiveDirectory module is a dependency for this script, we use it to get a list of machine names from AD for the OU.
if ( loadmod -name "activedirectory" ) {
write-host "Loading ActiveDirectory powershell module..." -foregroundcolor green
} else {
write-host "Sorry, you do not have the ActiveDirectory powershell module installed." -foregroundcolor yellow
write-host "The script cannot contnue." -foregroundcolor yellow
exit
}
### Get the list of servers from AD for the OU specified by the user.
get-adcomputer -f * -searchbase "ou=$region,ou=servers,dc=domain,dc=com" | select name | out-file "c:\scripts\ps\$region.srvtmp.txt" -append
### We need to fix some format issues with the file before continuing
# Removed for brevity, cleans up the file output from get-adcomputer and sets variable $srvlist
$srvlist = gc "c:\scripts\ps\$region.srvlist.txt"
# Store for the return
$store = #()
# Fix the group string for the filename
$filestring = $group
$filestring = $filestring.replace(' ', '')
$filestring = $filestring.tolower()
foreach ( $srv in $srvlist ) {
if ( $srv -eq "bustedserver" ) {
# This box hangs and does not tear down WMI when it can't complete, timeout does not work
write-host "skipping $srv"
} else {
$response = test-connection $srv -count 1 -quiet
### This does not work super well, might have to try a custom function
if ($response -eq $false ) {
write-host "$srv was offline during test" -foregroundcolor darkmagenta
} else {
write-host "Checking $group on " -nonewline; write-host $srv -foregroundcolor cyan
$groupinfo = new-object PSObject
$members = gwmi -computer $srv -query "SELECT * FROM Win32_GroupUser WHERE GroupComponent=`"Win32_Group.Domain='$srv',Name='$group'`""
$members = $members | sort-object -unique
$count = 0
if ($members -ne $null) {
add-member -inputobject $groupinfo -membertype noteproperty -name "Server" -value $srv
add-member -inputobject $groupinfo -membertype noteproperty -name "Group" -value $group
foreach ($member in $members) {
$count += 1
$data = $member.partcomponent -split "\,"
$domain = ($data[0] -split "=")[1]
$name = ($data[1] -split "=")[1]
$line = ("$domain\$name").replace("""","")
add-member -inputobject $groupinfo -membertype noteproperty -name "Member $count" -value $line
}
}
if ($DEBUG) {
write-host $groupinfo
}
$store += $groupinfo
}
}
}
}
#$store | export-csv -path "$HOME\desktop\$region-$filestring-audit.csv" -notype
$store
If I run this script against a group like Administrators, or Remote Desktop Users the output looks like the following.
Server: SERVER1
Group: Remote Desktop Users
Member1: GroupName1
Member2: GroupName2
Member3: GroupName3
If I run this script against the group Backup Operators, I only get the first group even if there are many. In the debug write-host statement, it will show all of the groups. When printing the store, it only shows the first one. Even if there are two or more, it will alsways print...
Server: SERVER1
Group: Backup Operators
Member1: GroupName1
Any ideas on why this is broken specifically for 'Backup Operators' and not others would be appreciated.