Powershell AD Syntax - If Statement is duplicating a string - powershell

New scripter with hopefully an easy proofread.
I made an Powershell Active Directory script that processes people on Leave of Absence (LOA) within my organization from a daily CSV file I get.
In summary my problem segment should:
Check the $Line variable's currentStatus if that employee is "On Leave"
Disable that Active Directory account.
Grab that AD account's 'Description' field and look for the string part "LOA - " and add that ONLY if it is missing. Problem is that if that field already has "LOA - " in the description, it puts another one... and another, so on.
Example:
Description (Good): LOA - Chef
Description (Bad): LOA - LOA - Chef
Description (Please NO): LOA - LOA - LOA - Chef
Etc.
I'm fairly certain the problem is with this line here but I can't figure out how to fix it.
If ($null -ne ($newDescript | ? {$loaPhrases -notcontains $_}))
$loaPhrases = "LOA -|LOA-|LOA - |LOA- |LOA - LOA - "
ElseIf ($Line.currentStatus -eq "ON LEAVE")
{
#Add 'LOA - ' to description and disable AD
Set-ADUser $User.samAccountName -Enabled 0
'Disabled AD'
$Description = Get-ADUser $User.samAccountName -Properties Description | Select-Object -ExpandProperty Description
$newDescript = $Description.substring(0, $Description.IndexOf('-')+1)
If ($null -ne ($newDescript | ? {$loaPhrases -notcontains $_}))
{
$Description = "LOA - " + "$Description"
$Description = $Description.trim()
Set-ADUser $user.samAccountName -Description $Description
}
}

It looks like you're overcomplicating it a bit. Try the -match statement for a regex matching of your $loaPhrases.....
if ($description -notmatch $loaPhrases)
This could do with fancier editing and checks, but basically checks your regex string (separated by |, as you have) for the loa string. If it's not there, it does your addition of the "LOA - " part.
Edit: Adding option to simply look at the start of a string
As Steven pointed out in the comments, it's likely you're looking for a LOA string at the start of the description to determine whether you need to add one in. As all your LOA strings begin with "LOA", you could simply check to ensure it doesn't start with "LOA", to determine if you need to add it in.
if (!($description.startswith("LOA")))
The ! flips the boolean value returned by $description.startswith("LOA"), so if it does start with "LOA", it returns false, and if it doesn't start with "LOA", it returns true, and enters the if statement.
As an aside, earlier in, you set -Enabled 0. My recommendation, for readability, is to change that to -Enabled $false, as it indicates clearly you're disabling the account, rather than setting it to an integer (even though this works / 0 usually being "false").

Building on Guy S's Helpful Answer, and considering you are new to this I wanted to make a few extra points.
Considering you are changing more than 1 property you might consider using a different approach to setting the user object. In the case of the Active Directory cmdlets there's an instancing capability that I really like. Such an approach might look something like below:
ElseIf ($Line.currentStatus -eq "ON LEAVE")
{
# Get and store the instance in a variable:
$ADUser = Get-ADUser $User.samAccountName -Properties Description
$ADUser.Enabled = $false # Change the desired properties...
Write-Host 'Disabled AD'
If( $ADUser.Description -notmatch $loaPhrases )
{
$ADUser.Description = "LOA - " + $ADUser.Description.Trim()
}
# After changing properties run a single set command citing -Instance:
Set-ADUser -Instance $ADUser
}
Notice, I used Write-Host instead of just quoting "Disable AD". In a top-down script, it's probably not a big deal but if this code were inside a function that string would get returned. That causes variant arrays, unexpected function returns, and associated problems. Again, not necessarily a problem here, but you'll want to be aware of that going forward. In fact, it's a common confusion as people get more advanced with PowerShell.
I have seen anomalies where -Instance doesn't like null values. However, the Description attribute doesn't seem to have that problem. Splatting may be another option and is super useful in situations even slightly more complex than this... Read more about splatting here,
I also want to talk a little more about your original code and the subsequent samples.
First -contains, -in and their "not" variants don't work well on strings. For example, "string" -contains "ing" will return "False". So, conceptually the approach is a bad match for what you were trying to do. Most would use -match which is a RegEx based approach.
Note: For simple stuff -Like can use a less robust wildcard approach. In your case -match is your best bet because of the variation in the match string"
-contains and -in are great for checking arrays and other collections:
$Array = "One", "Two"
$Array -contains "one"
"one" -in $Array
Above, both -contains and -in will return "True"
Given the indication that you want to search the beginning of the string, I'd modify the RegEx to use the "^" anchor like: $loaPhrases = "^LOA -|^LOA-|^LOA - |^LOA- |^LOA - LOA - " Come to think of it a simple "^LOA" may do the trick.
Regarding Guy's answer, I'd avoid .StartsWith() or for that matter .EndsWith() only because they are case sensitive.
Lastly, I'll point out the comparison to $null is unnecessary even besides the other issues.
$Array = "One", "Two"
If( ( $Array | Where-Object{ $_ -eq "Three"} ) ){
"Something"
}
In the above example "Something" will not get echoed to the screen. If you change "Three" to "Two" it will.

