Click an Ad

If you find this blog helpful, please support me by clicking an ad!

Wednesday, December 9, 2015

Command by Command: My Standard Ubuntu Server Build

These are the instructions that I use to build my Ubuntu Server VMs. Once I get done with these, I then add any other software that the server needs. I live in a Microsoft world, so creating this was quite an exercise and took a long time. It was initially created on Ubuntu 14.04.1, but I just ran through it on 14.04.3 and it was fine. To that end, I used to have a section on installing VMware Tools manually, but I got a prompt on 14.04.3 that I should use open-vm-tools, so I'm going that route.

Here we go:

Ubuntu 64-bit (14.04.3 tested (original written on 14.04.1)

My Standard VM build:
60GB HDD
Network Connection (with internet)
4GB RAM
1 CPU
Obviously change depending on your ultimate use case.

During Installation:
All defaults except:
Hostname
Non-Root User Account
Password
Proxy, if needed
Security Automatic Updates only
No Package Installation
Remove disk, reboot

Log in
Change login to root:
sudo su -

Install open-vm-tools with
apt-get update
apt-get install open-vm-tools

Configure static IP Address, etc:
nano /etc/network/interfaces
change 'iface eth0 inet dhcp' to 'iface eth0 inet static'
add the following lines:
address <ipaddress>
gateway <gateway>
netmask <Mask>
dns-nameservers <DNSServersSeperatedByASpace>
Restart the computer
Log in as non-root user
Verify connectivity using ifconfig, ping, nslookup

Update apt-get
sudo apt-get update
Install Ubuntu patches:
sudo apt-get upgrade

Install and configure OpenSSH:
apt-get install openssh-server
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.default
chmod a-w /etc/ssh/sshd_config.default
addgroup sshusers
nano /etc/ssh/sshd_config
Change the following:
X11Forwarding no
LogLevel VERBOSE
LoginGraceTime 30
                        MaxStartups 2:30:10
Add the following lines:
AllowTcpForwarding no
AllowGroups sshusers
usermod -a -G sshusers <Non-Root User>
Restart the SSH service:
sudo restart ssh
Run this command to rate limit the SSH Connections (if more than 10 attempts within 30 seconds, all the following attempts will fail since the connections will be DROPped.)
sudo ufw limit ssh

Created DNS A and PTR records
Verified SSH works for Non-Root user

Set up UFW (Uncomplicated Firewall) (AS ROOT):
ufw allow ssh
ufw logging on
ufw enable
TO SHOW STATUS: ufw status
LOG FILE LOCATION: /var/log/ufw.log

Prior to setting up sendmail, ensure your mailserver will accept anonymous mail from this server's IP address.

Set up the ability to send emails:
Install sendmail:
apt-get install sendmail
Create a copy of the default file before editing:
cp /etc/mail/sendmail.mc /etc/mail/sendmail.mc.defaults
Configure sendmail:
nano /etc/mail/sendmail.mc
Your last two lines are as follows:
MAILER('local')dnl
MAILER('smtp')dnl
Put this code before those two lines:
define('SMART_HOST','mailserver.contoso.com')dnl
Save and exit
Enable changes:
cd /etc/mail
m4 sendmail.mc > sendmail.cf
make
/etc/init.d/sendmail reload
Test sendmail functionality:
echo "My test email being sent from sendmail" | /usr/sbin/sendmail youremail@contoso.com

NTP Client Setup:
apt-get install ntp
nano /etc/ntp.conf
comment all lines that begin with 'server' by placing a # in front of them
Add the following line before the first 'server' line:
server <NTPServerFQDN>
Restart NTP:
service ntp restart
Test NTP:
ntpq --numeric --peers
In the results, you will see the remote IP of the server you configured.

Fail2Ban setup:
apt-get install fail2ban
cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
nano /etc/fail2ban/jail.local
change destemail = alertEmail@contoso.com
action = %(action_mwl)s
RESTARTING: /etc/init.d/fail2ban restart
LOG FILE AT: /var/log/fail2ban.log

Tuesday, November 24, 2015

Using Powershell to Sift Through My Email

Every morning I have over 100 new emails. Most of these I glance at and archive, because I only need to know that the processes ran. After thinking about how to better optimize this, it occured to me that I wasn't getting the information I needed.

For example, I get over 20 emails from Veeam about Backup Jobs and BackupCopy Jobs. They're successful (if they aren't there's a rule that forwards the offending email to my normal email address), so what's the problem? Well, the problem is that I see that they're successful, grab the whole chunk, and mark as read/archive. Was there 20? Or only 19? Now, in this example I would know if one of the jobs was hung or something because I'm running another script to check for snapshots before the workday, but what about other things like MySQL backups, or Backup Exec jobs (yes, shudder)?

So, here's what I'm doing. These emails are all sent to a reporting mailbox, that forwards any emails with issues to the appropriate personnel. I will use Veeam email as an example.

An email with a subject of "Veeam Job [Success] Daily-Job" comes in. A rule on my reporting mailbox marks it as read and throws it into an "Archive" subfolder.

At 7:15AM, a scheduled task runs on my computer at work, with Outlook open and the reporting mailbox loaded. I'm going to do this script in pieces, explaining each part in between.

#Mailbox Name
$account_address = "reporting"

#Folder in that Mailbox
$mails_folders = "archive"

#Email Variables
$To = "me@contoso.com"
$From = "reporting@contoso.com"
$SMTPServer = "mail.contoso.com"
$Subject = "Reporting Mailbox Summary"

##########################################################################
#          Date/Time Variables                                           #
##########################################################################

#Begin: 5pm yesterday 
$BeginningDateTimeString = (((Get-Date).AddDays(-1)).ToString("yyyy-MM-dd") + " 17:00:00")
[datetime]$BeginningDateTime = $BeginningDateTimeString

#End 7am today
$EndDateTimeString = ((Get-Date).ToString("yyyy-MM-dd")) + " 07:00:00"
[datetime]$EndDateTime = $EndDateTimeString

##########################################################################
#          Stuff with Outlook                                            #
##########################################################################

#Create outlook.application object
$outlook = new-object -com outlook.application
$MailNameSpace = $outlook.GetNameSpace("MAPI")
$MailFolders = $MailNameSpace.Folders |? {$_.Name -eq $account_address}

#Getting main inbox folder
$inbox = $MailFolders.Folders |? {$_.Name -eq "Inbox"}

#Specify folder of mails to calculate
$folder_to_calculate = $inbox.Folders |? {$_.Name -eq "$mails_folders"}

#Get the mail
$Emails = $folder_to_calculate.Items


At this point, $Emails has all of my mail in it (I have autoarchive enabled on the mailbox to delete after 30 days). Now, I'm only interested in a subset of this data. Searching through a months worth of email would take awhile, so I constrain the dataset with the following line, so I only get email received last night after 5pm and before this morning at 7am (see date/time variables above):

$TimePeriodMails = $Emails | Where-Object {$_.ReceivedTime -gt $BeginningDateTime -and $_.ReceivedTime -lt $EndDateTime}

Now I filter what I'm interested in by subject. Here's 4 lines:

$ApplicableMails = $TimePeriodMails | where-object {
    $_.TaskSubject -like 'Backup Exec Alert: Job Success *' -or `
    $_.TaskSubject -like "PS Report - GPO Backup Report" -or `
    $_.TaskSubject -like "PS Report - MySQL Backup Status - SUCCESS - *" -or `
    $_.TaskSubject -like "Veeam Job ``[Success``] Daily-*"}

One interesting tidbit I discovered, through much gnashing of teeth and Googling, is that when you do a string comparison, and the string includes square brackets you have to double-escape them (using the backtick)!

Once I have my emails, I just need to gather a count of the data, which I did like so:

#Create an array to hold the data
$ResultArray = @()

#Look for the Backup Exec Emails and count them
$ArrayItem = New-Object psobject
$ArrayItem | Add-Member -MemberType NoteProperty -Name Name -Value "BE Backup Successful"
$BESuccessMails = $ApplicableMails | where-object {$_.TaskSubject -like 'Backup Exec Alert: Job Success (Server: *'}

#Count those, and convert that number to a string
$BESuccessMailsCount = (($BESuccessMails | measure-Object).count).ToString()

#This next line is for my reference, once I get the process down, I'll put in here how many emails I should see. 
#You'll see why this is important (to me) later.
$BESuccessMailsCountShouldBe = "777"

#Make an array item and add the data I want to the result array
$ArrayItem | Add-Member -MemberType NoteProperty -Name ShouldBe -Value $BESuccessMailsCountShouldBe
$ArrayItem | Add-Member -MemberType NoteProperty -Name Is -Value $BESuccessMailsCount
$ResultArray += $ArrayItem

I'll spare you the other 4 search blocks; they're the same format, just with different names and data to look for.

The last step is to add some formatting, because who doesn't like a nice table to look at? You'll see here that I've included the number of emails I SHOULD see, so that with minimal effort I can deduce that all of my stuff ran.

##########################################################################
#          Format and Send                                                                                                           #
##########################################################################

#HTML Style Formatting
$style = "<style>BODY{font-family: Arial; font-size: 10pt;}"
$style = $style + "TABLE{border: 2px solid black; border-collapse: collapse;}"
$style = $style + "TH{border: 2px solid black; background: #dddddd; padding: 5px; }"
$style = $style + "TD{border: 2px solid black; padding: 5px; }"
$style = $style + "</style>"

#Export the array, with the style, to HTML
$Body = $ResultArray | ConvertTo-Html -Head $style | out-string

Send-Mailmessage -To $To -From $From -SMTPServer $SMTPServer -Subject $Subject -Body $Body -BodyAsHTML

It looks like this , which is much abbreviated, and not using the same fields as above (sorry):


You'll notice here that I haven't received the number I expected, which I've since fixed (this was due to that double-escaping of square brackets!).

Thanks for reading, and Happy Thanksgiving!

Thursday, November 19, 2015

Getting Installed Chrome Extensions Remotely

I was looking into whitelisting Chrome extensions via the Google Chrome Group Policy, and I needed to find out which extensions my employees were using. So, I made the following function to help with that. You need to run the function as an account with local admin group membership, since it will use the administrative shares to find the installed extensions. Also worth noting is that all of those "Apps" icons count as extension from Google perspective. Comments in the code, as usual:

################ BEGIN SCRIPT ################

#Example: Get-ChromeExtensions -Computername $Computername -Username $Username

#Define the function with computername and username
function Get-ChromeExtensions {
[CmdletBinding()]            
 Param             
   (                       
    [Parameter(Mandatory=$true,
               Position=0,                          
               ValueFromPipeline=$true,            
               ValueFromPipelineByPropertyName=$true)]            
    [String[]]$ComputerName,
    [Parameter(Mandatory=$true,
               Position=1,                          
               ValueFromPipeline=$true,            
               ValueFromPipelineByPropertyName=$true)]            
    [String[]]$UserName
   )#End Param

Process
{
    #Get Webpage Title
    #I ripped off this function from https://gallery.technet.microsoft.com/scriptcenter/e76a4213-cd05-4735-bf80-d5903171ae11 -Thanks Mike Pfeiffer!
    Function Get-Title { 
    param([string] $url) 
    $wc = New-Object System.Net.WebClient 
    $data = $wc.downloadstring($url) 
    $title = [regex] '(?<=<title>)([\S\s]*?)(?=</title>)' 
    write-output $title.Match($data).value.trim() 
    } #End Function Get-Title

    #Build the path to the remote Chrome Extension folder
    $GoogleExtensionPath = "\\" + $ComputerName + "\C$\Users\" + $Username + "\AppData\Local\Google\Chrome\User Data\Default\Extensions"
    
    #Check that the computer is reachable
    If ((Test-Connection $Computername -Quiet -Count 1) -eq $False){
        Write-Host -foregroundcolor Red "$ComputerName is not online"
        return
    } #End If

    #Check that the path exists
    If ((Test-Path $GoogleExtensionPath) -eq $False){
        Write-Host -foregroundcolor Red "Path not Found: $GoogleExtensionPath"
        Write-Host -foregroundcolor Red "Chrome is probably not installed OR the username has no profile/is wrong" 
        return
    } #End If

    #Get the foldernames, which are the Google Play Store ID #s
    $ExtensionIDNumbers = Get-Childitem $GoogleExtensionPath *. | select name

    #Build a name for the output file
    $OutputFileName = "C:\Temp\GoogleExtensionList_" + $Computername + "_" + $Username + ".csv"

    #Create an array
    $GoogleExtensionsArray = @()

    #Cycle through each Google ID, and look up the Google Play Store Title, which is the extension name
    Foreach ($GoogleID in $ExtensionIDNumbers){
        $GoogleExtensionsArrayItem = New-Object system.object
        $ExtensionSite = "https://chrome.google.com/webstore/detail/adblock-plus/" + $GoogleID.Name
        $Title = ((Get-Title $ExtensionSite).split("-"))[0]
        $GoogleExtensionsArrayItem | Add-Member -MemberType NoteProperty -Name AppName -Value $Title
        $GoogleExtensionsArrayItem | Add-Member -MemberType NoteProperty -Name ExtensionID -Value ($GoogleID.Name)
        $GoogleExtensionsArray += $GoogleExtensionsArrayItem
    } #End Foreach

    #Export the list of extensions
    $GoogleExtensionsArray | export-csv $OutputFileName -NoTypeInformation
    

}#Process

}#Get-ChromeExtensions

################ END SCRIPT ################

What struck me is that we really need to think of the Chrome browser as almost its own OS. You've removed the ability for your users to install applications on their computer (they don't have local admin privs, right?), but what's to stop them from installing Chrome Extensions. Solitaire and Minesweeper are bad, but Bejeweled is ok?

Monday, October 12, 2015

My Neverending Ping Script

It happened that I needed to remote into every one of our computers to do some work. Of course, some systems were offline, go figure. After working through my computer list and removing the ones I was able to access and fix, I wrote a script to ping the rest of them and email me when they were found to be online.

The way the script works, is that you feed it a list of computers, and it goes through trying to ping them. If the ping is successful, it send me an email, then removes that system from the list. The script keeps running until the amount of systems in the list reaches 0.

########################### BEGIN SCRIPT ###########################

#Variables
$ComputerListPath = "C:\Temp\NeverendingPingList.txt"
$To = "me@contoso.com"
$From = "help@contoso.com"
$Body = "Responding to pings!"
$SMTPServer = "mailserver.contoso.com"

#Get Computer List
$Computers = Get-Content $ComputerListPath

#Count the number of computers
$Count = ($Computers | Measure-Object).count

If ($Count -le 0)
{
Write-Host "No computers to ping, exiting"
exit
}

#Create a "matchlist" - if a computer responds, it is added to this list, then when the script iterates through the foreach loop, it skips the computers that exist in this list
[System.Collections.ArrayList]$Matchlist = @()

#Clear the screen
Clear-host

#For each computer in the list, ping it, while the count is greater than 0
Do
{
foreach ($Computer in $Computers)
{
#If the computer matches a member of the matchlist, skip to the next iteration of the foreach loop
If ($Matchlist -contains $Computer) { Continue }

#Test the connection to the computer, using 1 ping packet
$Result = (Test-Connection -ComputerName $Computer -Count 1 -Quiet)

#If no ping response, write to host
If ($Result -eq $false) { Write-Host "No ping received from $Computer - will pass again" }

#If it responds, email me AND remove it from the list of computers, then recount computers
If ($Result -eq $true)
{
$Subject = "$Computer is on the network!!!"
Send-MailMessage -To $To -From $From -SmtpServer $SMTPServer -Body $Body -Subject $Subject
$Matchlist.Add($Computer)
} #End If
} #End Foreach

Write-Host "`r`n========================================================`r`n========================================================`r`n"

#Sleep for 30 seconds
Start-Sleep -Seconds 30

} while ($Count -gt 0)

########################### END SCRIPT ###########################

Thursday, October 8, 2015

Getting the Windows Install Date from ALL of Your Computers

This script is inspired by a post I ran across on Windows Networking showing how to extract and format the Windows installation date via Powershell. This data would be useful to me for planning hardware refreshes.

On to the script!

################ BEGIN SCRIPT ################

#Purpose: To scan computers for Windows Install Date

#Choose whether this is an initial scan or a rescan
$InitialChoice = Read-Host "Enter 'I' for initial scan, or 'R' for rescan"

#Output file variables
$MissedComputersFile = "C:\Temp\Script - InstallDatesMissed.txt"
$OutputFile = "C:\Temp\Script - InstallDates.csv"

#Get the computer list, depending on initial choice
If ($InitialChoice -like "I"){
    $Computers = get-adcomputer -filter * | select name | sort name
} #End If

If ($InitialChoice -like "R"){
    $Computers = Get-Content $MissedComputersFile
    Remove-Item $MissedComputersFile -Force
} #End If

#Build an empty array for the data
$Results = @()

#Foreach computer in the list
Foreach ($Computer in $Computers){
    
    #Here I need to pick a naming method but it's dependent on where the data came from (the initial choice and subsequent computer name import)
    #For the initial scan, where data comes from AD:
    If ($InitialChoice -like "I"){$TestSystem = ($Computer.Name)}

    #For the rescan, where data comes from the text file:
    If ($InitialChoice -like "R"){$TestSystem = $Computer}
    
    #Test the connection
    If (Test-Connection -ComputerName $TestSystem -count 1 -quiet){
        
        #Create a new object to populate
        $ResultsEntry = New-Object System.Object
        
        #Get the computer's install date from WMI
        $InstallDate = (Get-WmiObject -ComputerName $TestSystem win32_operatingsystem | select @{Name="InstallDate"; Expression={$_.ConvertToDateTime($_.InstallDate)}}).InstallDate
        
        #Reformat the install date
        $InstallDateRefined = ($InstallDate.Year).ToString() + "-" + ($InstallDate.Month).ToString() + "-" + ($InstallDate.Day).ToString()
        
        #Add the name of the computer to the object
        $ResultsEntry | Add-Member -type NoteProperty -name Name -value $TestSystem
        
        #Add the install date to the object
        $ResultsEntry | Add-Member -type NoteProperty -name OSInstallDate -value $InstallDateRefined
        
        #Add the object to the array
        $Results += $ResultsEntry

        #Print a status message to the screen
        Write-Host -ForegroundColor Green "$TestSystem - Windows Install Date is $InstallDateRefined"
    } #End If
    
    #This runs if the test-connection is no good
    Else {
        
        #Adds the name of the system to the "Missed" file
        $TestSystem | Add-Content $MissedComputersFile

        #Print a status message to the screen
        Write-Host -ForegroundColor Red "$TestSystem - Not Online"
    } #End If
} #End Foreach

#Export the array to CSV
$Results | sort name | export-csv $OutputFile -NoTypeInformation


################ END SCRIPT ################

So after the initial script, you just feed it the "Missed" file over and over until you get everything.

Wednesday, October 7, 2015

Exchange Issues with Delegation due to AdminSDHolder and Protected Groups

We had some really weird issues going on delegating permissions to certain users within Active Directory. Basically, when we would assign them some rights, the rights would just disappear for no rhyme nor reason. What we discovered was Protected Groups. I'm not going to try to explain it, because I don't completely understand it (here's someone much more intelligent doing so on Technet), but I'm going to tell you how to fix it!

The jist of it is that you can't delegate stuff to a user account that is a member of a protected group. Complicating matters is that when you remove someone from a protected group, the setting does not change! You have to go into ADSIEdit and change the AdminCount property on the user's AD object from 1 to 0 manually. Well, you could script that too, but I only had this happen with a few users so I didn't bother with that.

WHY we ran across this is that we had removed some user accounts from the Domain Admins group and had issues with delegating in Exchange. I hear it's a problem with Lync, too, but we don't run that. Again, simply removing someone from the protected group (Domain Admins in this example) does not change the setting. It also bears mentioning that nesting counts. So if User A is a member of a group that's a member of Domain Admins, their AdminCount value will change to 1.

The following commands must be performed inside an Active Directory Powershell Session.

To find which groups are protected, use this command:
Get-ADGroup -LDAPFilter "(objectcategory=group)(admincount=1)" | select name | sort name

To find out which users are protected, use this:
Get-ADUser -LDAPFilter "(objectcategory=person)(samaccountname=*)(admincount=1)" | select name

So what you have to do is get the list of protected users, then cross out any users that are direct or indirect members of the protected groups. The users that remain are unjustly protected. To resolve, simply change the AdminCount value on the remaining users.

Here's a handy function I ran across to get nested group memberships (shout out to Piotr Lewandowski for that).

The protected groups by default include:
Account Operators
Administrators
Backup Operators
Cert Publishers
Domain Admins
Domain Controllers
Enterprise Admins
Print Operators
Read-only Domain Controllers
Replicator
Schema Admins
Server Operators


Thursday, October 1, 2015

All OUs in this Domain Should be Protected from Accidental Deletion

I run the best practices analyzer on my domain controllers on the first of the month, every month.

Today I got this result: All OUs in this Domain Should be Protected from Accidental Deletion


So this begets the question: How do I find out which OUs are not protected?

Answer: Of course, make sure you are running this command after doing an import-module activedirectory, from a computer that has the Active Directory Powershell module installed.

The command is:
Get-ADOrganizationalUnit -filter * -Properties * | where {$_.ProtectedFromAccidentalDeletion -eq $False} | select DistinguishedName

There may be a good reason for them not to be protected, but if you want to go ahead and set protection on each OU, you can run this command:
Get-ADOrganizationalUnit -filter * | Set-ADOrganizationalUnit -ProtectedFromAccidentalDeletion $true

Friday, September 25, 2015

Spiceworld 2015

So I'm still in Austin for one more night. I attended Spiceworld 2015 for the first time, and what an amazing conference! I was a little disappointed in a couple of the breakout sessions but that's to be expected. For the price, it was well worth it to network with other admins, get my "business" card out there with my blog's address, and talk to some vendors. We're looking at a possible converged infrastructure project, and possibly a new backup solution (even though I love Veeam, they don't support KVM). I got a lot of questions answered and met a lot of great people, including from my municipal sector. Hopefully we can keep the relationships going and help each other out. It was really cool, and a little bit overwhelming, to get so much swag from the vendors. Here's a picture from day one, and day two was just as much.


Wednesday, September 2, 2015

Windows 10 - Useful Group Policies (Part 2 of ??) - Get Rid of Microsoft Edge Taskbar Pin

I'm crunched for time today, but I thought I would at least throw out something today.

One of my current sticking points in creating a workable Windows 10 machine for my users is getting rid of the Microsoft Edge icon that's pinned to the taskbar.

Yes, Edge looks appealing as a browser. My helpdesk, though, is going to have all kinds of questions and issues related to Edge. I don't have the manpower, so we're getting rid of Edge as much as we can.

The only way I've found to get rid of the pinned Edge icon is to nuke all of the pinned items.

To accomplish this, I use a Group Policy Preferences Registry item under User Configuration.

Create a delete action for HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Taskband, but remember to go to the "Common" tab and check the box that says "Apply once and do not reapply", or else your users' pinned items will be disappearing on them!

Now if I could just figure out how to get rid of the Start Menu pinned item.....

Monday, August 31, 2015

Windows 10 - Useful Group Policies (Part 1 of ??)

So I've got the RTM version of Windows 10 x64 Pro installed in a VM and on a laptop. My first step with an OS is to pin down irritating things and figure out how to get rid of them in an automated fashion. Once I figure out all of that, I can then dip my toe into the MDT/WDS world and get a standard build going.

I put my Windows 10 computers in the same OU as my other computers.

You also need to import the Windows 10 Group Policies into your Central Store. You can read about how to do this on Technet here.

Create a Windows 10 WMI Filter

For your WMI filter, you'll want to use this query:
select * from Win32_OperatingSystem where Version like "10.%"

Now, create a Windows 10 GPO, link that WMI filter to it, and link it to your OU.

With that accomplished, we can now begin setting up the Windows 10 GPO.

First GPO setting: Loopback Processing
I'll split these GPOs up to apply to user/computer OUs later, but right now I want everything together, and I want any user that logs into a Windows 10 box to get the same settings. To do this, I will use loopback processing. This can be a tricky feature, so I always refer to this handy guide when I do it. Basically, you use this when you want a set of "user" group policies to apply to computers in an OU no matter who logs in. Loopback processing is usually used when you have a single-purpose computer that is locked down, like a kiosk, but in this case it's a test machine. You will find the policy in Computer\Policies\Administrative Templates\System\Group Policy, and it's called "Configure Group Policy loopback processing mode. I set that the to enabled and replace (see the article linked above).

Now how about some settings?

Ok, Ok. Here's what I'm using so far:

Computer\Policies\Administrative Templates\System\Logon
Show first sign-in animation, Disabled
Turn off picture password sign-in, Enabled
Turn on PIN sign-in, Disabled

Computer\Policies\Administrative Templates\Windows Components\Data Collection and Preview Builds
Disable pre-release features or settings, Disabled (this one is confusing, pay attention to the description!)
Toggle user control over Insider builds, Disabled

Computer\Policies\Administrative Templates\Windows Components\Delivery Optimization
Download Mode: Enabled (None)
We're still talking about what we're going to do about Windows 10's bittorrent-like ability to propagate downloads. It seems really cool, but we need to do our due diligence and all of that.

Computer\Policies\Administrative Templates\Windows Components\Internet Explorer\Security Features
Allow fallback to SSL 3.0 (Internet Explorer), Enabled (No Sites)
Beware the Poodle!

Computer\Policies\Administrative Templates\Windows Components\Microsoft Edge
Send all intranet traffic over to Internet Explorer, Enabled

Computer\Policies\Administrative Templates\Windows Components\OneDrive
Prevent the usage of OneDrive for file storage, set to HELL YES.

These turn off Cortana, and also disable the "web" part of the start menu search. I only want to search my computer.
Computer\Policies\Administrative Templates\Windows Components\Search
Allow Cortana, Disabled
Do not allow web search, Enabled
Don't search the web or display web results in search, Enabled



Tomorrow, I'll post some Group Policy Preferences, covering some registry entries that turn off some undesired features.

Friday, August 28, 2015

Powershell Script to report all Exchange Public Folder Permissions

One project I'm currently working on is to go through all of our groups and make us a one-resource/one-group shop as far as AD is concerned. This has been a BIG job. Security-enabled distribution groups have been given rights to file shares and added to local server groups, among a ton of other miscellaneous crap.

One step in organizing what each group actually has rights to is for me to go through our Public Folder infrastructure and ferret out who has access to what. We've got over a hundred public folders. I started doing this manually and after about two folders I thought, "This has to be possible with Powershell." Well, guess what?

As usual, make sure c:\temp is present, as that's where I write my files. Also, I wrote this to run from my local (Exchange 2010) management shell.

Further comments are within the script.

######### BEGIN SCRIPT #########

Write-Host -Foreground Red "This script must be run from the Exchange Management Shell!"
$Init = Read-Host "Press Enter to Continue"

#Get all of the Public Folders
$PublicFolders = get-publicfolder -recurse

#Create a new array to hold the data
$Permissions = @()

Foreach ($Folder in $PublicFolders){
#Full path and name of the public folder
[string]$Foldername = ($Folder.ParentPath) + '\' + ($Folder.Name)

#Get the permissions of the public Folder
$FolderPermissions = Get-PublicFolderClientPermission $Folder

Foreach ($Entry in $Folderpermissions){
#If the User identity in NOT Null
If (($Entry.User).ActiveDirectoryIdentity){
#Get the User Identity of the permission
$UserIdentity = (($Entry.User).ActiveDirectoryIdentity).ToString()

#Create a new object to hold the data
$PermissionItem = New-Object System.Object

#Put the Full path and name of the public folder into the object
$PermissionItem | Add-Member -type NoteProperty -name Folder -value $FolderName

#Put the User Identity of the permission into the object
$PermissionItem | Add-Member -type NoteProperty -name User -value ($Entry.User).ExchangeAddressBookDisplayName

#Had to get funky here, and the Accessrights are an array and not readily enumaratable (is that even a word?)
Foreach ($Value in ($Entry.AccessRights)){$Rights = (($Value.Permission).ToString())}

#Add the user's rights to the object
$PermissionItem | Add-Member -type NoteProperty -name Rights -value $Rights

#Add the object into the array
$Permissions += $PermissionItem
} #End If
} #End Foreach $Entry
} #End Foreach $Folder

#Export unique user values to a text document
$Permissions | select user -unique | sort user | out-file "C:\temp\UniquePublicFolderPermissions.txt"

#Export the permissions for all folders to CSV
$Permissions | Export-CSV "C:\temp\AllPublicFolderPemissions.csv" -NoTypeInformation

######### END SCRIPT #########

Wednesday, August 26, 2015

Windows 10 - Crappy Update Release KB Articles

IT Pros are having a lot of problems with the lack of explanation in Windows 10 updates. Microsoft's position on these seems to be "Trust us, you should install this update."

I can't say it any better than this user voice feature request did.

Do MS sysadmins a favor and vote this feature request up!

Tuesday, August 25, 2015

WDS Server not Responding to PXE Requests?

A curious issue befell me yesterday. I was trying to image a PC using our tried and true WDS server. I think WDS is WAY too complicated, and for some reason my eyes start to gloss over when I'm trying to read the documentation. This Windows service really needs an easy button. I realize the complexity lends to WDS's vast customization, but I've got a fairly simple environment; it shouldn't be this complicated.

Anyway, so I press F12 to PXE boot the computer, and it times out. I restart my DHCP service, which is on a different server, double-check that the DHCP options are what they should be, and try again, multiple times. Rebooting the WDS server accomplished zilch. I try a network drop in a different office; nothing. I try a new laptop and it works! Well, so it's not DHCP, and it's not the WDS server. It's not the network. What the crap?

Multiple blog posts and Technet articles regurgitate the same advice, that the DHCP settings are wrong. Nope. THEN I finally found the needle in the haystack, this Technet post.

The first solution outlined was a bit scary: run WDSUTIL /delete-AutoAddDevices /devicetype:approveddevices

In the past, I removed computers listed in the "Active Directory Prestaged Devices" only to find that they had also been removed from Active Directory. Again, I'll admit that I don't really know what I'm doing with this thing. I claim ignorance! So, removing devices with WDSUTIL is scary to me.

The second option, though, turned out to be the magical one that made everything work again!

1. On the WDS server, open Windows Deployment Services and stop the services.
2. Copy all files in \RemoteInstall\Mgmt and paste them to a temp folder
3. Start the WDS service (those files will be recreated)
4. Try your PXE boot.
5. When it works, delete the files you copied to the temp folder

Wednesday, July 8, 2015

Problems Pushing Software via GPO Leads Me to Group Policy 1058 Error 65

After I pushed out this month's Adobe Patch via group policy, I wasn't getting one of my test systems to process the patch. I found the following message in the system log:

Source: Group Policy
Event ID: 1058
The processing of Group Policy failed. Windows attempted to read the file \\contoso.com\SysVol\contoso.com\Policies\{49BA4D4E-A307-40D3-A809-67CE80C5165A}\gpt.ini from a domain controller and was not successful. Group Policy settings may not be applied until this event is resolved. This issue may be transient and could be caused by one or more of the following: 
a) Name Resolution/Network Connectivity to the current domain controller. 
b) File Replication Service Latency (a file created on another domain controller has not replicated to the current domain controller). 
c) The Distributed File System (DFS) client has been disabled.

If you flip over to the details tab, it says ErrorCode 65, and farther down that Network access is denied.

This problem is directly related to the group policy settings that Microsoft recommended to harden group policy, and is outlined in MS15-011 and MS15-014.

Apparently, some enterprising gent had this issue before, opened a case with Microsoft, and posted the case resolution at the bottom of this Technet forum post.

Resolution:- Suggested to edit the GPO for UNC hardening and change value of RequireMutualAuthentication & RequireIntegrity to 0 from 1 (previous value) for path \\*\NETLOGON & \\*\SYSVOL. We have confirmed that this is a known reported problem where we get ErrorDescription Network access is denied. In event id 1058 and group policy processing fails for computers when KB3004361 is applied.

I left NETLOGON alone - all of our login scripts in there appear to be processing normally, but I did change the SYSVOL to 0 and 0. After I performed gpupdate /force and rebooted twice, my software installed successfully and there was no GP 1058 error.

Tuesday, June 30, 2015

So, Which of My Computers is Using Cached Exchange Mode?

I know a lot of scripts that I write about on here can be rendered unnecessary by good use of the technology available to me. Unfortunately, it seems that often there is something in the way (politics, money, manual process, complexity, etc) that makes it a whole lot easier for me to just script out something and send myself a report once in a while.

I don't know about you, but I've had my share of problems with Outlook caused by cached mode being enabled. I know this is controllable by Group Policy, but we have a lot of people that use Outlook Calendars extensively, and they need cached mode on. This is one of those cases where it's easier to run this monthly and keep things tight, than it would be for me to try and scope a group policy to omit people from all over the place, and remember to incorporate new hires that match this profile.

The trick here was to find out how I would know if a client connected and was on cached mode. The best option, it turned out, was to look in the RPC logs of the Exchange server itself.

#########################################################
# BEGIN SCRIPT
#########################################################
# Phase One: Preperation
#########################################################

#Import Active Directory Module
Import-Module activedirectory

#Function to find Hostnames from IP Addresses
Function Get-HostFromIP
{
$IP = $args[0]
$result = $null
$result = [System.Net.Dns]::gethostentry($ip)
If ($Result){
    $DNS = [string]$Result.HostName
}
Else
{
    $DNS = "No HostName Found"
}
$DNS
} #End Function

#Path Variables
$ExchangeLogFolder = "\\mailserver\c$\Program Files\Microsoft\Exchange Server\V14\Logging\RPC Client Access"
$LocalHoldingFolder = "C:\Logs\ExchangeCachedMode"
$OutputFile = "C:\Temp\Cached Mode On - Desktops.csv"

#Email Variables
$SMTPServer = "mail.contoso.com"
$To = "reporting@contoso.com"
$From = "helpdesk@contoso.com"
$Body = "See Attached"

#Remove any output file if it already exists
Remove-item $OutputFile -force -ErrorAction SilentlyContinue

#Delete any pre-existing file in the Local Holding Folder
Get-Childitem -Path $LocalHoldingFolder | remove-item -force -ErrorAction SilentlyContinue

#########################################################
# Phase Two: Copying over the RPC logs from Exchange
#########################################################

#Copy the files
$Files = get-childitem -Path $ExchangeLogFolder | select fullname
Foreach ($File in $Files){
    Copy-Item $File.fullname -Destination $LocalHoldingFolder
}

#Remove the first 5 lines of each LOG file, change the fields row, and output in a consistent CSV format
$Files = get-childitem -Path $LocalHoldingFolder | select fullname
Foreach ($File in $Files){
    $Text = Get-Content $File.Fullname
    $Output = $Text[4..($Text.count)]
    $Output[0] = $Output[0] -replace "`#Fields: ",""
    $Newfile = (($File.Fullname)+"OUT.csv")
    $Newerfile = (($File.Fullname)+"FINAL.csv")
    $Output | %{$_ | Add-content $NewFile}
    $NewFileContent = Import-csv $Newfile
    $NewFileContent | select client-name,client-mode,client-ip | export-csv -NoTypeInformation $Newerfile
} #End Foreach

#Remove the old files that I don't need anymore
Get-childitem $LocalHoldingFolder -Filter "*.LOG" | %{Remove-Item $_.fullname -Force -ErrorAction SilentlyContinue}
Get-childitem $LocalHoldingFolder -Filter "*.LOGOUT.csv" | %{Remove-Item $_.fullname -Force -ErrorAction SilentlyContinue}

#########################################################
# Phase Three; Merging the CSV files
#########################################################

#Get some info
$CSVFilePath = $LocalHoldingFolder

#Get info from the CSV file path
$CSVFiles = get-childitem $CSVFilePath | select fullname, name

#Initialize/Clear the output array
$Output = @()

#Cycle through and add csv content to array
foreach($CSV in $CSVFiles) {          
    if(Test-Path $CSV.fullname) {          
        $FileName = [System.IO.Path]::GetFileName($CSV.FullName)          
        $temp = Import-CSV -Path $CSV.fullname | select *, @{Expression={$FileName};Label="FileName"}          
        $Output += $temp          
    } else {          
        Write-Warning "$CSV.fullname : No such file found"          
    }
} #End Foreach

#Export Array content to specified output file
$Output | Export-Csv -Path ($LocalHoldingFolder + "\temp.csv") -NoTypeInformation

#Remove the old files that I don't need anymore
Get-childitem $LocalHoldingFolder -Filter "*.LOGFINAL.csv" | %{Remove-Item $_.fullname -Force -ErrorAction SilentlyContinue}

#########################################################
# Phase 4: Getting the data together
#########################################################

#Import the data for further refinement
$Content = Import-CSV ($LocalHoldingFolder + "\temp.csv")

#Select only the properties I want
$Refined = $Content | select client-name,client-mode,client-ip

#Run through some filters
$Refined2 = $Refined | where-object {
    #Don't care about entries that list no ip address
$_."client-ip" -ne "" -and
#Here's the interesting bit: Classic means cached exchange mode is NOT on
    $_."client-mode" -ne "Classic" -and
#There are some IP scopes that I can't do anything about
    ($_."client-ip" -like "*192.168.98*" -or
    $_."client-ip" -like "*192.168.99*") -and
#I don't care about Exchange traffic
    $_."client-name" -notlike "*Exchange*"
    }

#Removing duplicate IPs
$Refined2 = $Refined2 | Sort-Object client-ip -Unique

#Initialize a new array
$Refined3 = @()

#Put the same data into the new array, but also include the hostname based on the IP
Foreach ($Item in $Refined2){
    $SubRefined3 = New-Object System.Object
    $ClientHostName = Get-HostFromIP $Item."Client-IP"
    $ClientDescription = (Get-ADComputer ($ClientHostname -replace (".contoso.com","")) -Properties * | select Description).Description
    $SubRefined3 | Add-Member -type NoteProperty -name ClientName -value $Item."Client-Name"
    $SubRefined3 | Add-Member -type NoteProperty -name ClientMode -value $Item."Client-Mode"
    $SubRefined3 | Add-Member -type NoteProperty -name ClientIP -value $Item."Client-IP"
    $SubRefined3 | Add-Member -type NoteProperty -name ClientHostName -value $ClientHostName
    $SubRefined3 | Add-Member -type NoteProperty -name ClientDescription -value $ClientDescription
    $Refined3 += $SubRefined3
} #End Foreach

#Remove any items where a DNS hostname could not be found
$Refined3 = $Refined3 | where-object {$_.ClientHostname -notlike "No Hostname Found"}

#Do AD Lookups to remove any computers that are laptops, based on AD OU. I have no issue with laptops being on cached exchange mode.
$Refined3 = $Refined3 | where-object {((get-adcomputer ($_.ClientHostName -replace (".contoso.com","")) | select DistinguishedName).DistinguishedName) -notlike "*OU=Laptop*"} | sort-object ClientHostName

#Filter out any hostnames that I don't want in the report
$Refined4 = $Refined3 | Where-Object {
    $_.ClientHostName -notlike "def*" -and
    $_.ClientHostName -notlike "ghi*" -and
    $_.ClientHostName -notlike "No Hostname Found"}

#Final Export, excluding computers where Cached Exchange Mode is needed
$Refined4 | Where-Object {$_.ClientHostName -notlike "A123456*" -and
$_.ClientHostName -notlike "B4545875*"} | export-csv $OutputFile -NoTypeInformation

#Get some counts
$Count = (($Refined4 | Measure-Object).count)
$CountString = (($Refined4 | Measure-Object).count).ToString()
$Subject = "PS Report - Clients Using Cached Exchange Mode - $CountString"

#Only send an email if there are more than zero results
If ($Count -gt 0){
    #Send Email
    Send-Mailmessage -To $To -From $From -SMTPServer $SMTPServer -Subject $Subject -Body $Body -Attachments $OutputFile
} #End If

#Remove Temp Files
Remove-Item $OutputFile -Force -Erroraction SilentlyContinue
Remove-Item ($LocalHoldingFolder + "\temp.csv") -Force -Erroraction SilentlyContinue

#########################################################
# END SCRIPT
#########################################################

My Daily Certificate Authority Check

Earlier this year I rolled out my organizations own Public Key Infrastructure. Certificates.

I use the script below to send me an email that includes the following in the subject:
How many days until the next certificate will expire
A list of all issued certificates
How many requests are pending.

Like this subject, for example: PS Report - Issuing CA Info (Next Expiration is 296 days from now, 0 Requests Pending). A list of all issued certificates, with common name, issue date, and the template they are based on is attached as an HTML file.

A prerequisite for this script is the PS PKI Module, which can be found here on Codeplex.

This script runs from my issuing certificate authority server.

######################################################################
# BEGIN SCRIPT
######################################################################

#Import the PS PKI Module
Import-Module PSPKI

#Variables
$TempFile = "C:\Temp\CA_Report.html"
$Today = get-date
$To = "reportingaddress@contoso.com"
$From = "me@contoso.com"
$SMTPServer = "mailserver.contoso.com"

#Get the CA Name
$CAName = (Get-CA | select Computername).Computername

#Get Details on Issued Certs
$Output = Get-CA | Get-IssuedRequest | select RequestID, CommonName, NotAfter, CertificateTemplate | sort Notafter

#Take the above, and exclude CAExchange Certs, Select the first one, and get an integer value on how many days until the earliest renewal is necessary
$RelevantInfo = ($Output | where-Object {$_.CertificateTemplate -notlike "CAExchange"})
$EarliestExpiryInteger = ([math]::abs(($Today - ($RelevantInfo[0].Notafter)).Days)).ToString()

#Write the Relevant Info to a temp file
$RelevantInfo | ConvertTo-HTML | out-file $TempFile

#Get Details on Pending Requests
$Pending = Get-CA | Get-PendingRequest

#Get number of pending requests - If pending requests is null, then PendingCount is left at zero
If ($Pending){$PendingCount = ($Pending | Measure-Object).count}
Else {
$PendingCount = 0
$Pending = "`r`nNone"
} #End Else
$PendingCountStr = $PendingCount.ToString()

#Make the mail body
$Body = "See Attached"

$Subject = "PS Report - Issuing CA Info (Next Expiration is $EarliestExpiryInteger from now, $PendingCountStr Requests Pending)"

Send-mailmessage -To $To -From $From -SmtpServer $SMTPServer -Subject $Subject -Body $Body -Attachments $TempFile

Remove-Item $TempFile -force

######################################################################
# END SCRIPT
######################################################################

Get a List of All Files with a Certain Extension

It happens sometimes that I need to find all files with a certain extension on a given drive. I accomplish this in a more automated way through Microsoft's File Server Resource Manager (FSRM), These cases include reporting on ISO, MP3, and video files that I don't really want cluttering up my file servers. Occasionally however, I just want a list of Access database files, or something. The outlier cases. For that, I have this little script:

#####################################################################
# BEGIN SCRIPT
#####################################################################

#This script prompts for a file extension and a root path, then searches recursively within that path for that extension and sends you a report of all the files.

#Get Hostname
$Hostname = ($env:computername)

#Prompt for file extension to search for
$ext = Read-host "File Extension (do not enter a period)"
$ext = $ext.ToUpper()

#Create Temp File Location
$TempFile = "C:\Temp\FileQuery $ext.csv"

#Get Root Path to search in
$PathToSearch = Read-Host "Path to search (i.e. P:\)"

#Get your email address
$EmailAddress = Read-Host "Your email address (to send the report to)"

#Conduct search, export to CSV (Temp File)
get-childitem -Path $PathToSearch -Filter *.$ext -recurse | select name, Length, DirectoryName | export-csv -NoTypeInformation $TempFile

#Send CSV via email
Send-Mailmessage -to $EmailAddress -from me@contoso.com -smtpserver mailserver.contoso.com -Subject "$Ext Files on $Hostname in $PathToSearch" -Body "See Attached" -attachments $TempFile

#Delete the Temp File
remove-item $Tempfile

#####################################################################
# END SCRIPT
#####################################################################

Getting my Windows Clients Patched

Most of the time my WSUS group policies do the job of making my client systems update when they're supposed to, but other times it just doesn't work out. On top of that, I don't have good reporting on the patch levels of my systems. Yeah I know. It's on my list.

In the meantime, I use the script below to force computers to check for updates, and then open up a separate (awesome, not mine!) Powershell GUI app that I patch them with.

I also use this process to reboot all of my "client" machines if needed. For example, when a version of Flash comes out that fixes a vulnerability being actively exploited in the wild. Ahem. I patch Flash via Group Policy on system startup. For this, I just skip the patching functionality and use PosPAIG to reboot computers.

You can find PoshPAIG at codeplex here. Seriously, if you patch systems, even if you aren't going to use this script, check it out. I use it for my servers too. Saves me SO much time.

The process is as follows:
1. Get all the client computers from AD (with some exceptions).
2. Force them to check for updates.
3. Wait.
4. Feed the list into PoshPAIG.
5. Patch and reboot (or just reboot).

Heeeere we go - I'll talk through the scripts via the built-in documentation.

################################################################################
BEGIN SCRIPT
################################################################################
# Phase One: Getting our Data Together
################################################################################

#The path to PoshPAIG (it's a Powershell GUI that allows you to scan/patch multiple systems)
$PoshPAIGFolder = "C:\PS\PoshPAIG\Start-PoshPAIG.ps1"

#File for storing a list of which systems are initially powered on
$OutputFilePoweredOn = "c:\temp\WsusMegaCheckIn_PoweredOn.txt"

#File for storing a list of which systems weren't powered on, so I can deal with them later.
$OutputFilePoweredOff = "C:\Temp\WSUSMegaCheckIn_PoweredOff.txt"

#Remove any existing outfiles, ignoring errors
Remove-Item $OutputFilePoweredOn -force -erroraction silentlycontinue
Remove-Item $OutputFilePoweredOff -force -erroraction silentlycontinue

#Import the AD Module
import-module activedirectory

#Get the computers from AD, with some exceptions: I don't want anything from the computers container or Servers OU, nor Domain Controllers, NAS devices, or WAPs. 
$PreLimComputers = get-adcomputer -filter * -properties * | where {
$_.distinguishedname -notlike "*CN=Computers*" `
-and $_.distinguishedname -notlike "*OU=Servers*" `
-and $_.distinguishedname -notlike "*OU=Domain Controllers*" `
-and $_.Name -notlike "NAS*" `
-and $_.Name -notlike "WAP*"
}

################################################################################
# Phase Two: Refining our data a bit
################################################################################

#Now, there are a few people who get all bent out of shape if I reboot their computers (like me, and the computer I'm running this script from). Instead of adding them statically to the list above, I've created an AD group that contains their computer accounts. This allows me to use this group as an exception to any rebooting-type script that I write. So, here I will  based on NoPatchReboot Group Membership

#Create a blank array
$Groups = @("NoPatchReboot")

#Create a Regex - I'll be honest that I know what this does, but not how it does it. I just can't wrap my head around regex. BUT.....
$RegEx = '^({0})' -f ($groups -join '|')

#Now I'm going to create a new list of computers, where none of the computers are members of my NoPatchReboot AD Group, helped along by the mysterious, enigmatic regex
$Computers = $PreLimComputers | where-object {($_.MemberOf | Get-AdGroup).Name -notmatch $Regex} | select name | sort name


################################################################################
# Phase 3: Let's do some Work
################################################################################

#Foreach computer, if it is pinged successfully, send it a remote command to check for updates
#Also add it to the "Powered On" text file that you can use in PoshPAIG

Foreach ($computer in $Computers){
    $System = $Computer.Name
    If ((Test-Connection -ComputerName $System -Count 1 -Quiet) -eq $true){
        Invoke-Command -ComputerName $System -ScriptBlock {c:\windows\system32\wuauclt.exe /detectnow}
        Write-host -Foregroundcolor GREEN "$System forced to check for updates"
        $system | add-content $OutputFilePoweredOn
    } #End If
} #End Foreach

#Start PoshPAIG for me
start-process powershell -ArgumentList "-File $PoshPAIGFolder"

Write-Host "Import $OutputFilePoweredOn to patch live systems"

################################################################################
END SCRIPT
################################################################################

At this point, PoshPAIG is open, and I would import the PoweredOn text file, then scan and patch those systems.

I was going to get into my WHOLE script, but I decided that it was a little too specific in a not-applicable-to-most kind of way.

The gist is, though, that while PoshPAIG was doing it's thing, I would initialize the next phase of my script, which uses Wake-On-LAN (WOL) packets to turn machines on. Is the machine in another subnet? But, WOL packets are broadcast and can't be routed! Oh noes! Pffft. Got it covered. I know a server on site that's all to happy to send out WOL packets to machines I target.

I'll put in some code here, because it's generally useful, but this is just code snippets now.....

The first thing we need are MAC addresses for the WOL packets. I have spiceworks pull a report of IP Addresses and MAC Addresses every day and dump it out to a CSV just for this purpose. Once my Domain Controllers get onto Server 2012 I plan (hope) to replace this kludge with the new Powershell DHCP commandlets.
$MACAddressTableFromSpiceworks = Import-CSV "\\spiceworks\rpt\report-174.csv"

Once I get a MAC address, I use this to send a WOL packet (to computers on-site):
$MacByteArray = $MacAddress -split "[:-]" | ForEach-Object { [Byte] "0x$_"} 
[Byte[]] $MagicPacket = (,0xFF * 6) + ($MacByteArray  * 16)
$UdpClient = New-Object System.Net.Sockets.UdpClient
$UdpClient.Connect(([System.Net.IPAddress]::Broadcast),7)
$UdpClient.Send($MagicPacket,$MagicPacket.Length) | out-null
$UdpClient.Close() | Out-Null

What's neat about the above (which I didn't make, btw) is that it doesn't care if you use hyphens or colons as the delimiter, by using -split.

