Copied from [[https://wiki.cherryblossomfarmette.com:8443/doku.php?id=powershell|Rob Sickler's Wiki]] ====== Powershell ====== Powershell is quickly replacing the [[cmd|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 [[https://en.wikipedia.org/wiki/.NET_Framework|.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. You can call PS scripts or //cmdlets// right from the command line if needed however, although you can run long and complex commands this way, it can get messy rather quickly. For //complex// stuff, a script is usually easier to read and troubleshoot. * 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\VMware'' with the use of the ''ForEach-Object'' cmdlet: 1..4 | ForEach-Object {mkdir "C:\_Drivers\VMware\test$_"} * This will do the same but ''ForEach-Object'' has been substituted with the alias of ''ForEach'': 1..4 | ForEach {mkdir "C:\_Drivers\VMware\test$_"} * This will do the same but ''ForEach-Object'' has 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 ''-Persist'' parameter 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 [[https://en.wikipedia.org/wiki/ISO_image|ISOs]] & [[https://en.wikipedia.org/wiki/VHD_(file_format)|VHDs]]. Sometimes you can just right-click one and mount the file. Worst case, you can also do this via Powershell. Note that you may need admin access to mount VHDs. * 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 machine((Remote machines take a little more //work//.)). * 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. If you're working on a **Active Directory Domain Controller (ADDC)**, the tools should already be installed. However, if you're working on a Windows 10 client or another server that isn't an ADDC, it will likely be lacking the tools you need. In those cases, you'll likely need to install the **Remote Server Administration Tools (RSAT)** which is available for most modern Windows operating systems - servers and desktops alike. The tool kit has many tools for remotely managing Windows machines but, if you want to perform Active Directory management tasks, you need to - at the very least - install the proper bits to do so. Once you have the proper tools installed, and you have the correct permissions, you should be able to do most of what is written herein. The only other thing you may run into is whether or not you'll need to import the AD module in PS ahead of time. This, however, usually only pertains to **older versions** of PS and/or Windows. In those cases, you can just open PS and run the following to spin up the module: 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 ''FT'' is short-hand for ''Format-Table''. the ''-a'' is short for ''Auto-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-ADAccount'' cmdlet: 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 } * This will list **locked out** users, in a **certain OU**, who also have **expired passwords**: Search-ADAccount -UsersOnly -SearchBase "OU=Employees,OU=NonSCE,OU=Users,OU=HMN,DC=na,DC=autotrader,DC=int" -LockedOut | Where-Object { ($_.Enabled -eq $true) -and ($_.PasswordExpired -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 value((true or false)): (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 * ''FT'' is short for ''Format-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 [[https://en.wikipedia.org/wiki/Internet_Information_Services|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. In some cases, depending on the version of the OS and PS, you may need to import the Web-Admin module prior to running other commands: 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 rsickler2@gvsd.org [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 [[https://4sysops.com/archives/use-powershell-invoke-command-to-run-scripts-on-remote-computers/|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: <# 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 ''-ThrottleLimit'' parameter - if you see fit: # 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 [[https://en.wikipedia.org/wiki/Regular_expression|Regular Expressions (Regex)]] but I've found it to be useful on many occasions. I've used it for manipulating text in [[https://notepad-plus-plus.org/|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 [[https://www.smartertools.com/|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 === # 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 ''255'' and 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 with ''IP.'' 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 [[https://en.wikipedia.org/wiki/Network_interface_controller|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-Object'' to sort the data based on those columns. In the example below, I sorted the data via the ''ifIndex'' property 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 [[https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_notation|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: # 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: # 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. <# 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. Uninstalling the package will do just that - uninstall it. If you wish to remove the **payload** - changing the **InstallState** flag to **Removed** - you can do that too. Doing so will clear up more space but, if you ever want to reinstall it, you'll need to supply the source files. In most cases, I prefer not to remove the payload due to the issues one has when trying to supply source files on a server which has a **patch-level** significantly **higher** than that of the source files. * 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**: "PineValleyTome@gmail.com" | Measure-Object -Character | Select-Object Characters * Get the integer for each character in your email address: [int[]][char[]]"PineValleyTome@gmail.com" * You can join them to keep them on one line for easy reading: [int[]][char[]]"PineValleyTome@gmail.com" -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. If your smallest number was something like 30, and you subtracted 22 from it, you'd end up with 8 - a 1-digit number. So, be mindful of your math and make sure you don't break the uniformity you're looking for. * 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[]]"PineValleyTome@gmail.com" | %{ $_ - 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[]]"PineValleyTome@gmail.com" | %{ $_ - 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: PineValleyTome@gmail.com