Catching errors within powershell script - powershell

I have written a script to pull permissions from file directories so that we can audit access to folders. I just want to see what groups we have not what users so I wrote this script to pull out all group names and remove the domain name from the value so that it can then run it through a second script that corrects the AD group name for us if its incorrect at all since we ran into an issue where for some reason some were coming back with slightly different names. The problem is all the AD users named in permissions come back as errors. I want those errors to not even show up on screen is there a way to do that? As you can see I have been trying a few different ways to pipe them to a log or the -ea ignore option but it still shows the errors on screen.
$filelocationscsv = "C:\AD\Excel\File Share migration.csv"
$filelocationcsvcontents = Get-Content -LiteralPath $filelocationscsv
$AllFolders = #()
foreach ($location in $filelocationcsvcontents) {
$AllFolders += $location.Substring(0,$location.Length-1)
}
$outputfilelocation = "C:\AD\Excel\permissions.csv"
$Results = #()
$errResults = #()
Foreach ($i in $Allfolders) {
if (Test-Path $i){
Write-Host "Obtaining file permissions for $i."
$acl = (Get-Acl $i -Filter *).Access | select -ExpandProperty IdentityReference
foreach($Access in $acl) {
if ($Access.Value -notlike "BUILTIN\Administrators" -and $Access.Value -notlike "domain\Domain Admins" -and $Access.Value -notlike "CREATOR OWNER" -and $access.Value -notlike "NT AUTHORITY\SYSTEM" -and $access.Value -notlike "Everyone" -and $access.Value -notlike "BUILTIN\Users" -and $access.Value -notlike "s-1*") {
[string]$perm = $Access.Value.Split('\')[1]
if($checkgroup = Get-ADGroup $perm){
#try
#{
## if( $LASTEXITCODE -gt 0 ){
## # Handle the error here
## # This example writes to the error stream and throws a terminating error
## $errResults += $LASTEXITCODE
## Write-Error "Unable to ping server, ping returned" -EA Ignore
## }
$Properties = [ordered]#{'AD Group'=$perm}
$Results += New-Object -TypeName PSObject -Property $Properties
#}
#Catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException]
#{
# Write-Verbose "$perm skipped." -Verbose
# #$ErrorMessage =
# #$FailedItem = $_.Exception.ItemName
# #$errResults += $ErrorMessage + $FailedItem
#}
}
}
}
}
else {
Write-Host "$i is not accessible"
}
}
$Results | select -Property 'AD Group' -Unique | Export-Csv $outputfilelocation -NoTypeInformation
Its worth noting these errors do not stop my script from running its more of an aesthetic function as well as a learning opportunity for myself. I can use my script like it is but I would love to make it look cleaner and learn how to handle errors better.

As you
indicate you are interested in learning more about error handling, one thing I learned this week are these common Parameters for error handling and recording:
-ErrorAction
-WarningAction
-ErrorVariable
-WarningVariable
You can silence the error messages by using the parameter -ErrorAction SilentlyContinue but capture the error by using the parameter -ErrorVariable
EXAMPLE: get-adgroup -ErrorAction SilentlyContinue -ErrorVariable MyErrors
You can read and manipulate the errors by calling $MyErrors
The warnings work the same way
It might give an alternative to Try/Catch.

Thank you #pwnosh you're a genius!
I changed line 20 to
if($errResults += try {$checkgroup = Get-ADGroup $perm -ErrorAction Stop } catch {[Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException]}){
This line forces the users into my CSV as well but then the second script cleans them out anyways with
$results = #()
foreach($Group in ($Groups = Import-csv C:\AD\Excel\permissions.csv)){
$groupname = $Group.'AD Group'
Write-Host "Confirming $groupname group name in AD."
$results += get-adgroup -Filter "name -like '$groupname'" -Properties * -SearchBase "dc=domain,dc=local,dc=net" | select name
}
$results | Export-Csv C:\AD\Excel\ADGroups.csv -NoTypeInformation

Related

Reducing memory consumption of PowerShell script

I know this is pulling quite a bit of data, but at present it's capping my memory consumption when I run it on my local machine. The good news is, it's returning the output that I need. Can someone help me with performance optimization? So far, I haven't done much for fear of messing up a script that returns my desired output. Thanks in advance for any suggestions.
#// Start of script
#// Get year and month for csv export file
$DateTime = Get-Date -f "yyyy-MM"
#// Set CSV file name
$CSVFile = "C:\Temp\AD_Groups"+$DateTime+".csv"
#// Create emy array for CSV data
$CSVOutput = #()
Measure-Command {
#// Get all AD groups in the domain
$ADGroups = Get-ADGroup -Filter "GroupScope -ne 'DomainLocal' -AND GroupCategory -eq 'Security' -AND Member -like '*'" -SearchBase "OU=SHS, DC=shs, DC=net" -Properties Member #-ResultSetSize 1000 Name -like '*''s*' -AND
#// Set progress bar variables
$i=0
$tot = $ADGroups.count
foreach ($ADGroup in $ADGroups) {
#// Set up progress bar
$i++
$status = "{0:N0}" -f ($i / $tot * 100)
Write-Progress -Activity "Exporting AD Groups" -status "Processing Group $i of $tot : $status% Completed" -PercentComplete ($i / $tot * 100)
#// Ensure Members variable is empty
$Members = ""
#// Get group members which are also groups and add to string
$MembersArr = Get-ADGroup $ADGroup.DistinguishedName -Properties Member | Select-Object -ExpandProperty Member
if ($MembersArr) {
foreach ($Member in $MembersArr) {
$ADObj = Get-ADObject -filter {DistinguishedName -eq $Member}
#// Initialize regex variable
$matches = ""
if ($ADObj.ObjectClass -eq "user") {
$UserObj = Get-ADObject -filter {DistinguishedName -eq $Member}
$match = $UserObj -match '\([a-zA-Z0-9]+\)'
$empid=$matches[0] -replace ".*\(","" -replace "\)",""
if ($UserObj.Enabled -eq $False) {
continue
}
$Members = $empid
}
# Check for null members to avoid error for empty groups
if ([string]::IsNullOrEmpty($Members)) {
continue
}
$HashTab = [ordered]#{
"GroupName" = $ADGroup.Name -replace "'s", "''s"
"GroupCategory" = $ADGroup.GroupCategory
"GroupScope" = $ADGroup.GroupScope
"MemberID" = if([string]::IsNullOrEmpty($empid)){""}
else{$empid}
}
#// Add hash table to CSV data array
$CSVOutput += New-Object PSObject -Property $HashTab
}
}
#// Export to CSV files
$CSVOutput | Sort-Object Name, Member | Export-Csv $CSVFile -NoTypeInformation
}
}
I've experienced this too with code that loops through thousands of accounts. The problem is that the garbage collector doesn't have time during the loop to clean up, since your code is constantly doing something. In .NET, I'd call .Dispose() manually to make sure stuff is cleaned up, but here you can't.
You can try calling [System.GC]::Collect() after you assign each variable in the loop. For example, after $MembersArr = and after $ADObj = to (hopefully) make it deallocate the memory used for the previous value.
Also, I think that $UserObj = Get-ADObject... line should be calling Get-ADUser, not Get-ADObject. As it is, $UserObj.Enabled will never have a value and your continue will never be hit.
But you can save yourself the use of Get-ADUser entirely by asking for the userAccountControl value in Get-ADObject and using that to determine if the user is disabled. For example:
$ADObj = Get-ADObject -filter {DistinguishedName -eq $Member} -Properties userAccountControl
# Clean up the old $ADObj value
[System.GC]::Collect()
#// Initialize regex variable
$matches = ""
if ($ADObj.ObjectClass -eq "user") {
$match = $ADObj -match '\([a-zA-Z0-9]+\)'
$empid=$matches[0] -replace ".*\(","" -replace "\)",""
if ($ADObj.userAccountControl -band 2) {
continue
}
$Members = $empid
}
The $ADObj.userAccountControl -band 2 condition checks is a bitwise AND comparison to check if the second bit of the userAccountControl value is set, which means that the account is disabled.

Checking if PC exists in AD, and if not creating it

Need to write a script to take an input file (text) list of names, check if it exists in AD, and create new computers.
The requirements are as follows -
Computer names are based on the users name (input from file)
Names must be 15 characters (for name resolution)
if the truncated name doesnt exist, create a computer object in specific OU with the truncated name.
If the truncated name does exist, append -# and test to see if it exists until it finds one that doesnt, then create new computer object with that name.
At the end I will need to output the results to an array but I haven't started adding that yet since this doesn't work.
So I finally got the "else" part but the if part at the beginning does not work.
$users = get-content C:\scriptdata\VMS.txt
$OU = ************
foreach ($user in $users)
{
$cleanname = if ($user.Length -gt 15) { $user.Substring(0, 15) } else { $user }
$exist = (get-adcomputer $cleanname) -eq $null
if ((get-adcomputer $cleanname) -like "get-adcomputer : Cannot find an object with identity")
{
New-ADComputer -Name $cleanname -Path "$OU" -SAMAccountName $cleanname -confirm
}
else
{
$count=0
DO{
$count++
$cleanname13 = if ($user.Length -gt 13) { $user.Substring(0, 13) } else { $cleanname }
$cleannamedash = $cleanname13 + '-' + "$count"
}
UNTIL ((get-adcomputer $cleannamedash | out-null) -eq $null)
New-ADComputer -Name $cleannamedash -Path "$OU" -SAMAccountName $cleannamedash -confirm
}
}
currently works for -# but not for those that dont exist at all.
Have a look at Naming conventions in Active Directory for computers, domains, sites, and OUs.
You'll find that there is more to a valid computer name than just the length.
Mind that the New-ADComputer cmdlet creates a new computer object, but does not join a computer to a domain.
Something like this should work (untested)
$computers = Get-Content C:\scriptdata\VMS.txt | Where-Object { $_ -match '\S'}
$OU = ************
foreach ($name in $computers) {
$newName = ($name -creplace '[\\/:*?"<>|.]','').Substring(0, 15)
try {
$computer = Get-ADComputer -Filter "Name -eq '$newName'" -PassThru -ErrorAction Stop
}
catch {
$computer = $null
}
if ($computer) {
# a computer with that name already exists, create a new name by adding a dash and two digit number
$count = 0
$name12 = $newName.Substring(0, 12) # we're going to add three characters
# get an array of computernames that are like the one you want to create
$existingComputers = Get-ADComputer -Filter "Name -like '$name12-*'" | Select-Object -ExpandProperty Name
do {
$newName = '{0}-{1:00}' -f $name12, ++$count
}
until ($existingComputers -notcontains $newName -or $count -gt 99)
if ($count -gt 99) {
$newName = '{0}-XX' -f $name12
throw "Cannot create computer $newName because all index numbers 00..99 are taken.."
}
}
# use splatting, because New-ADComputer has MANY parameters
$props = #{
'Name' = $newName
'Path' = $OU
'SamAccountName' = $newName
'Enabled' = $true
'Confirm' = $true
}
Write-Host "Creating computer '$newName'"
New-ADComputer #props
}
I assume you mean that this is the line that's not working:
if ((get-adcomputer $cleanname) -like "get-adcomputer : Cannot find an object with identity")
And even this doesn't work:
$exist = (get-adcomputer $cleanname) -eq $null
The reason is the same in both cases: If the computer doesn't exist, then Get-ADComputer throws an exception and the comparison is never done.
There is a good article about this here, but in short, the solution is to catch the exception. For you, it would look something like this:
try {
$computer = Get-ADComputer $cleanname
# If we get here, we know it exists
# You can put your loop here and just keep looping until Get-ADComputer throws an exception
}
catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException]
{
# Whatever you last tried doesn't exist. Create it here.
}