Now, if the system is at another site, based on IP address, I would send a list of systems at that site into a text file there, and then call a WOLScript to send the WOL packets to those computers, like so:

$process=[WMICLASS]"\\$SisterSiteServer\ROOT\CIMV2:win32_process"
$result=$process.Create("powershell.exe -File C:\ps\WOLScript.ps1")
$ProcessID = $result.processID

I want this script to just wait until the script at the sister site is finished running
Only when the process initiated above completes will $Running be $null, and then the while loop will end

$Running = 1
While ($Running -ne $null){
Start-Sleep -seconds 30 #wait 30 seconds
$Running = (get-process -id $ProcessID -ComputerName $SisterSiteServer -ea 0) #Check the process again
} #End While

Aaaaand now I take a break while those computers boot up ad settle down. Stop. Coffeetime.
Write-Host "Sleeping for 5 minutes to allow powered on computers to settle"
Start-Sleep -Seconds 300

And at this point I pretty much start all over, forcing those systems to check for updates
Foreach ($computer in $PoweredOffSystems){
    $System = $Computer
    If ((Test-Connection -ComputerName $System -Count 1 -Quiet) -eq $true){
        Invoke-Command -ComputerName $System -ScriptBlock {c:\windows\system32\wuauclt.exe /detectnow}
        Write-host -Foregroundcolor GREEN "$System forced to check for updates"
    } #End If
    Else {
        Write-host -Foregroundcolor RED "$System is Offline"
    } #End Else
} #End Foreach

Now I take note of any non-responsive systems so that I can investigate them later to find out why WOL didn't work.

Nowadays, that's usually because the system is on the wireless. I briefly considered removing laptops from the initial list from AD, but hey, some of them might be docked, so I'll get them while I can.