Powershell problem with values comparison in ARS - false positive - powershell

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.

Related

How to speed up the process in PowerShell

I've a simple script that generate orphan OneDrive report. It's working but it's very slow so I'm just wondering how could I modify my script to speed up the process. Any help or suggestion would be really appreciated.
Basically this is what I'm doing:
Get the owner email from "Owner" column and check it using AzureAD to see if I get any error
If I get an error then check it in on-prem ADGroup
If that owner is existed in the on-prem ADGroup then it's an orphan
Export only that user to a new csv file
$ImportData = "E:\scripts\AllOneDriveUser.csv"
$Report = "E:\scripts\OrphanOneDrive.csv"
$CSVImport = Import-CSV $ImportData
ForEach ($CSVLine in $CSVImport) {
$CSVOwner = $CSVLine.Owner
try{
Get-AzureADUser -ObjectId $CSVOwner
}catch{
$StatusMessage = $_.Exception.Message
if($Null -eq $StatusMessage){
Write-Host "User Found, Ignore from orphan list."
}else{
#Owner not found in AzureAD
$group = 'TargetGroup'
$filter = '(memberof={0})' -f (Get-ADGroup $group).DistinguishedName
$filterName = Get-ADUser -LDAPFilter $filter
$ModifiedOwner = $CSVOwner -split"#"[0]
if( $ModifiedOwner[0] -in $filterName.Name ){
Write-host "Adding it into orphaned list"
$CSVLine | Export-Csv $Report -Append -notypeinformation -force
}else{
Write-Host "Not orphaned"
}
}
}
}
I have over 8000 record in my import csv file and over 5000 member in my on-prem AD group so it taking very long.
You can greatly improve your script by using a HashSet<T> in this case, but also the main issue of your code is that you're querying the same group over and over, it should be outside the loop!
There is also the use of Export-Csv -Append, appending to a file per loop iteration is very slow, better to streamline the process with the pipelines so Export-Csv receives the objects and exports only once instead of opening and closing the FileStream everytime.
Hope the inline comments explain the logic you can follow to improve it.
$ImportData = "E:\scripts\AllOneDriveUser.csv"
$Report = "E:\scripts\OrphanOneDrive.csv"
# we only need to query this once! outside the try \ catch
# a HashSet<T> enables for faster lookups,
# much faster than `-in` or `-contains`
$filter = '(memberof={0})' -f (Get-ADGroup 'GroupName').DistinguishedName
$members = [Collections.Generic.HashSet[string]]::new(
[string[]] (Get-ADUser -LDAPFilter $filter).UserPrincipalName,
[System.StringComparer]::OrdinalIgnoreCase
)
Import-CSV $ImportData | ForEach-Object {
# hitting multiple times a `catch` block is expensive,
# better use `-Filter` here and an `if` condition
$CSVOwner = $_.Owner
if(Get-AzureADUser -Filter "userprincipalname eq '$CSVOwner'") {
# we know this user exists in Azure, so go next user
return
}
# here is for user not found in Azure
# no need to split the UserPrincipalName, HashSet already has
# a unique list of UserPrincipalNames
if($hash.Contains($CSVOwner)) {
# here is if the UPN exists as member of AD Group
# so output this line
$_
}
} | Export-Csv $Report -NoTypeInformation

Powershell: Attempting to update all sAMAccountNames in an OU; nothing past the initial "if" is working

Due to some recent changes in organizational standards, I'm having to update the sAMAccountName of every user in my location's Users OU. I've written and run the code, testing it against my own account several times, but have run into consistent issues with the else, Write-Host, and Export-CSV commands.
$OU = 'users ou'
$users = Get-ADObject -Filter * -SearchBase $OU -Properties sn,givenName,userPrincipalName,sAMAccountName,employeeType
$newPre2K = ''
$memberName = ''
foreach($user in $users)
{
$memberName = $user.sn + ", " + $user.givenName
$newPre2K = (($user.userPrincipalName).Split(".")[0]) + $user.employeeType
Try
{
if($user.sAMAccountName -ne $newPre2K)
{
$user.sAMAccountName = $newPre2K
Set-ADObject -Instance $user
Write-Host $memberName + ": sAMAccountName change successful." -ForegroundColor Green | Export-CSV -Path "C:\Users\(me)\Desktop\sAMAccUpdate.csv" -NoTypeInformation -Append
}
else
{
Write-Host $memberName + ": Member sAMAccountName has already been updated." -ForegroundColor Yellow | Export-CSV -Path "C:\Users\(me)\Desktop\sAMAccUpdate.csv" -NoTypeInformation -Append
}
}
Catch
{
Write-Host $memberName + ": Unknown Error. Review CSV to identify failed changes and attempt to remediate." -ForegroundColor Red | Export-CSV -Path "C:\Users\(me)\Desktop\sAMAccUpdate.csv" -NoTypeInformation -Append
}
}
The initial if statement functions exactly as I want it to, as does the Write-Host. However, the Export-CSV afterwards, while it does at least create the CSV file, does not put anything in it. Additionally, even after running it several times in a row, which would basically force an "else" result as my sAMAccountName was already updated and therefore doesn't fulfill the -ne argument in the if statement, I don't get any output from Write-Host, and nothing goes into the CSV—though it does create it if it doesn't already exist.
I've tried changing the else statement to elseif($user.sAMAccountName -eq $newPre2K), and the result is the same: nothing from Write-Host, nothing in the CSV. I've tried both altogether removing the Try...Catch as well as placing it within the initial if and still nothing changes. I've tried swapping Write-Host/Export-CSV with Write-Output/Out-File, and that also gives me nothing.
If anyone could help me figure out the probably very obvious mistake I'm making, for both the else/elseif statement as well as the Write-Host/Export-CSV piece, I'd greatly appreciate it.
I was right, it was something really obvious I was missing.
When I was testing the code against my own account, I was specifying myself in the -Filter argument of $users = Get-ADObject -Filter * .... However, the reason the else statement "wasn't working" was purely because I wasn't changing my sAMAccountName in the argument to reflect the fact that it had been changed, so it was looking for an object that just didn't exist. Obviously a program can't tell you whether something has been changed already or not if the thing you're specifically telling it to look for no longer exists.
As for the Write-Host/Export-CSV piece, I just did it again with Write-Output/Out-File and it works now. It didn't before, so I must have been mucking something up, but I'm unsure what.