Related

Powershell - Test if user is part of any Groups

I'm having trouble getting a small PS script to work the way I want it to.
Powershell version: 5.1
Ultimate goal: I want to parse all existing local User accounts and generate a list of those that are not part of any local Groups. (This is not an AD environment, nor part of a domain. This is all just local OS accounts and Groups.) Also, I am only interested in the Groups that I have manually created - I don't care about the default System Groups. (More on how I try to achieve this, later.) And then, after I get this part working, this output (list of User names) will then be used (in the future - not shown in this code) as input to another block that will add these not-in-a-Group users to a Group.
I found an example bit of code - which worked, and resulted in the correct set of user names. But it was slow - it took >5 minutes. So I wanted something faster.
The approach that I'm working with now generally seems like it will work, and is pretty quick. I'm just having trouble getting it to restrict things down correctly. It seems I'm having trouble referencing properties of objects returned by the cmdlets. (See code a bit further down. There are various counters and write-host steps in here, too, that are purely for helping me see what's going on and debugging - they aren't required for the actual task.)
My general outline of how I am going about this:
Compile a list of all Users in all Groups
But, I only want Groups that have no Description - these are the ones that I have created on the machine. The OS default Groups all have a Description. This will help me narrow down my list of Users.
Loop over the list of all Users on the system
Compare each user to the List from Step 1
If User is on the List, then that User is IN a Group, so skip it
If User is not on the List, then save/report that Name back
[Side note - the user 'WDAGUtilityAccount' turned out to also not be in any Groups, but I don't want to change that one; so have a specific test to exclude it from the list.]
[original version of code]
# --- Part 1
$UsersList = foreach ( $aGroup in get-localgroup ) {
$groupcount++
# Only want Groups that don't have a Description
if ( $null -eq $aGroup.Description ) {
Get-LocalGroupMember $aGroup.name
}
write-host "$groupCount -- $aGroup.Name _ $aGroup.Description"
}
# Just for debugging - to see the content of Part 1
write-host "List = $UsersList"
# ---- Part 2
Get-LocalUser | ForEach-Object {
$count++
$name = $_.Name
if ( $name -eq "WDAGUtilityAccount" ) {
# nothing - skip this one
} elseif ( $UsersList.Name -inotcontains "$env:COMPUTERNAME\$_" ) {
$count2++
write-host "$Count ($Count2) : $name -- not a part of any groups"
}
}
It appears that my attempts to extract the Properties of the Group in Part 1 are failing - I am getting literal text 'Name' and 'Description', instead of the aGroup.Name aGroup.Description property values. So my output looks like "MyGroup1.Name" instead of "MyGroup1" (assuming the actual name of the group is 'MyGroup1').
I believe that I am approaching the 'Group' object correctly - when I do 'get-localgroup | get-member" it says that it is a 'LocalGroup' object, with various properties (Name and Description being two of those).
There may be many other approaches to this - I'm interested in hearing general ideas; but I would also like to know the specific issues with my current code - it's a learning exercise. :)
Thanks, J
[ Version 2 of code - after some suggestions... ]
$UsersList = foreach ( $aGroup in get-localgroup ) {
$groupcount++
#Write-Host $aGroup.Description
# Only want Groups that don't have a Description (i.e. are the Groups that we created, not the default System/OS groups)
if ( $null -eq $($aGroup.Description) ) {
$UserList += Get-LocalGroupMember $($aGroup.name)
write-host "$groupCount -- $($aGroup.Name) _ $($aGroup.Description)"
}
}
write-host "List = $UsersList"
Get-LocalUser | ForEach-Object {
$count++
$name = $_.Name
if ( $name -eq "WDAGUtilityAccount" ) {
# nothing - skip this one
} elseif ( $($UsersList.name) -inotcontains "$env:COMPUTERNAME\$_" ) {
$count2++
write-host "$Count ($Count2) : $name -- not a part of any groups"
}
}
I believe this could be reduced to:
Store all members of a any group where the group's Description is null.
Get all local users and filter where their user's Name is not equal to WDAGUtilityAccount and they are not part of the stored member's SID array.
$members = Get-LocalGroup | Where-Object { -not $_.Description } | Get-LocalGroupMember
Get-LocalUser | Where-Object {
$_.Name -ne 'WDAGUtilityAccount' -and $_.SID -notin $members.SID
} | Format-Table -AutoSize

