This is an old revision of the document!
Powershell
Powershell is quickly replacing the Command Prompt as the go-to command line interface for many IT admins. While I don't think PS will ever replace CMD, I do feel it will come very close to doing so. For the most part, CMD was written for batch files and basic commands and, as such, has a fair amount of limitations. However, PS was built on the .Net Framework and is now open-source. Between the programming language back-end and the open source community support, PS has far more potential!
Basic Info from PS
Some commands only work in a certain environment. Some commands only work in certain versions. It important to know these things when you try to run various commands or need to troubleshoot scripts.
Version
Knowing the version of Powershell is important. Again, some commands may not work in certain versions. The Windows OS will ship with a version that can likely be updated over the life of the OS so don't think you're stuck with the same version forever.
- Find the version of Powershell:
host
- You should see something like this:
Name : Windows PowerShell ISE Host Version : 5.1.16299.98 InstanceId : 24d2b47e-52f8-4787-a977-d152fdc22529 UI : System.Management.Automation.Internal.Host.InternalHostUserInterface CurrentCulture : en-US CurrentUICulture : en-US PrivateData : Microsoft.PowerShell.Host.ISE.ISEOptions DebuggerEnabled : True IsRunspacePushed : False Runspace : System.Management.Automation.Runspaces.LocalRunspace
- There's also a built in variable for it so you can do something like check the version and proceed further only if it's of a certain version or better:
If ($host.Version -ige "2.0") {Write-Host "Now we're cooking with gas!"}
Execution Policy
Checking and Setting the Policy
If you try to run a script, it may nag about the Execution Policy and fail to run. In those cases, you may need to set the policy appropriately or try bypassing it altogether.
- To check the policy, try the following:
Get-ExecutionPolicy - To set the policy, open an elevated instance of PS and try something like the following:
Set-ExecutionPolicy RemoteSigned
Bypassing the Policy
Setting the Execution Policy on a system may not always be the way to go about it. Maybe you're in a production environment and, for security reasons, they don't want to allow PS scripts to run all the time. A good example of a scenario like this is one I worked in before. I ended up creating a Scheduled Task that called Powershell.exe from cmd.exe and did so with a parameter to bypass the policy - just for that execution.
- To temporarily bypass the Execution Policy and run a script, try running something like this in an elevated command prompt:
powershell.exe -ExecutionPolicy Bypass -Command "C:\MyAwesomeScript.ps1"
- To temporarily bypass the Execution Policy and run a command, try running something like this in an elevated command prompt:
powershell.exe -ExecutionPolicy Bypass -Command "& {Enable-PSRemoting -Force}"
Aliases
Powershell has the ability to use aliases. You can even define your own! You may run across scripts that contain aliases and you may wonder what they stand for. Below, you'll find examples of how to go about finding aliases and what those aliases stand for. By and large, I don't like to use them in documentation due to readability reasons but I will use them to save some keystrokes when I'm doing something quick & dirty that I didn't write a script for.
- To show all current aliases, run the following command:
Get-Alias - You may have seen the alias,
%, before; it's pretty common. If you wanted to know what it meant, you could check it like this:Get-Alias -Name %
- To find the alias for a certain cmdlet, use something like this:
Get-Alias -Definition ForEach-Object
For those of you who are playing along at home, if you ran all the commands I just mentioned, you probably saw something like this:
PS C:\WINDOWS\system32> Get-Alias -Name % CommandType Name Version Source ----------- ---- ------- ------ Alias % -> ForEach-Object PS C:\WINDOWS\system32> Get-Alias -Definition ForEach-Object CommandType Name Version Source ----------- ---- ------- ------ Alias % -> ForEach-Object Alias foreach -> ForEach-Object
Putting Theory into Practice
Using an alias is easy enough; you just use it in lieu of the full cmdlet. For instance, the following three commands will do the exact same thing:
- This will create four directories inside
C:\_Drivers\VMwarewith the use of theForEach-Objectcmdlet:1..4 | ForEach-Object {mkdir "C:\_Drivers\VMware\test$_"}
- This will do the same but
ForEach-Objecthas been substituted with the alias ofForEach:1..4 | ForEach {mkdir "C:\_Drivers\VMware\test$_"}
- This will do the same but
ForEach-Objecthas been substituted with the alias of%:1..4 | % {mkdir "C:\_Drivers\VMware\test$_"}
Working with the Filesystem
Working with Mapped Drives
Generally speaking, I hate mapped drives in corporate environments. Unless great care is taken to ensure consistency, they quickly become a mess. Think about it… When mapping a drive, you can choose any unused drive letter you want. So, I could map the network share, \\H2P-IOLWEB1\Code, to drive R:\ and tell you that the file you wanted was on said drive. However, you don't have that path mapped the same way I do or, in some cases, you don't have the path mapped at all! So, if I told you to take a look at R:\MyAwesomeScripts\MapDrive.ps1 for some great examples, you wouldn't be able to. Or, at best, you'd need to adjust your path. Moreover, if a path isn't available when the machine starts up, it can throw errors unless you've accounted for that as well. However, there are a few cases where a mapped drive is still useful. For instance, capturing a WIM file and dumping it to a network share from WinPE would be a good example of when using a mapped drive makes sense. You can also map drives via Group Policy and that would allow for some consistency. But, for the most part, I avoid mapped drives unless I'm in WinPE.
- Mapping a drive, just for the current PS session, can be done like this:
New-PSDrive -Name "R" -PSProvider "FileSystem" -Root "\\H2P-IOLWEB1\Code" -Credential rsickler@autotrader.int
- Again, this is a mapped drive that will only be visible to the PS session in which it was created; you will not see this mapped drive in Explorer. Basically, you'd use something like this in a script. The script would map the drive, do stuff…. and finally un-map the drive and finish.
- Using the
-Persistparameter allows it to be visible in Explorer and the drive will still be there after a reboot. However, if you run the command via an elevated prompt, it will not show up in your Explore windows as you've technically mapped the drive for the Administrator's profile:New-PSDrive -Name "R" -PSProvider "FileSystem" -Root "\\H2P-IOLWEB1\Code" -Credential rsickler@autotrader.int -Persist
- Removing the mapped drive can be done like this:
Remove-PSDrive -Name "R" -Force
Copying Files and Folders
This is a very basic function for most people as they tend to just use Explorer to drag-and-drop. However, if you're on a Server Core machine or you're using remote commands through PS, you can copy files via other means.
- Copy a folder and its contents into another local folder. C:\_Temp2 will have a new folder, called VMware, inside it:
Copy-Item -Recurse -Path "C:\_Drivers\VMware" -Destination "C:\_Temp2"
- Copy a file to a UNC path:
Copy-Item -Path "C:\sickler.txt" -Destination "\\h2t11-iolweb1\c$"
- Copy a folder and its contents into another folder on a remote share, while prompting for the password to a predefined dotted-domain user:
Copy-Item -Recurse -Path "C:\sickler" -Destination "\\h2t11-iolweb1\c$" -Credential rsickler@autotrader.int
- Copy a file from a UNC path:
Copy-Item -Path "\\nas1.homenet.local\it\Software\VMWare\VMware vCenter Server\v6.0.0u1b\VMware-VIMSetup-all-6.0.0-3343019.iso" -Destination "C:\Users\RSICKLER\Desktop"
- Copy a file and rename it on the fly (creating a backup in this case):
Copy-Item -Path "$env:SystemRoot\System32\Sysprep\unattend.xml" -Destination "$env:SystemRoot\System32\Sysprep\unattend_original.xml"
Moving Files and Folders
Again, this is very basic but it helps to know how to do it via PS.
- Move a folder, and its contents, into another local folder:
Move-Item -Path "C:\_Drivers\VMware" -Destination "C:\_Temp2"
- Move a file into another folder, forcing an overwrite if needed:
Move-Item -Path "C:\_Temp\unattend.xml" -Destination "$env:SystemRoot\System32\Sysprep" -Force
- Move a file into another folder but renaming it on the fly:
Move-Item -Path "C:\_Temp\unattend.xml" -Destination "$env:SystemRoot\System32\Sysprep\Working_unattend.xml"
- Move a folder and its contents into another folder on a remote share, while prompting for the password to a predefined domain user:
Move-Item -Recurse -Path "C:\_Drivers\VMware" -Destination "\\h2t11-iolweb1\c$\_Drivers" -Credential autotrader\rsickler
Deleting Files and Folders
Again, this is very basic but it helps to know how to do it via PS. Furthermore, this cmdlet can be used by many other providers so you can use it to delete files, folders, registry keys, variables, etc.
- Delete a file:
Remove-Item -Path "C:\_Drivers\VMware\Info.txt"
- Delete a read-only file:
Remove-Item -Path "C:\_Drivers\VMware\Info.txt" -Force
- Delete a folder with files and other folders in it:
Remove-Item -Path "C:\_Drivers\VMware\Win10" -Force -Recurse
Renaming Files and Folders
This too is a simple task but it's handy to know how to do this via a quick command.
- Rename a log file:
Rename-Item -Path "C:\_Drivers\VMware\Daily.log" -NewName "Monday.log"
- Rename a folder:
Rename-Item -Path "C:\_Drivers\VMware" -NewName "VirtualBox"
- Rename multiple files, of the same type, to another type:
Get-ChildItem "C:\_Drivers\VMware\*.log" | Rename-Item -NewName { $_.Name -Replace '\.log$','.txt' }
- I've used this bit of code before. While making screen-shots for various sections of this wiki, I used VirtualBox and that program spits out nice screen-shots but they are named via a time-stamp and that's not what I wanted. Rather than manually renaming the files, I used the following code. However, the order in which Explorer displays them and the order in which code sees them differ. So, be mindful of this when you're using code like this:
Dir "C:\Users\User1\VirtualBox VMs\Server2012\WDS_Setup\*.png" | ForEach-Object -begin { $count=1 } -process { Rename-Item $_ -NewName "WDS_Setup_$count.png"; $count++ }
Working with ISOs and VHDs
Server 2012 has built-in support for mounting ISOs & VHDs. Sometimes you can just right-click one and mount the file. Worst case, you can also do this via Powershell.
- Mount the ISO:
Mount-DiskImage -ImagePath C:\_Temp\SW_DVD5_Lync_2013_32-BIT_X64_English_MLF_X18-54527.ISO
- Dismount the ISO:
DisMount-DiskImage -ImagePath C:\_Temp\SW_DVD5_Lync_2013_32-BIT_X64_English_MLF_X18-54527.ISO
Working with Processes
One can use PS to show running processes. It won't be real-time like what you'd see with Windows Task Manager but it's still helpful for easily finding the PID and other stats since the list won't be bouncing around as other processes come and go. It's even more helpful with processes on a remote machine since you don't need to RDP into it to check the running processes.
Showing Local Processes
- List all locally running processes:
Get-Process - List all locally running instances of Explorer:
Get-Process -Name Explorer
- Using a wildcard, list all locally running instances of any process that starts with Exp:
Get-Process -Name Exp*
- Using a wildcard, list all locally running instances of any process that starts with Exp but only show the ProcessName column and the ID column:
Get-Process -Name Exp* | Select ProcessName,ID
- Using multiple wildcards, list all locally running instances of any process that starts with Exp or Note:
Get-Process -Name Exp*,Note*
- List all locally running processes and show their names, who the publisher is and their version information:
Get-Process | Select ProcessName,Company,FileVersion
Showing Remote Processes
- List all running processes on a remote server:
Get-Process -ComputerName H2P-IOLRPC2
- Using a wildcard, list all running processes on a remote server that start with MS:
Get-Process -Name MS* -ComputerName H2P-IOLRPC2
- Using multiple wildcards, list all running processes on a remote server that start with MS or SVC:
Get-Process -Name MS*,SVC* -ComputerName H2P-IOLRPC2
Killing Local or Remote Processes
Killing a local process is very straight forward. Killing a remote process isn't as clean as I'd like it to be but it still works. So long as you have the correct permissions, killing a remote process can be achieved with the Invoke-Command cmdlet.
- Kill a local process:
Stop-Process -Name DesktopInfo* -Force
- Kill a remote process, asking for the password for a predefined domain user:
Invoke-Command -ScriptBlock { Stop-Process -Name DesktopInfo* -Force } -ComputerName h2s2-netappfp1 -Credential autotrader\rsickler
Working with Services
One can use PS to get info on services. It's also helpful with services on a remote machine since you don't need to RDP into it to check the running processes.
Showing Local Services
- Show a basic list of services on the local machine:
Get-Service - Some services have names that are quite long and those full names will not be shown unless you format a table with proper sizing or format it as a list. This will show the same list of services but format it into a table and size it so you can see all of it:
Get-Service | Format-Table -AutoSize
- As mentioned earlier, you can also format the output as a list:
Get-Service | Format-List
- Checking on a specific service:
Get-Service -Name "wuauserv" | Format-Table -AutoSize
Showing Remote Services
As previously mentioned, this is handy for checking on services on remote machines.
- Checking on a remote service and making a nice, readable table with the results:
Get-Service -ComputerName "wds12" | Format-Table -AutoSize
- Checking on a specific service on a remote server:
Get-Service -ComputerName "wds12" -Name "wuauserv" | Format-Table -AutoSize
Changing the Status of Services
Sometimes, you need to start, stop or restart a service on a local or remote machine1).
- Start a service:
Start-Service -Name "wuauserv"
- Stop a service:
Stop-Service -Name "wuauserv"
- Restart a service:
Restart-Service -Name "wuauserv"
- Restart a service on a remote computer:
Invoke-Command -ComputerName "wds12" -ScriptBlock {Restart-Service -Name "WDSServer" -Force}
Working with Active Directory
PS can do all kinds of things in AD but I typically use it for quick and dirty reporting. From time to time, I'll also use it for bulk actions like cleaning up old computer and user objects.
Import-Module ActiveDirectory
Working with Computer Objects
List All Machines
- List all computer objects and a brief set of details for each:
Get-ADComputer –Filter *
- List all machines, in table-format, and auto-size the table to make it fit nicely on the page. The table will only have 3 columns - Name, ObjectClass and DistinguishedName:
Get-ADComputer –Filter * | Format-Table Name,ObjectClass,DistinguishedName -AutoSize
- This is a similar command but it uses short-hand/aliases. The
FTis short-hand forFormat-Table. the-ais short forAuto-Size:Get-ADComputer –Filter * | FT Name,ObjectClass,DistinguishedName -a
List Computer Objects Based on OS
- This will list the name & OS and dump them out into a CSV on your desktop:
Get-ADComputer -Filter { OperatingSystem -Like '*Windows Server*' } -Properties OperatingSystem | Select-Object Name, OperatingSystem | Export-CSV $env:USERPROFILE\Desktop\Servers.csv -NoTypeInformation
- This will list the name & OS and format the results into a table:
Get-ADComputer -Filter { OperatingSystem -Like '*Windows Server*' } -Properties OperatingSystem | Select-Object Name, OperatingSystem | Format-Table -AutoSize
- This will show machines in a certain OU:
Get-ADObject -Filter {ObjectClass -eq "Computer"} -SearchBase "OU=STAGE3,OU=Environments,OU=ATC Member Servers,OU=ATC_DD,DC=HOMENET,DC=local" | Format-Table Name
- This will give you a count of all computer object that are enabled and running some form of Windows Server 2003:
Get-ADComputer -Filter { OperatingSystem -Like 'Windows Server 2003*' -and (Enabled -eq "True") } -Properties OperatingSystem | Select-Object Name, OperatingSystem | Measure-Object | Select-Object Count
- This will show computer objects, running a desktop version of Windows and sort them based on the last time they authenticated to the domain:
Get-ADComputer -Filter {OperatingSystem -notLike '*SERVER*' } -Properties lastlogondate,operatingsystem |select name,lastlogondate,operatingsystem | Sort-Object lastlogondate | FT -AutoSize
- This takes the command above and takes it a little further. It creates variables (In PS, variables start with a money sign -
$.) and calls upon them in order to calculate and show you the machines running a Windows desktop OS, with names starting with MS, that have not authenticated to the domain in 180 days. The list gets sorted by the LastLogonDate property. Something like this could later be piped through a command to disable or delete the object. However, in this case, it will only spit out a sorted list:$DaysInactive = 180 $Time = (Get-Date).AddDays(-($DaysInactive)) Get-ADComputer -Filter {(LastLogonTimeStamp -lt $Time) -and (OperatingSystem -notLike '*SERVER*') -and (SamAccountName -like "ms*")} -Properties LastLogonDate, OperatingSystem | Select-Object Name, LastLogonDate, OperatingSystem | Sort-Object LastLogonDate
List Enabled or Disabled Computer Objects
- This will show you a list of disabled machines:
Get-ADComputer -filter {enabled -eq $False}
- This will dump a list of enabled machines to a CSV but only list their names:
Get-ADComputer -filter {enabled -eq $True} | Select-Object Name | Export-CSV "c:\temp\disabled-machines.csv" -NoTypeInformation
- This will dump a list of disabled machines to a text file but only list their names:
Get-ADComputer -filter {enabled -eq $False} | Select-Object Name | Out-File "c:\temp\disabled-machines.txt"
- This will list machines in a certain OU, filtering the results so it only lists client PCs that are not disabled:
Get-ADComputer -Searchbase "OU=Workstations,OU=HN Computers,OU=HomeNet,DC=HOMENET,DC=local" -Filter { (OperatingSystem -NotLike "*Windows Server*") -and (Enabled -eq "True") }
- This will search for machines in a certain OU, filtering the results so it only lists client PCs that are not disabled and goes a little further by not listing machines with a host-name starting with CONF-. This exports a list of host-names to a CSV:
Get-ADComputer -Searchbase "OU=Workstations,OU=HN Computers,OU=HomeNet,DC=HOMENET,DC=local" -Filter { (OperatingSystem -NotLike "*Windows Server*") -and (Enabled -eq "True") -and (Name -NotLike "CONF-*") } | Select-Object Name | Export-CSV "c:\client-pcs.csv" -NoTypeInformation
- This will search for machines in a certain OU, filtering the results so it only lists client PCs that are not disabled and then places the results in table-format - listing OS and Host-Name:
Get-ADComputer -Searchbase "OU=Workstations,OU=HN Computers,OU=HomeNet,DC=HOMENET,DC=local" -Filter { (OperatingSystem -NotLike "*Windows Server*") -and (Enabled -eq "True") } -Properties OperatingSystem | Format-Table Name,OperatingSystem -AutoSize
Working with User Objects
List Information About User Objects
- This will show you a list of disabled users:
Get-ADUser -filter {enabled -eq $False} | Select-Object Name
- This will also show you a list of disabled users but it uses the
Search-ADAccountcmdlet:Search-ADAccount –AccountDisabled –UsersOnly | FT Name
- This will give you a list of enabled users in a certain OU and export it to a CSV:
Get-ADUser -Searchbase "OU=Valley Creek,OU=HN Users,OU=HomeNet,DC=HOMENET,DC=local" -Filter { (Enabled -eq "True") } | Select-Object Name | Export-CSV "C:\users.csv" -NoTypeInformation
- This will do a few things. It'll search for domain controllers in your forest. Then, it'll run the same command on all of the ADDCs that were found to see where and when a user account was possibly locked out. Then, it spits it out into a grid view GUI that can be sorted as needed. This can help track down rogue scripts with bad creds. We used to use the Account Lockout Status tool for this sort of thing but it's nice to know you can script it:
(Get-ADForest).Domains | ForEach-Object { Get-ADDomainController -Discover -DomainName $_ } | ForEach-Object { Get-ADDomainController -server $_.Name -filter * } | ForEach-Object { Get-ADUser -Identity "rsickler" -Properties BadLogonCount, badPWDCount, LastBadPasswordAttempt, LastLogonDate, LockedOut, Enabled -Server $_ } | Out-GridView
- This will do the same as above but you'll specify the ADDC(s) you want to run the command against. In this case, I'm running the command against three ADDCs -
ACADEMICPDC,ACADEMICDC2&ACADEMICDC3:("ACADEMICPDC","ACADEMICDC2","ACADEMICDC3") | ForEach-Object {Get-ADUser -Identity rsickler -Properties BadLogonCount, badPWDCount, LastBadPasswordAttempt, LastLogonDate, LockedOut, Enabled -Server $_} | Out-GridView
- This will list all properties of a user. Watch for issues with this. I've seen this fail when the user's primary group was not set to Domain Users:
Get-ADUser rsickler -Properties *
- This will list some properties of a user:
Get-ADUser fmiller -Properties * | Select-Object Name, EmailAddress, Enabled, LockedOut, PasswordExpired, PasswordLastSet, PasswordNeverExpires, AccountExpirationDate
- To list all of the locked out users in a certain OU, run something like this:
Search-ADAccount -LockedOut -UsersOnly -SearchBase "OU=Employees,OU=NonSCE,OU=Users,OU=HMN,DC=na,DC=autotrader,DC=int"
- This will list locked out users in a certain OU - so long as they are not disabled:
Search-ADAccount -LockedOut -UsersOnly -SearchBase "OU=Employees,OU=NonSCE,OU=Users,OU=HMN,DC=na,DC=autotrader,DC=int" | Where-Object { $_.Enabled -eq $true }
- To unlock all locked accounts in a certain OU, run something like this:
Search-ADAccount -LockedOut -UsersOnly -SearchBase "OU=Employees,OU=NonSCE,OU=Users,OU=HMN,DC=na,DC=autotrader,DC=int" | Unlock-ADAccount
- To unlock all locked accounts in a certain OU and be prompted for each one, run something like this:
Search-ADAccount -LockedOut -UsersOnly -SearchBase "OU=Employees,OU=NonSCE,OU=Users,OU=HMN,DC=na,DC=autotrader,DC=int" | Unlock-ADAccount -Confirm
- This is a quick and dirty way to figure out if a user is locked out; it will report a boolean value2):
(Get-Aduser "rlsickler" -Properties LockedOut).LockedOut
- This will check an account to see if it's locked. If it is, the script will warn you of such and unlock the account:
$Account = "rlsickler" $LockedUser = Get-ADUser -Identity $Account -Properties LockedOut If ($LockedUser.lockedout -eq $true){ Write-Warning -Message "The account, $Account, is locked!" Unlock-ADAccount $Account } else { Write-Host -ForegroundColor Green "The account, $Account, is not locked." }
- This expands on the code above. For 10 minutes, the script checks an account every 20 seconds to see if it's locked. If it is, it'll unlock it:
$Account = "rlsickler" $LockedUser = Get-ADUser -Identity $Account -Properties LockedOut $timeout = New-TimeSpan -Minutes 10 $sw = [diagnostics.stopwatch]::StartNew() While ($sw.elapsed -lt $timeout){ If ($LockedUser.lockedout -eq $true){ Write-Warning -Message "The account, $Account, is locked!" Unlock-ADAccount $Account } else { Write-Host -ForegroundColor Green "The account, $Account, is not locked." } Start-Sleep -seconds 20 } Write-Host "Time limit reached."
Working with AD Groups
List Groups
- This will give you a list of security groups in AD and dump them to a CSV:
Get-ADGroup -Filter { (GroupCategory -eq 'security') } -SearchBase "OU=Security,OU=Groups,OU=HMN,DC=na,DC=autotrader,DC=int" | Select-Object Name, GroupCategory | Export-CSV "C:\Groups.csv" -NoTypeInformation
- This will give you a list of Distribution Lists (DLs) in AD:
Get-ADGroup -Filter { (GroupCategory -eq 'Distribution') } -SearchBase "OU=Distribution Lists,OU=Groups,OU=HMN,DC=na,DC=autotrader,DC=int" | Format-Table Name -AutoSize
- This will scan a certain OU and give you an alphabetized list of security groups in AD, showing their names and descriptions:
Get-ADGroup -Filter { (GroupCategory -eq "security") } -SearchBase "OU=Security,OU=Groups,OU=HMN,DC=na,DC=autotrader,DC=int" -Properties Name, Description | Sort-Object “Name” | FT Name, Description
- If you know a partial name of the group(s) you are looking for, you can search via a wildcard:
Get-ADGroup -Filter { (Name -Like "*DEV_INFRASTRUCTURE_*") } | Select Name
List Members of a Group
- This will give you an alphabetically-sorted list of users, in table-format, of a group:
Get-ADGroupMember "IOL_Users" | Sort-Object "Name" | Format-Table "Name"
- This will give you a list of users, in table-format, of a group. However, if the group contains child-groups, those members will also be listed:
Get-ADGroupMember "HMN_STCOSD14_HMS_MODIFY" -Recursive | FT "Name" -AutoSize
FTis short forFormat-Table.
- This will dump a list of the members of a group into a CSV:
Get-ADGroupMember "HMN_STCODD14_ARKONA_MODIFY" | Select-Object "Name" | Export-CSV "c:\HMN_STCODD14_ARKONA_MODIFY.csv" -NoTypeInformation
- This will dump a list of the members of a group, and their e-mail addresses, into a CSV:
Get-ADGroupMember "~HMN - Employees" | Select-Object "SamAccountName" | %{Get-ADUser $_.samaccountname -Properties mail} | Select-Object "Name", "Mail" | Export-CSV "C:\Emails.csv" -NoTypeInformation
- This is a bit more complex but, say you had a list of groups and you needed to find the members of each of them. While you could use one of the examples above - one at a time - for each group on your list, there's a better way. Note the use of a calculated property, when using
Select-Object, to give us custom table-headings. In this case, it allows us to put the group's name on the line with the user for a clean & easy-to-read look. It also allows us to rename the default properties of Name and SamAccountName when outputting the data to the CSV:$Groups = "HMN_PRODNAS1_INBOUND_MODIFY","HMN_PRODNAS1_INBOUND_READ" $ResultsArray =@() ForEach ($Group in $Groups) { $ResultsArray += Get-ADGroupMember -Id $Group | Select-Object @{Expression={$group};Label="Group"},@{Expression={$_.Name};Label="Member's Name"},@{Expression={$_.SamAccountName};Label="Member's SAM Account Name"} } $ResultsArray | Export-CSV -Path "C:\GroupListResult.csv" -NoTypeInformation
- This too will generate a similar CSV but requires you to supply a text file populated with the groups you wish to query - one per line::
$Groups = Get-Content "C:\Groups.txt" $ResultsArray =@() ForEach ($Group in $Groups) { $ResultsArray += Get-ADGroupMember -Id $Group | Select-Object @{Expression={$group};Label="Group"},@{Expression={$_.Name};Label="Member's Name"},@{Expression={$_.SamAccountName};Label="Member's SAM Account Name"} } $ResultsArray | Export-CSV -Path "C:\GroupListResult.csv" -NoTypeInformation
Add an AD Group
- This will add a new security group:
New-ADGroup -GroupCategory Security -Name "IOL_APPLICATIONS_PROD_READ" -Path "OU=HN Groups,OU=HomeNet,DC=HOMENET,DC=local" -GroupScope DomainLocal -Description "Grants read only access to the resource." -Server dc1.homenet.local
- This will add a new DL:
New-ADGroup -GroupCategory Distribution -Name "DEVOPPS" -Path "OU=HN Groups,OU=HomeNet,DC=HOMENET,DC=local" -GroupScope DomainLocal -Description "DevOpps Team" -Server dc1.homenet.local
Clean up AD
- This will remove disabled computer objects from AD:
Search-ADAccount -AccountDisabled -ComputersOnly | Sort-Object | Remove-ADComputer
- This will remove disabled computer objects from a certain OU:
Search-ADAccount -AccountDisabled -SearchBase "OU=Workstations,OU=HN Computers,OU=HomeNet,DC=HOMENET,DC=local" -ComputersOnly | Sort-Object | Remove-ADComputer
- This will disable user objects, in a certain OU, if they've not had their password changed in 90 days and they have not been used for authentication in 90 days:
Get-ADUser -SearchBase 'OU=HN Users,OU=HomeNet,DC=HOMENET,DC=local' -filter * -Properties LastLogonDate, PasswordLastSet | Where-Object { $_.Enabled -eq $true -and $_.LastLogonDate -le (Get-Date).AddDays(-90) -and $_.PasswordLastSet -le (Get-Date).AddDays(-90) } | Set-ADUser -Enabled $false
- This will disable computer objects in a certain OU if they've not authenticated against the domain in 120 days:
Get-ADComputer -SearchBase "OU=VM Computers,OU=GV Computers,OU=GVSD,DC=gvsd,DC=org" -filter * -Properties LastLogonDate | Where-Object { $_.enabled -eq $true -and $_.LastLogonDate -le (Get-Date).AddDays(-120) } | Set-ADComputer -Enabled $false
Working with IIS
Using Powershell to Manage IIS
With IIS 7 and later, you can use Powershell to do a lot. I've barely scratched the surface with the few commands listed here. However, to use a lot of these, you need to have the Management Scripting Tools installed for IIS.
Import-Module WebAdministration
Export a List of Sites
- To export a list of sites, showing the sites' names and their respective application pools, run the following command:
Get-WebSite | Select-Object Name, ApplicationPool | Export-Csv -NoTypeInformation -Path "C:\_Temp\WebSites.csv"
Using Powershell Remotely
One can use PS remotely much like one can SSH into a Linux system and run commands as though they were local to the machine. Like with most things, you need to have all the correct permissions and settings for this to work.
PSRemoting
To get this to work, you need to enable PSRemoting. This is done on the server/host machine. It can be done a multitude of ways but we'll go into the manual method here.
On the Server/Host
With Linux, you need to allow remote access. Well, it's the same with Windows. Luckily, one or two commands will normally allow the access you need. First, the network category needs to be set to DomainAuthenticated or Private; if it's set to Public, this won't work. So, check that first and set it as needed. Obviously, these commands require an elevated prompt.
- Check the network category:
Get-NetConnectionProfile- You should see something like this:
Name : gvsd.org InterfaceAlias : Wi-Fi InterfaceIndex : 7 NetworkCategory : DomainAuthenticated IPv4Connectivity : Internet IPv6Connectivity : NoTraffic
- If needed, set it to, at the very least, Private. If you're on a domain, it's likely already set to DomainAuthenticated and that's fine:
Set-NetConnectionProfile -Name gvsd.org -NetworkCategory Private
- So long as your network category is not set to Public, you should be able to enable PSRemoting which will also punch holes in the Windows Firewall as needed:
Enable-PSRemoting
On the Client
Once your server/host is set correctly, you should be able to connect to it from a remote client so long as your network allows the traffic.
- Open a remote PS session:
Enter-PSSession -ComputerName H2S3-IOLWEB2 -Credential homenet\rsickler
- Close a remote PS session:
Exit-PSSession
Example
Once connected via a remote session, you can run most commands you'd run from the console if you were sitting in front of it. Some won't work and you'll see a message on the screen if you try to run those.
- While not the best example, below, is a quick and dirty way of getting some info from a remote host. It shows how to jump in, run the command you want and how to back out once you're finished:
PS C:\WINDOWS\system32> Enter-PSSession -ComputerName daedalus.gvsd.org -Credential [email protected] [daedalus.gvsd.org]: PS C:\Users\RSickler2\Documents> Get-CimInstance Win32_OperatingSystem | Select-Object Caption, InstallDate, ServicePackMajorVersion, OSArchitecture, BootDevice, BuildNumber, CSName | Format-List Caption : Microsoft Windows Server 2012 R2 Standard InstallDate : 5/27/2015 11:05:51 PM ServicePackMajorVersion : 0 OSArchitecture : 64-bit BootDevice : \Device\HarddiskVolume1 BuildNumber : 9600 CSName : DAEDALUS [daedalus.gvsd.org]: PS C:\Users\RSickler2\Documents> Exit-PSSession PS C:\WINDOWS\system32>
Invoking a Remote Command
Once PSRemoting has been enabled, you can also invoke various commands without entering a PSSession. There's another site that has a lot of examples as well.
Example Scripts
- You've deployed hundreds of machines with PSRemoting enabled but you forgot to enable Remote Desktop Protocol access. You can remotely enable it - if PSRemoting is enabled. Below, is a script I've used to do just that. As mentioned in the notes, it pulls a list of computer names from a text file. The script parses a text file that has one computer name per line. For each computer name in the list, it checks connectivity and then invokes a command to tweak the registry key that enables RDP access:
- Enable_RDP_Remotely_Invoke-Command.ps1
<# This will enable RDP access - remotely - so long as PSRemoting has been enabled. Otherwise, you'd likely need to do this by hand. It pulls computer names from a text file. Adjust paths as needed. #> $Computers = Get-Content "C:\_Temp\Machines.txt" $Creds = Get-Credential rsickler2@gvsd.org ForEach-Object ($Computer in $Computers) { if (Test-Connection -ComputerName $Computer -Count 1 -BufferSize 16 -ErrorAction SilentlyContinue -Quiet) { Invoke-Command –Computername $Computer –ScriptBlock { Set-ItemProperty -Path "HKLM:\System\CurrentControlSet\Control\Terminal Server" -Name "fDenyTSConnections" –Value 0 } -Credential $Creds } else { Write-Output "$computer is offline" | Out-File "C:\_Temp\CopyErrors.txt" -Append } }
- You can do the same thing but with a script you have on the local machine. Below, is a script I've used before. The script goes through that list of machines and, for each computer name in the list, it checks for connectivity and then calls another script on my desktop. Powershell will send the script immediately to the first 32 computers in the list if you have 32 or fewer computer names in said list. If you have more than 32 computer names, Powershell will queue the rest as needed. Once more get done, the script will get copied to the next one in the queue until the script completes all PSSessions. This default behavior can be changed with the
-ThrottleLimitparameter - if you see fit:- Enable_RDP_Remotely_Invoke-Command_Script.ps1
# This allowed me to run a local script on remote machines. $Computers = Get-Content "C:\_Temp\Machines.txt" $Creds = Get-Credential rsickler2@gvsd.org foreach ($Computer in $Computers) { if (Test-Connection -ComputerName $Computer -Count 1 -BufferSize 16 -ErrorAction SilentlyContinue -Quiet) { Invoke-Command -ComputerName "$($Computer)" -FilePath "$env:USERPROFILE\MyAwesomeScript.ps1" -Credential $Creds } else { Write-Output "$computer is offline" | Out-File "C:\_Temp\CopyErrors.txt" -Append } }
Regex and Powershell
I'm no expert with Regular Expressions (Regex) but I've found it to be useful on many occasions. I've used it for manipulating text in Notepad++ but, now and then, you find yourself using it in various scripts. Here are some examples that I've used over the years.
Seeking out IPs
I used to help manage a SmarterMail mail server. Our config would capture potential SPAM emails and drop them into a holding cell. We'd go through the folder daily and weed out the few messages that were accidentally flagged as SPAM. I wrote a script to handle it and it uses some RegEx
When the raw mail files (EML and HDR) would get flagged as SPAM, they'd get prefixed with the sender's egress IP. So, the message would go from something like My cat photos.eml and My cat photos.hdr to 64.206.246.226IP.My cat photos.eml and 64.206.246.226IP.My cat photos.hdr. To drop the messages back into the processing queue, we'd manually rename the files, stripping out the 64.206.246.226IP. piece of it, and move them into the proper folder. Doing this for hundreds of files a day is mind-numbing! My script below seeks out any file with said preceding text and moves it - renaming it on the fly.
The Script
- Rename_SmartMail_Spam_files.ps1
# Useful for dealing with the SPAM spools on SmarterMail $Source = "\\h2p-mail1\Smartermail\Spool\spam\hold2" $Dest = "\\h2p-mail1\Smartermail\Spool" Get-ChildItem $Source\* -Include "*.eml","*.hdr" | Foreach-Object { Move-Item $Source\$($_.Name) $Dest\$($_.Name -Replace '(\d{1,3}\.){3}(\d{1,3}IP)\.','') }
The Breakdown
I'll do my best to break this down. Note the use of the -Include parameter for Get-ChildItem is telling the code to only touch the following two file-types: EML and HDR.
- The magic RegEx string is:
'(\d{1,3}\.){3}(\d{1,3}IP)\.',''- This part matches any digit (0-9) up to three times (Basically a numerical value that's between one and three digits long.) The max number any single octet can have is
255and that consists of three digits. In our example IP,64.206.246.226, the first octet only has two digits but the last three octets have three digits each:\d{1,3} - This part is escaping the period because you need to search for it as part of the string of text (IPv4 addresses have periods.) and a period is a special character in RegEx so you need to escape it if you want to search for it:
\.
- This part is telling the code to search for that string -
(\d{1,3}\.)- three times. An IPv4 IP address has four octets. However, the last octet doesn't end with just a period; it ends withIP.so, the last bit of code -(\d{1,3}IP)\.- is searching for that on the back end of the other three octets:{3} - Lastly, this part tells Powershell to replace anything matching the search string with nothing as there's nothing between those two single quotes:
''
Networking Tasks with Powershell
Getting the Info to Manage a Network Interface Controller (NIC)
The following commands will list certain info for your NICs. Some of the info is needed to make changes so some of these commands are generally used prior to managing your NICs.
- This displays all NICs, their interface index number, and their priority:
Get-NetIPInterface - Another cmdlet will also show some of the same properties:
Get-NetAdapter - If you have several NICs, you may want to sort them. When you run either of the commands above, you should see info dumped into a table with multiple columns. You can use
Sort-Objectto sort the data based on those columns. In the example below, I sorted the data via theifIndexproperty but I could have chosen any of the other valid properties for the cmdlet:Get-NetIPInterface | Sort-Object "ifIndex" | Format-Table -AutoSize
Setting IP Addresses and DNS Server Addresses
Once you've used something similar to the commands above, you can use some of that info to make changes. Setting a static IP is one of those things you can do.
- Using one of the commands above, you should have seen a value for the ipIndex This value can be used for setting a static IP. So, assuming the ipIndex is 5, we can give that NIC a static IP with the following info:
- An IP address of 10.244.211.35.
- A subnet mask of 255.255.255.0 which is shown via the CIDR notation or, sometimes called, slash notation number - 24.
- A gateway: 10.244.211.1
- The command would look something like this:
New-NetIPAddress –InterfaceIndex 5 –IPAddress "10.224.211.35" –PrefixLength 24 –DefaultGateway "10.224.211.1"
- You may also need to set the primary and secondary DNS addresses for that same NIC:
Set-DNSClientServerAddress –InterfaceIndex 5 -ServerAddresses "10.224.210.7,10.224.210.8"
- If you ever needed to set the NIC to pull DNS server addresses from DHCP, you would do something like this:
Set-DnsClientServerAddress –InterfaceIndex 5 –ResetServerAddresses
- Setting the NIC to pull an IP address from DHCP would look like this:
Set-NetIPInterface -InterfaceIndex 5 -Dhcp Enabled
Bulk Changing of IP Info on Multiple Servers
This is something I was tasked with years ago. I had to make changes to NICs on hundreds of servers. I was not about to do it by hand so I wrote scripts to do it. The servers had multiple NICs configured so I had to make sure I was changing the correct NIC's config and that complicated matters a bit.
- This script filters out all NICs other than the NIC configured with the gateway specified. It then replaces the existing DNS servers for that NIC. It gets a list of computers, on which to run the command, from a text file:
- Change_DNS_Based_On_Gateway.ps1
# Changes the DNS servers for a NIC with a specific gateway so it doesn't change another NIC's DNS servers. $computer = Get-Content C:\temp\servers.txt $NICs = Get-WMIObject Win32_NetworkAdapterConfiguration -ComputerName $computer | Where-Object {$_.DefaultIPGateway -eq "192.168.0.1"} ForEach-Object ($NIC in $NICs) { $DNSServers = “10.224.202.236",”10.224.202.237" $NIC.SetDNSServerSearchOrder($DNSServers) $NIC.SetDynamicDNSRegistration(“TRUE”) }
- This script filters out all NICs other than the NIC configured with a certain subnet. It then replaces the existing DNS servers for that NIC with a NULL value - which basically removed the unneeded DNS servers in our case. It gets a list of computers, on which to run the command, from a text file:
- Change_DNS_Based_On_Subnet.ps1
# Changes the DNS servers for a NIC with a specific subnet so it doesn't change another NIC's DNS servers. $computer = Get-Content C:\temp\servers.txt $NICs = Get-WMIObject Win32_NetworkAdapterConfiguration -ComputerName $computer | Where-Object {$_.IPAddress -like "172.16.234.*"} ForEach-Object ($NIC in $NICs) { $NIC.SetDNSServerSearchOrder() $NIC.SetDynamicDNSRegistration(“TRUE”) }
Setting the Connection Profile
When your PC connects to a network, via a cable or a wireless network, Windows tries to place the network in one of three categories. Certain settings are then tweaked based on these categories - Public, Private and DomainAuthenticated. For instance, if it's set as a Public network, the Windows Firewall blocks more traffic than it would had you selected a Private network. The Private network profile will allow for things like shared folder and printer access but, if the profile is set to DomainAuthenticated, certain ports that are used for remote admin work get opened. I've had to change this from time to time and, frankly, I forget where to make these changes in the GUI so I always fall back to PS.
- You can see the current profile settings like this:
Get-NetConnectionProfile - Assuming we're still using the same NIC as above, we can change the profile like this:
Set-NetConnectionProfile -InterfaceIndex 5 -NetworkCategory Private
Using the Connection Profile for Printer Installations
I've used Get-NetConnectionProfile in a logon script for installing printers. Basically, it would make sure the user was connected to the proper network before trying to install printers the network printers. So, if the user was at home, it wouldn't waste any time trying to make sure the printers were installed. Below is the script I used. Never mind the reasons why it was used; it's just another example of pulling info from a NIC and using it accordingly.
- Add_Printers_Logon.ps1
<# This should work in Windows 10 & Server 2016. To use this script, create a shortcut in common startup (START > RUN > SHELL:COMMON STARTUP). The shortcut should call PS something like this: powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -NonInteractive -NoLogo -File "C:\Scripts\add_printers_logon.ps1" #> # I've added a sleep-timer to avoid a timing issue with getting the system and its networking up and running. # This can be adjusted to suit your needs. Start-Sleep -Seconds 15 # Testing for an active network connection. $upTest = ( Get-NetConnectionProfile | Where-Object {$_.IPv4Connectivity -ne "NoTraffic"} ) # Print server. Some prefer an IP or NetBIOS name here. $PrintServer = "prntsrvr2.gvsd.org" # If the active network connection is on the correct domain, the printer installations will begin. if ($upTest.NetworkCategory -eq "DomainAuthenticated" -and $upTest.Name -ieq "gvsd.org" ) { Add-Printer -ConnectionName "\\$PrintServer\Color402_q" Add-Printer -ConnectionName "\\$PrintServer\Copy Center B-W_Q" Add-Printer -ConnectionName "\\$PrintServer\Copy Center Color_Q" Add-Printer -ConnectionName "\\$PrintServer\Print_Q" } else { exit }
Miscellaneous PS Commands
I don't particularly like having a miscellaneous section but I'd rather not create subsections for every little command or script I've ever used when they don't fit in with the rest of this wiki.
Restart or Shutdown a Computer
These are very simple commands but also very useful. In a properly configured domain environment, they're very useful for working with remote machines.
- To restart the local machine use this command:
Restart-Computer -Force
- To restart a remote machine use something like this. Note the use of the user at dotted domain format in lieu of the domain-prefixed user:
Restart-Computer -Force -ComputerName H2P-IOLDP45 -Credential rsickler@homenet.local
- To turn off the local machine use this command:
Stop-Computer -Force
- To turn off a remote machine use something like this. Note the use of the domain-prefixed user in lieu of the user at dotted domain format:
Stop-Computer -Force -ComputerName H2P-IOLDP45 -Credential homenet\rsickler
Rename a Computer
These commands can be used locally or, in a properly configured domain, you can use them remotely.
- Rename the local machine, and get prompted for the name on the fly. You'll be told to reboot the next chance you get:
Rename-Computer - Rename the local machine while specifying the new name in the command and forcing the reboot immediately afterwards:
Rename-Computer -NewName ms-sped-240 -Restart
- Rename a remote machine by specifying various bits of needed info via several parameters:
Rename-Computer -ComputerName ms-sped-24 -DomainCredential rsickler2@gvsd.org -NewName ms-sped-240 -Restart -PassThru -Force
Join the Computer to the Domain
- Join a local computer to the domain while being prompted for various info on the fly:
Add-Computer - Join the local computer to the domain while specifying the domain name in the command:
Add-Computer -DomainName gvsd.org
- Join the remote computer, SB2 to the domain while specifying various bits of needed info via several parameters:
Add-Computer -DomainName gvsd.org -ComputerName sb2 -LocalCredential "sb2\Administrator" -Restart -Credential rsickler@gvsd.org -Verbose
Remove the Computer from the Domain
- Remove the local machine from the domain, allowing the command to prompt you with various questions:
Remove-Computer - Remove a remote machine from the domain whilst specifying various bits of needed info via several parameters:
Remove-Computer -UnjoinDomainCredential rsickler2@gvsd.org -ComputerName sb2 -PassThru -Restart -Force
Working with Features and Roles on a Server
With relative ease, one can use PS to manage Features & Roles. I last used many of these on Server 2012-R2.
List Features and Roles on a Server
- List all Features & Roles on a server:
Get-WindowsFeature - List all Features & Roles on a server in a table format:
Get-WindowsFeature | FT -AutoSize
- List all Features & Roles on a server that start with Server-GUI-:
Get-WindowsFeature | Where { $_.Name -Like "server-gui-*" } | FT -AutoSize
- If you know exactly what package you're looking for on a server, you can run something like:
Get-WindowsFeature -Name Web-Mgmt-Console
- List all Features & Roles on a server and show their InstallState:
Get-WindowsFeature | FT Name, InstallState -AutoSize
Install Features and Roles on a Server
One can use PS to do one-at-a-time installs or one could write a PS script to handle it. Generally speaking, if you're just installing a small number of packages at a time, Server 2012-R2 is smart enough to tell you when a package installation can't complete due to a dependency.
- Install one package:
Install-WindowsFeature Telnet-Client
- Install several packages:
Install-WindowsFeature Telnet-Client, Telnet-Server
- Installing a package while specifying a WIM file and index as a source and allowing the server to reboot as needed:
Install-WindowsFeature server-gui-shell -source:wim:d:\sources\install.wim:2 -restart:$true
Uninstall Features and Roles on a Server
Like with the installations, one can use PS to do one-at-a-time uninstalls or one could write a PS script to handle it.
- Uninstall some features and reboot the server as needed:
Uninstall-WindowsFeature server-gui-shell, server-gui-mgmt-infra -restart:$true
- Uninstall some features but don't reboot the server:
Uninstall-WindowsFeature server-gui-shell, server-gui-mgmt-infra -restart:$false
- Uninstall some features and remove their payloads from the server:
Uninstall-WindowsFeature server-gui-shell, server-gui-mgmt-infra, Telnet-Client, Telnet-Server -remove
- This is dangerous but clears up more space. Only do this on a servers you're sure will never need these other packages. This will uninstall all features with the InstallState flag set to Available, removing the payload for each package:
Get-WindowsFeature | Where-Object { $_.InstallState -Eq “Available” } | Uninstall-WindowsFeature -Remove
Useless Fun with PS
Mask an Email Address
I had seen an example of this in a signature out on a forum. It took some digging but I got it working.
- Measure the number of characters in your email address; mine has 24:
"[email protected]" | Measure-Object -Character | Select-Object Characters
- Get the integer for each character in your email address:
[int[]][char[]]"[email protected]"
- You can join them to keep them on one line for easy reading:
[int[]][char[]]"[email protected]" -join " "
- You should see a line of numbers; something like this:
80 105 110 101 86 97 108 108 101 121 84 111 109 101 64 103 109 97 105 108 46 99 111 109
- Note how it's a mix of 2-digit & 3-digit numbers. Long story short, you can work with those numbers but it's not ideal or as fun. You want uniformity so that when you put them all back together, it looks right. If all the numbers were 2 or 3 digits, and not a mix of the two, it would be much easier.
- What can we do to all of those numbers to make them all the same number of digits long? Note the smallest & largest numbers. In my case, they're 46 & 121, respectively.
- We could add 54 to 46 & get 100 and that would give us a 3-digit number. We could subtract 22 from 121 and get 99 and that would give us a 2-digit number. However, you'd need to do the same for all of them so pick one and go with it.
- In my case, I chose a nice round number (30) and subtracted it from all my numbers, making them all 2-digits long:
([int[]][char[]]"[email protected]" | %{ $_ - 30 }) -join " "
- Once again, you should see a line of numbers but they should all be the same number of characters long; something like this:
50 75 80 71 56 67 78 78 71 91 54 81 79 71 34 73 79 67 75 78 16 69 81 79
- Or, if you pack them together:
([int[]][char[]]"[email protected]" | %{ $_ - 30 }) -join ""
- You'd get this:
507580715667787871915481797134737967757816698179
- Remember when you counted the number of characters your email had? Here's where you'll use it. You'll see (in the code below) the range operator:
0..23. If you had enough fingers and you were to count from 0 through 23, you'd actually use 24 fingers - the number of characters in my email address. In the code below, we're adding 30 to that long number:30+.- If, in the prior steps, you added instead of subtracted, you'd do the reverse here. I subtracted 30 so now I'm adding 30.
- It's not actually adding 30 to the long number; the substring, on the back of the command, is breaking the number up into 2-digit pieces and adding 30 to each piece:
[string](0..23|%{[char][int](30+("507580715667787871915481797134737967757816698179").substring(($_*2),2))})-replace " "
- Running the code above should spit out:
[email protected]