Pass a variable as a switch parameter in Powershell

I'm trying to create a powershell script that will grab all Active Directory accounts that are enabled, and inactive for 90 days. The script will prompt the user to choose between querying computer or user accounts.
Depending on the choice, it will pass it over to the main command as a variable.
The commands work correctly if I don't pass a variable.
I'm not sure if what I'm trying to do is possible.
Sorry for any bad code formatting. Just starting out.
Clear-Host
write-host "`nProgram searches for Enabled AD users account that have not logged in for more than 90 days. `nIt searches the entire domain and saves the results to a CSV file on users desktop." "`n"
$choice = Read-host -Prompt " What do you want to search for Computer or Users Accounts`nType 1 for users`nType 2 for Computers`n`nChoice"
$account
if ($choice -eq 1) {
$account = UsersOnly
}
Elseif ($choice -eq 2) {
$account = ComputersOnly
}
Else {
write-host "This is not an option `n exiting program"
exit
}
$FileName = Read-Host -Prompt "What do you want to name the CSV file"
$folderPath = "$env:USERPROFILE\Desktop\$FileName.csv"
Search-ADAccount -AccountInactive -TimeSpan 90 -$account | Where-Object { $_.Enabled -eq $true } | select Name, UserPrincipalName, DistinguishedName | Export-Csv -Path $folderPath
Splatting is the way to achieve this. It's so named because you reference a variable with # instead of $ and # kind of looks a "splat".
it works by creating a hashtable, which is a type of dictionary (key/value pairs). In PowerShell we create hashtable literals with #{}.
To use splatting you just make a hashtable where each key/value pair is a parameter name and value, respectively.
So for example if you wanted to call Get-ChildItem -LiteralPath $env:windir -Filter *.exe you could also do it this way:
$params = #{
LiteralPath = $env:windir
Filter = '*.exe'
}
Get-ChildItem #params
You can also mix and match direct parameters with splatting:
$params = #{
LiteralPath = $env:windir
Filter = '*.exe'
}
Get-ChildItem #params -Verbose
This is most useful when you need to conditionally omit a parameter, so you can turn this:
if ($executablesOnly) {
Get-ChildItem -LiteralPath $env:windir -Filter *.exe
} else {
Get-ChildItem -LiteralPath $env:windir
}
Into this:
$params = #{
LiteralPath = $env:windir
}
if ($executablesOnly) {
$params.Filter = '*.exe'
}
Get-ChildItem #params
or this:
$params = #{}
if ($executablesOnly) {
$params.Filter = '*.exe'
}
Get-ChildItem -LiteralPath $env:windir #params
With only 2 possible choices, the if/else doesn't look that bad, but as your choices multiply and become more complicated, it gets to be a nightmare.
Your situation: there's one thing I want to note first. The parameters you're trying to alternate against are switch parameters. That means when you supply them you usually only supply the name of the parameter. In truth, these take boolean values that default to true when the name is supplied. You can in fact override them, so you could do Search-ADAccount -UsersOnly:$false but that's atypical.
Anyway the point of mentioning that is that it may have been confusing how you would set its value in a hashtable for splatting purposes, but the simple answer is just give them a boolean value (and usually it's $true).
So just changing your code simply:
$account = if ($choice -eq 1) {
#{ UsersOnly = $true }
} elseif ($choice -eq 2) {
#{ ComputersOnly = $true }
}
# skipping some stuff
Search-ADAccount -AccountInactive -TimeSpan 90 #account
I also put the $account assignment on the left side of the if instead of inside, but that's your choice.

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

Remote Powershell to retrieve specific registry value from lots of servers

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.