Get-GPOReport and Search For Matched Name Value

I'm trying to use the PowerShell command 'Get-GPOReport' to get GPO information in XML string format so I can search it for sub-Element values with unknown and different Element tag names (I don't think XML Object format will work for me, so I didn't perform a cast with "[xml]"), but I haven't been able to parse the XML output so that I can grab the line or two after a desired "Name" Element line that matches the text I'm searching for.
After, I have been trying to use 'Select-String' or 'Select-XML' with XPath (formatting is unclear and I don't know if I can use a format for various policy record locations) to match text and grab a value, but I haven't had any luck.
Also, if anyone know how to search for GPMC GUI names (i.e. "Enforce password history") instead of needing to first locate back-end equivalent names to search for (i.e. "PasswordHistorySize"), that would also be more helpful.
The following initial code is the part that works:
$String = "PasswordHistorySize" # This is an example string, as I will search for various strings eventually from a file, but I'm not sure if I could search for equivalent Group Policy GUI text "Enforce password history", if anyone knows how to do that.
$CurrentGPOReport = Get-GPOReport -Guid $GPO.Id -ReportType Xml -Domain $Domain -Server $NearestDC
If ($CurrentGPOReport -match $String)
{
Write-Host "Policy Found: ""$($String)""" -Foregroundcolor Green
#
#
# The following code is what I've tried to use to get value data, without any luck:
#
$ValueLine1 = $($CurrentGPOReport | Select-String -Pattern $String -Context 0,2)
$Value = $($Pattern = ">(.*?)</" ; [regex]::match($ValueLine1, $Pattern).Groups[1].Value)
}
I've been looking at this since yesterday and didn't understand why Select-String wasn't working, and I figured it out today... The report is stored as a multi-line string, rather than an array of strings. You could do a -match against it for the value, but Select-String doesn't like the multi-line formatting it seems. If you -split '[\r\n]+' on it you can get Select-String to find your string.
If you want to use RegEx to just snipe the setting value you can do it with a multi-line regex search like this:
$String = "PasswordHistorySize" # This is an example string, as I will search for various strings eventually from a file, but I'm not sure if I could search for equivalent Group Policy GUI text "Enforce password history", if anyone knows how to do that.
$CurrentGPOReport = Get-GPOReport -Guid $GPO.Id -ReportType Xml -Domain $Domain -Server $NearestDC
$RegEx = '(?s)' + [RegEx]::Escape($String) + '.+?Setting.*?>(.*?)<'
If($CurrentGPOReport -match $RegEx)
{
Write-Host "Policy Found: ""$String""" -Foregroundcolor Green
$Value = $Matches[1]
}
I'm not sure how to match the GPMC name, sorry about that, but this should get you closer to your goals.
Edit: To try and get every setting separated out into it's own chunk of text and not just work on that one policy I had to alter my RegEx a bit. This one's a little more messy with the output, but can be cleaned up simply enough I think. This will split a GPO into individual settings:
$Policies = $CurrentGPOReport -split '(\<(q\d+:.+?>).+?\<(?:\/\2))' | Where { $_ -match ':Name' }
That will get you a collection of things that look like this:
<q1:Account>
<q1:Name>PasswordHistorySize</q1:Name>
<q1:SettingNumber>21</q1:SettingNumber>
<q1:Type>Password</q1:Type>
</q1:Account>
From there you just have to filter for whatever setting you're looking for.
I have tried this with XPath, as you'll have more control navigating in the XML nodes:
[string]$SearchQuery = "user"
[xml]$Xml = Get-GPOReport -Name "Default Domain Policy" -ReportType xml
[array]$Nodes = Select-Xml -Xml $Xml -Namespace #{gpo="http://www.microsoft.com/GroupPolicy/Settings"} -XPath "//*"
$Nodes | Where-Object -FilterScript {$_.Node.'#text' -match $SearchQuery} | ForEach-Object -Process {
$_.Name #Name of the found node
$_.Node.'#text' #text in between the tags
$_.Node.ParentNode.ChildNodes.LocalName #other nodes on the same level
}
After testing we found that in the XML output of the Get-GPOReport cmdlet, the setting names does not always match that of the HTML output. For example: "Log on as a service" is found as "SeServiceLogonRight" in the XML output.

PowerShell return multiple values from if condition

I have a Powershell script returning data from an API which works fine as long as I only attempt to return one $device.realm, but I need multiple realms. I'm a newb to PS.
Any help here is really appreciated
Here is my code
$Output = forEach ($device in $devices) {
if ($device.realmName -eq 'Archive') {
[PSCustomObject]#{
HostName = $device.name
IPAddress = $device.primaryInterfaceAddress
Realm = $device.realmName
SerialNumbers = (($device.dynamicFields | where { $_.name -EQ "serial number" } | Select-Object -ExpandProperty values) -join "," | out-string).TrimEnd()
}| Select-Object Hostname,IPAddress,Realm,SerialNumbers | Export-csv C:\temp\Archive.csv -notype -Append
}
I need to return multiple $device.realms as in
if ($device.realmName -eq 'Archive' -and 'Default' -and 'Farms')
Once I add the additional -and's every realm is returned instead of just the one's I need to return.
I believe the issue at hand here is that the statement within the If block that you're querying as ($device.realmName -eq 'Archive' -and 'Default' -and 'Farms')
is not, when evaluated logically "Evaluate true if the device realmname is Archive, Default, or Farms." It is evaluating whether device.realmname is archive, and then just interpreting the two -ands in your example as true, as they are not querying a comparison, but just the presence of a non-null string. Not sure what is leading it to return everything, I'd have to see some more direct examples to be sure, but in my experience that is most common when you include an -or in a comparison pointing to a nonnull string, which will make the entire statement true.
What I would suggest is as follows: Use the regex operators built in to powershell for cases like this. You could use
if($device.realmname -eq 'Archive' -or $Device.realmname -eq 'farm' -or $device.realmname -eq 'Default')
which would, I believe, return what you are looking for, but I find it a bit complex. More complicated queries on a single property, I find, are easiest to do via -match, through something invoking the -match operator, which allows you to build a regex query statement that can include Or's or And's with a bit simpler of a synatax, like so:
if($Device.realmName -match 'Archive|Farm|Default')

Reducing a string by one until no user is found in AD Powershell

I'm trying to create a Powershell loop that will reduce a string to twenty characters and then compare that to current Active Directory users and if it exists then it will remove one character from the end and then check again. It would keep doing this until it finds a string that isn't a current user. This is what I have currently:
$uName = (Read-Host "New User Name")
if ($uName.length -gt 20) {
$uNameSAM = $uName.SubString(0, 20)
foreach ($uNameSAMCheck in $uNameSAM) {
Write-Host "`nDue to length limitations shortening Name to: $uNameSAM`n" -Foreground Red
}else {
$uNameSAM = $uName
}
}
This works well for the shortening part, but I'm not sure how to go about checking it against AD for if the inputted name already exists and if so shortening it again and checking again and so on.
As Riley mentions, this is kind of an unusual pattern to enforce, but don't let me stop you.
The simplest way to approach this is doing to be with some form of loop, here I think perhaps a while loop might be appropriate.
$Username = Read-Host -Prompt "New User Name"
if ($Username.length -gt 20) {
$SamAccountName = $Username.SubString(0, 20)
Write-Warning "Due to length limitations shortening Name to: $uNameSAM"
}
else {
$SamAccountName = $Username
}
while (Get-ADUser -Filter "SamAccountName -eq $SamAccountName") {
$SamAccountName = $SamAccountName.Substring(0, $SamAccountName.Length - 1)
}
Write-Host -ForegroundColor Green "'$SamAccountName' is available!"
There are many possible approaches. This particular one makes use of the fact that you can utilise PS's native type coercions to your favour. A while loop (as well as if statements, until loops, etc.) is expecting a boolean result. If a cmdlet returns any complex object at all, it will generally be coerced to $true. If it returns an empty / null result, it will be coerced to $false.
This behaviour is a little trickier when working with base data types like integers, strings, and the like, but for working with complex objects it's a very straightforward way to ask "did this command give me any output". In the case of Get-ADUser -Filter, if the filter doesn't match a user, no output is generated.
We don't want to use Get-ADUser -Identity because that will unfortunately throw a terminating error if it doesn't find the user requested. That can be a little more annoying to code around, so I've opted for the -Filter method instead.

Trying to create a powershell user creation script, want to append number to username if it already exists

I am attempting to create a user creation script as a way to teach myself more Powershell. Currently I am only working on creating just the username and want to make sure each username is unique.
After a user inputs the name and number of a user the script should do the following.
Get the the first name
Get the middle initial
Get the last name
Combine the first letter of the first name + middle initial + 6 characters from the last name
If users already exists, add number starting at 1 until username is unique.
I am currently stuck on step 5. If the username is not unique it appends a number one. I.E. User Brenda T Follower has a username of BTFollow and if that username exists already it becomes BTFollow1.
However if BTFollow and BTFollow1 already exist instead of making BTFollow2 it makes BTFollow12.
Lastly, although not a huge issue I want my parameters to show what comes after Read-Host but that text is not appearing only the variable name.
Here is my code.
Param(
#Gather users first name, required input and must not be empty or null
[Parameter(Mandatory=$True)]
[ValidateNotNullOrEmpty()]
[string]
$FirstName = (Read-Host -Prompt 'Please input the users first name.'),
#Gather users middle initial, required input and must not be empty or null and must only be one character
[Parameter(Mandatory=$True)]
[ValidateNotNullOrEmpty()]
[ValidateLength(1,1)]
[string]
$MiddleInitial = (Read-Host -Prompt 'Please input the users middle initial.'),
#Gather users last name, required input and must not be empty or null
[Parameter(Mandatory=$True)]
[ValidateNotNullOrEmpty()]
[string]
$LastName = (Read-Host -Prompt 'Please input the users last name.'),
#Gathers user phone extension, required input, mustn ot be empty or null, and must only user numbers
[Parameter(Mandatory=$True)]
[ValidateNotNullOrEmpty()]
[ValidatePattern("[0-9][0-9][0-9][0-9]")]
[ValidateLength(4,4)]
[String]
$PhoneExtension = (Read-Host -Prompt 'Please input the users 4 digit exension, numbers only')
)
$i = 0
#Create user name
$Username = $FirstName.Substring(0,1) + $MiddleInitial + $LastName.Substring(0,6)
#Check username does not exist, if it does add numbers
Do {
Try {
Get-ADUser $UserName | Out-Null
$UserName = $Username + ++$i
Continue
}
Catch {
Break
}
} While ($True)
Write-Host "Username is $Username"
Figured it out with the help of Reddit. I needed to replace my current code.
Do {
Try {
Get-ADUser $UserName | Out-Null
$UserName = $Username + ++$i
Continue
}
Catch {
Break
}
} While ($True)
New code that works.
#Check username does not exist, if it does add numbers
$UniqueUserName = $Username
while (Get-ADUser -Filter "SamAccountName -like '$UniqueUserName'"){
$UniqueUserName = $Username + ++$i
}
Because you iteratively modified $UserName in your loop, you ended up appending an additional number in each iteration:
$UserName = $Username + ++$i
With BTFollow as the original value, the value is BTFollow1 after the first iteration, and in the second iteration you then append 2 to that, resulting in BTFollow12, and so on.
Your own answer works (though -like should be replaced with -eq) and, given its appealing concision and the presumed relative rarity of duplicate names, is probably the way to go.
The following solution provides optimizations, but the required complexity may not be worth it; at least I hope it showcases some interesting techniques.
You can avoid a costly loop around Get-AdUser altogether, by pre-filtering potential duplicates with Get-ADUser -Filter and then processing the candidates locally:
$highestIndexSoFar =
Get-ADUser -Filter "SamAccountName -like '$UserName*'" |
ForEach-Object { if ($_.SamAccountName -match '\d*$') { [int] $Matches[0] } } |
Sort-Object -Descending | Select-Object -First 1
if ($null -eq $highestIndexSoFar) { # no duplicates -> use as-is
$UniqueUserName = $UserName
} else { # increment the highest index to date to make the username unique
$UniqueUserName = $UserName + (1 + $highestIndexSoFar)
}
Another advantage of the above solution is that it determines the highest number suffix in the actual names, which reliably uses a suffix that is the highest to date + 1, whereas the call-Get-AdUser-in-a-loop approach uses the first index that isn't taken (though that would only be a concern if users got deleted over time or if out-of-sequence suffixes were manually created).
Get-ADUser -Filter "SamAccountName -like '$UserName*'" retrieves potential duplicates; the -Filter syntax isn't sophisticated enough to allow regex-based matching, so this pre-filtering can only find all usernames that share the same prefix, which may include false positives (e.g., jdoet for jdoe, whereas ultimately only a numeric suffix such as jdoe1 should be considered).
Regex '\d*$' is then used to weed out such false positives, and the actual, potentially empty numeric suffix (\d*) is reflected in the automatic $Matches variable's [0] entry.
The numeric suffix, if present or empty, is then cast to [int] and output.
Note that an initially unindexed username such as BTFollow will cause $Matches[0] to be the empty string, which cast [int] converts to 0, so the first index appended will be 1.
Sort-Object -Descending sorts the resulting numbers in descending order, and Select-Object -First 1 then extracts the first - i.e. the highest - suffix number found, if any.
If no matches are found at all, i.e. if the username isn't taken yet, there is nothing to sort, and Select-Object -First 1 effectively yields $null; otherwise, 1 must be added to highest number to form the new unique username.
Caveat: This approach may still fail if others are creating users simultaneously.