Problem with powershell script for AD server Status

I have a script that can have a list of AD servers (with Get-ADComputer) and the results goes to a TXT file. I don't know how to only have Online Servers only. I only need their names.
I tried to do some IF {} Else{} with the cmdlet Test-Connection -CN $Server but it doesn't work (I'm probably doing it wrong). Here is my code :
$TXTFile = "C:\Scripts\Serv.txt"
$TXTOutput = #()
Write-Host "INFO: Finding servers from AD"
$Servers = Get-ADComputer -Filter {OperatingSystem -like "*server*" -and Enabled -eq $true} | SORT Name
Write-Host "INFO:"$Servers.Count"Records found"
ForEach ($Server in $Servers) {
$ServerHash = $NULL
$ServerHash = [ordered]#{
"Servers Name" = $Server.Name
}
$TXTOutput += New-Object PSObject -Property $ServerHash
}
$TXTOutput
I want, if possible, to have all of my AD Online Servers name in a TXT file. For now I only have all of my servers (Online and Offline). It's my first post so sorry if I made it wrong !
You can use -Quiet parameter with Test-Connection cmdlet in order to get just True or False and then make a decision based on that result.
$TXTFile = "C:\Temp\Serv.txt"
$TXTOutput = #()
$servers=Get-ADComputer -Filter {OperatingSystem -like "*server*" -and Enabled -eq $true} | select -expandproperty Name
ForEach ($Server in $Servers) {
if ((Test-Connection $Server -Count 2 -Quiet) -eq $true) {
$TXTOutput += $Server
}
}
$TXTOutput | Out-File $TXTFile
You can pipe $TXTOutput to sort if you want. Keep in mind that this might take a while since you are basically pinging each server twice -Count 2.

