I am new to powershell, and I am attempting to create a CSV from an excel file for comparison sake to see who is currently logged into a computer. However, I'm running into an odd issue where sometimes the script will pull the same user multiple times even if they've never logged into a computer. Here is my full code. I know there is a lot of optimization that can be done (and parts that need to be removed, those should be noted). I assume I've misused Get-WMIObject or something similar, can anyone see why it would pull that information like it is?
$csvRunFile = "test.csv"
$output = "Results_$(Get-Date -format yyyy.MM.dd).csv"
#Import the created csv.
$csv = import-csv $CsvRunFile
$results = foreach($csv_line in $csv) {
$ctag = $csv_line.ctag
$test_ping = test-connection $ctag -Count 1 -Quiet
#If the computer is pingable (IE: Online)
switch ($test_ping) {
$true {
#Pull the actual logged in user.
$Username = (Get-WmiObject -ComputerName $ctag -Class Win32_ComputerSystem).Username.Split("\\")[1]
#If the last modified folder is 'public' put an error, otherwise pull the username's information from AD.
#This was from when it pulled from the \User folder rather than the last log in, this is probably removeable.
if ($Username -eq "Public") {
$ADName = "No User"
} else {
$ADName = Get-ADUser -Identity $Username
$ADName = $ADName.Name
} #end If
}#end Switch:True
#Show there was an error when pinging the computer.
$false {$ADName = "ERROR"}
}#end Switch
#write the results the new output CSV.
$result = [PSCustomObject]#{
CTAG = $ctag
Username = $ADName
}#end PSCustom Object
$result
} #end foreach
#Turn the .txt into a CSV so it can be manually compared to the list in the original excel file.
$results | Export-Csv -path $output
There might be a leftover-value in $UserName for some reason or $ADName is still it's old value because you tried to run Get-ADUser -Identity $null when there was no user logged on (WMI returns $null when there's no user logged in).
I also changed your ping-test from a switch to an if-test just to cleanup the code. I've never seen Public being returned, but I left it as it doesn't really hurt either.
Try:
#If the computer is pingable (IE: Online)
if($test_ping) {
#Clear username var just to be safe
$Username = $null
#Pull the actual logged in user.
$Username = (Get-WmiObject -ComputerName $ctag -Class Win32_ComputerSystem).Username | ? { $_ } | % { $_.Split("\\")[1] }
#If the last modified folder is 'public' put an error, otherwise pull the username's information from AD.
#This was from when it pulled from the \User folder rather than the last log in, this is probably removeable.
if (($Username -eq "Public") -or ($Username -eq $null)) {
$ADName = "No User"
} else {
$ADName = Get-ADUser -Identity $Username
$ADName = $ADName.Name
} #end If username public
} else { $ADName = "ERROR"}
Related
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
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.
I have problems of understanding values of variables in PowerShell and I check them with if statements.
$LDAPDirectoryService = '10.10.XXX.XXX:389'
$DomainDN = 'o=Enterprise'
#$LDAPFilter = '(&(objectCategory=Person)(memberOf=cn=alc-01-Planung-rw,ou=KT,o=enterprise))'
$LDAPFilter = '(&(cn=alc-01-Planung-rw))'
$null = [System.Reflection.Assembly]::LoadWithPartialName('System.Net')
$LDAPServer = New-Object System.DirectoryServices.Protocols.LdapConnection $LDAPDirectoryService
$LDAPServer.AuthType = [System.DirectoryServices.Protocols.AuthType]::Anonymous
$Scope = [System.DirectoryServices.Protocols.SearchScope]::Subtree
$AttributeList = #('*')
$SearchRequest = New-Object System.DirectoryServices.Protocols.SearchRequest -ArgumentList $DomainDN,$LDAPFilter,$Scope,$AttributeList
$groups = $LDAPServer.SendRequest($SearchRequest)
$groups
if ($groups -eq $null) {"No Group found"}
if ($groups -eq " ") {"No Group found"}
foreach ($group in $groups.Entries) {
$users = $group.attributes['member'].GetValues('string')
foreach ($user in $users) {
Write-Host $user
}
}
I want to check if the group exists and then if users are existing in this group. I tried many statements but none of them seem to work.
It's not null or blank, even when nothing is written down in the console.
This is what I got when I use group which doesn't exist:
Can anybody show me a solution?
What version of PowerShell are you running? Why are you not using the built-in AD group cmdlets for this or are you not using ADDS but some other LDAP service?
Or you may be on OSX/Linux and are using PSCore, which the ADDS/RSAT cmdlets are not there, well, not yet?
For your goals of …
I want to check if the group exists and then if users are existing in
this group.
… On Windows, with PowerShell 3x or higher, it's really only this...
# Get all AD groups and all members of each group
Clear-Host
(Get-ADGroup -Filter '*').Name |
%{
"`n*** The members of $PSItem are as follows: ***`n"
If((Get-ADGroupMember -Identity $PSItem).Count -ge 1)
{
(Get-ADGroupMember -Identity $PSItem).SamAccountName
}
Else
{
Write-Warning -Message "$PSItem does not exist or has no members."
}
}
# Filtered
Clear-Host
((Get-ADGroup -Filter '*').Name -match 'Domain Admins|Domain Users' ) |
%{
"`n*** The members of $PSItem are as follows: ***`n"
If((Get-ADGroupMember -Identity $PSItem).Count -ge 1)
{
(Get-ADGroupMember -Identity $PSItem).SamAccountName
}
Else
{
Write-Warning -Message "$PSItem does not exist or has no members."
}
}
Using your LDAP approach though... How about this...
'Administrators','Distributed COM Users' |
ForEach {
# Define LDAP search root, the Global catalog of the domain
$sLDAPSearchRoot = "LDAP://$((Get-ADDomainController).IPv4Address):389"
# The Groupname to looking for
($sGroupName = "$_")
# The LDAP query - query string
$sSearchStr = "(&(objectCategory=group)(name="+$sGroupName+"))"
# Get the search object
$oSearch = New-Object directoryservices.DirectorySearcher($oADRoot,$sSearchStr)
# Looking for the group
$oFindResult = $oSearch.FindAll()
# On success, get a DirectoryEntry object for the group
$oGroup = New-Object System.DirectoryServices.DirectoryEntry($oFindResult.Path)
# And list all members
If (($oGroup.Member).Count -ge 1)
{
$oGroup.Member |
%{($oMembers = New-Object System.DirectoryServices.DirectoryEntry($sLDAPSearchRoot+"/"+$_))}
}
Else
{ Write-Warning -Message "$($oGroup.Member) does not exist or has no members"}
}
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.
I am working on a side project and to make it easier for managment since almost all of out server names are 15 charactors long I started to look for an RDP managment option but none that I liked; so I started to write one and I am down to only one issue, what do I do to manage if the user types not enough for a search so two servers will match the Query. I think I will have to put it in an array and then let them select the server they meant. Here is what I have so far
function Connect-RDP
{
param (
[Parameter(Mandatory = $true)]
$ComputerName,
[System.Management.Automation.Credential()]
$Credential
)
# take each computername and process it individually
$ComputerName | ForEach-Object{
Try
{
$Computer = $_
$ConnectionDNS = Get-ADComputer -server "DomainController:1234" -ldapfilter "(name=$computer)" -ErrorAction Stop | Select-Object -ExpandProperty DNSHostName
$ConnectionSearchDNS = Get-ADComputer -server "DomainController:1234" -ldapfilter "(name=*$computer*)" | Select -Exp DNSHostName
Write-host $ConnectionDNS
Write-host $ConnectionSearchDNS
if ($ConnectionDNS){
#mstsc.exe /v ($ConnectionDNS) /f
}Else{
#mstsc.exe /v ($ConnectionSearchDNS) /f
}
}
catch
{
Write-Host "Could not locate computer '$Computer' in AD." -ForegroundColor Red
}
}
}
Basically I am looking for a way to manage if a user types server1
that it will ask does he want to connect to Server10 or Server11 since both of them match the filter.
Another option for presenting choices to the user is Out-GridView, with the -OutPutMode switch.
Borrowing from Matt's example:
$selection = Get-ChildItem C:\temp -Directory
If($selection.Count -gt 1){
$IDX = 0
$(foreach ($item in $selection){
$item | select #{l='IDX';e={$IDX}},Name
$IDX++}) |
Out-GridView -Title 'Select one or more folders to use' -OutputMode Multiple |
foreach { $selection[$_.IDX] }
}
else {$Selection}
This example allows for selection of multiple folders, but can you can limit them to a single folder by simply switching -OutPutMode to Single
I'm sure what mjolinor has it great. I just wanted to show another approach using PromptForChoice. In the following example we take the results from Get-ChildItem and if there is more than one we build a collection of choices. The user would select one and then that object would be passed to the next step.
$selection = Get-ChildItem C:\temp -Directory
If($selection.Count -gt 1){
$title = "Folder Selection"
$message = "Which folder would you like to use?"
# Build the choices menu
$choices = #()
For($index = 0; $index -lt $selection.Count; $index++){
$choices += New-Object System.Management.Automation.Host.ChoiceDescription ($selection[$index]).Name, ($selection[$index]).FullName
}
$options = [System.Management.Automation.Host.ChoiceDescription[]]$choices
$result = $host.ui.PromptForChoice($title, $message, $options, 0)
$selection = $selection[$result]
}
$selection
-Directory requires PowerShell v3 but you are using 4 so you would be good.
In ISE it would look like this:
In standard console you would see something like this
As of now you would have to type the whole folder name to select the choice in the prompt. It is hard to get a unique value across multiple choices for the shortcut also called the accelerator key. Think of it as a way to be sure they make the correct choice!