Script not assigning WMI objects to array correctly

I am trying to create a script that detects domain user profiles that are up to 15 days old and removes them. This will be bundled in a job that will log out any idle sessions before it runs.
I am aware that this would normally be done via GPO, however it is not fit for purpose for this particular business area for various reasons.
Here is my code:
#Assign how old the user profile must be for deletion
[int]$NoOfDays = 15
#Get WMI object where it is a domain account and over 15 days old
$Objects = #(Get-WmiObject -Class Win32_UserProfile | Where {
(!$_.Special) -and
$_.ConvertToDateTime($_.LastUseTime) -lt (Get-Date).AddDays(-15)
})
if ($Objects -eq $null) {
#If no users returned, write to host.
Write-Host "no profiles found for deletion"
} else {
#If users are found, do the following
foreach ($Object in $Objects) {
Write-Host "'$($object.LocalPath)' has been identified for deletion"
}
foreach ($Object in $Objects) {
try {
Write-Host "Attempting to delete profile '$($Object.LocalPath)'"
Remove-WmiObject
Write-Host "Deleted profile '$($Object.LocalPath)' successfully."
} catch {
Write-Host "Unable to delete profile '$($Object.LocalPath)'" -ErrorAction Continue
}
}
}
There is no output, it just returns to command line with no error straight away.
I have attempted commenting out the bit that removes the profile, ending the else after the first for each statement to see what is identified for deletion however it has not worked.
$Objects = #(...) ensures that $Objects is an array, even if the command doesn't return a result. An array (even an empty array) never equals $null, so your first condition is never triggered.
Change
if ($Objects -eq $null) {
to
if ($Objects.Count -gt 0) {
and the code should do what you expect.
Alternatively to Ansgar's solution you could avoid assigning the $Objects variable to an array (can't see why this is required).
$Objects = Get-WmiObject -Class Win32_UserProfile | Where {
(!$_.Special) -and
$_.ConvertToDateTime($_.LastUseTime) -lt (Get-Date).AddDays(-15)
}
-eq $null check can then be used. You may also find [string]::IsNullOrEmpty($Object) useful as this will detect an empty array.
You mention there is no output... that's suspect as you should get the Write-Hosts printing something. On my machine I got an error with $_.ConvertToDateTime... I recommend running your code a line at a tie in the console to make sure it's doing what you expect.
Ok, so it turns out that it was a combination of issues. The main one is the PS version it runs on, does not work on 4. The next stage is to try and force PowerShell to run it as version 3. As previously stated it does not need to be handled within an array. Here is my final script (does not like returns after each and condition so left the formatting as is):
if ($PSVersionTable.PSVersion.Major -eq 4)
{
Write-host "this script will terminate as machine is running PowerShell version 4"
exit 1
}
Else
{
Start-Sleep -s 5
Set-Location -Path C:\Users
#Assign how old the user profile must be for deletion
[DateTime]$AdjustedDate = (Get-Date).AddDays(-15)
[DateTime]$CompareDate = Get-Date $AdjustedDate -Format MM-dd-yyyy
$Hostname = $env:computername
#$testdate = Get-WmiObject -Class Win32_UserProfile | select {$_.ConvertToDateTime($_.lastusetime).ToShortDateString()}
#write-host $testdate
#Get WMI object where it is a domain account and over 15 days old
$Objects = Get-WmiObject -Class Win32_UserProfile | Where {(!$_.Special) -and ($_.ConvertToDateTime($_.lastusetime) -lt (Get-Date).AddDays(-15)) -and $_.LocalPath -notlike "*.NET*" -and $_.LocalPath -notlike "*SQL*" -and $_.LocalPath -notlike "*Admin*" -and $_.LocalPath -notlike "*ADMIN*"}
#If no users returned, write to host.
If($Objects.Count -eq 0)
{
Write-host "no profiles found for deletion"
}
#If users are found, do the following
Else
{
Foreach($Object in $Objects)
{
Write-host "'$($object.LocalPath)' has been identified for deletion"
Write-host " "
}
Foreach($Object in $Objects)
{
Try{
Write-Host "Attempting to delete profile '$($Object.LocalPath)'"
$UserSID = (Get-WmiObject Win32_UserProfile | Where {$_.LocalPath -like '$($object.LocalPath)'}).SID
Remove-WmiObject -InputObject $Object
Write-Host "Deleted profile '$($Object.LocalPath)' successfully."
}
Catch{
Write-Host "Unable to delete profile '$($Object.LocalPath)' due to the following error"
Write-Host "$error" -ErrorAction Continue
}
}
}
}

Powershell script extract AD group members - find last-login time for each user

I'm trying to create a script that will pull a list of group members from AD and run a foreach loop to determine when the last time each user logged into any given domain controller. I got some of the code for the measure-latest function here . I would like to have the script run through the foreach loop and print the samAccountName (username) and last login time stamp (measure-latest) for each user in the group, but so far have not been able to get it working. I think I've got something wrong in logic but i can't seem to figure it out. Any help is appreciated, thank you.
# Get a list of last login times for a group of users
# Script Requires Quest Cmdlet features: https://support.software.dell.com/activeroles-server/download-new-releases
Add-PSSnapin Quest.ActiveRoles.ADManagement
# filter out $nulls and produce the latest of them
function Measure-Latest {
BEGIN { $latest = $null }
PROCESS {
if (($_ -ne $null) -and (($latest -eq $null) -or ($_ -gt $latest))) {
$latest = $_
}
}
END { $latest }
}
# Get list of group users by username
Get-ADGroupMember -identity "Domain Admins" | select samAccountName | Export-csv -path C:\Scripts\UserInformationByGroup\Groupmembers.csv -NoTypeInformation
# Get list of users from group, assign user value
$userlist = import-csv C:\Scripts\UserInformationByGroup\Groupmembers.csv
$user = $userlist | Select samAccountName
# Loop through list of users and print Username ------ Last Login time
foreach ($user in $userlist) {
Get-QADComputer -ComputerRole DomainController | foreach {
(Get-QADUser -Service $_.Name -SamAccountName $user).LastLogon
} | Measure-Latest $samAccountName | out-file -filepath C:\Scripts\UserInformationByGroup\userListLastLogin.txt -append
}
I should mention that when I run the script like this, and just enter each username manually it works and prints the last login time:
Add-PSSnapin Quest.ActiveRoles.ADManagement
function Measure-Latest {
BEGIN { $latest = $null }
PROCESS {
if (($_ -ne $null) -and (($latest -eq $null) -or ($_ -gt $latest))) {
$latest = $_
}
}
END { $latest }
}
Get-QADComputer -ComputerRole DomainController | foreach {
(Get-QADUser -Service $_.Name -SamAccountName USER_NAME_HERE).LastLogon
} | Measure-Latest
This part of the pipeline makes no sense:
| Measure-Latest $samAccountName |
Since nothing is assigned to the variable $samAccountName
You'll need to add pipeline support to your Measure-Latest function, like so:
function Measure-Latest {
[CmdletBinding]
param(
[parameter(ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
$LastLogon
)
# rest of the script (begin-process-end blocks) goes here
}
And now you can pipe the LastLogon values directly to the function:
(Get-QADUser -SamAccountName $user).LastLogon |Measure-Latest | Out-File #or whatever
Thanks to the ValueFromPipelineByPropertyName flag, you can now also pipe the entire object to the function:
Get-QADUser -SamAccountName $user | Measure-Latest
Because the parameter name ($LastLogon) matches that property anyways
In addition to that, I would probably change the logic a bit, so that you don't perform 1 LDAP query per DC per user. If you have 5 Domain Controllers and 200 users, that's now 1000 individual queries to the directory service.
You could simply retrieve ALL the users from each DC, in a single query, using the -LdapFilter parameter with Get-QADUser:
$LDAPClauses = #()
foreach($user in $userlist){
$LDAPClauses += "(samaccountname={0})" -f $user
}
# The | in an LDAP filter means logical OR
$LDAPFilter = "(&(LastLogon=*)(|$(-join($LDAPClauses))))"
Now you can run just a single query per DC:
Get-QADUser -LdapFilter $LDAPFilter
and it'll retrieve all the users in $userlist that has a LastLogon attribute value on that specific DC (you're not really interested in $null-values anyways, right?)