So.. on my recent trip to Florida. Our Support team has a single login to make changes to settings for a server from a web page. So when the change is made, it changes a certain configuration file on the machine. But since they all use one login, they didn’t know who made the change. So I explored some options, and found that with PowerShell Studios you can compile a service and register it. It is very simple also. I am sure I can add more to my service to clean it up a little. I nulled all the variables at the end and found it saved me 2 MB of memory usage.. lol
So the service below detects changes to the file. Then does a netstat on port 8010 to get the users connected. (users have to use that port to connect to the webadmin) Does a NSLookup on what it finds, logs their IP, and creates a backup of the rule. Also, I added something to only keep 90 days of rules because these files are quite large on our side. I kept the write host in the code to help with commenting on the script. The code below uses .net FileWatcher service, and is pretty powerful. The file Watcher Service can watch for changes, deletions, and new.
To install the service you would just compile the Service into a EXE then just use command prompt or powershell to .\WatcherService.exe /i register the service and then start it. You can use the /u argument to uninstall the service.
# Warning: Do not rename Start-MyService, Invoke-MyService and Stop-MyService functions
function Start-MyService
{
# Place one time startup code here.
# Initialize global variables and open connections if needed
$global:bRunService = $true
$global:bServiceRunning = $false
$global:bServicePaused = $false
}
function Invoke-MyService
{
$global:bServiceRunning = $true
while ($global:bRunService)
{
try
{
if ($global:bServicePaused -eq $false) #Only act if service is not paused
{
#Checks for a Job, if a job is still open it just skip
$JobCount = get-job
If ($JobCount -eq $Null)
{
$server = $env:computername
$folder = '\\' + $Server + '\d$\Admin\Settings' # Enter the root path you want to monitor.
$filter = 'Rules.*' # You can enter a wildcard filter here. You can use Extension also
$fsw = New-Object IO.FileSystemWatcher $folder, $filter -Property @{ IncludeSubdirectories = $false; NotifyFilter = [IO.NotifyFilters]'FileName, LastWrite' }
Register-ObjectEvent $fsw Changed -SourceIdentifier FileChanged -Action {
$name = $Event.SourceEventArgs.Name
$changeType = $Event.SourceEventArgs.ChangeType
$timeStamp = $Event.TimeGenerated
Write-Host "The file '$name' was $changeType at $timeStamp" -fore white
$GetIP = Get-NetTCPConnection -LocalPort 8010 | ?{ $_.LocalAddress -ne "127.0.0.1" -and $_.LocalAddress -ne "0.0.0.0" } | select-object Remoteaddress -Unique
Foreach ($IP in $GetIP)
{
$IPAddress = $IP.RemoteAddress
$NameofUser = (Resolve-DnsName $IPAddress).NameHost
#this might be different than your enviornment, in ours we cannot nslookup a VPN adddress.
If ($IPaddress -like "192*")
{
Write-Host "A VPN User most likely Changed the rules files With IP of $IPAddress"
Out-File -FilePath 'D:\WatcherService\Logs\Changelog.txt' -Append -InputObject "$timeStamp A VPN User most likely Changed the rules files With IP of $IPAddress"
}
Else
{
Write-Host "$NameofUser most likely Changed the rules files With IP of $IPAddress"
If ($NameofUser -eq $null)
{
Out-File -FilePath 'D:\WatcherService\Logs\Changelog.txt' -Append -InputObject "$timeStamp Could not find User, but their IP is $IPAddress and RuleFile Was Changed"
}
Else
{
Out-File -FilePath 'D:\WatcherService\Logs\Changelog.txt' -Append -InputObject "$timeStamp $NameofUser most likely Changed the rules files With IP of $IPAddress"
}
}
}
$TimeforFile = get-date -Format M.dd.y.H.mm.s
$FilePath = "D:\WatcherService\RulesBackup\" + $TimeforFile + ".txt"
get-content D:\Admin\Settings\rules.settings | Add-Content $FilePath
sleep 10
$GetIP = $Null
$TimeforFile = $Null
$FilePath = $Null
$changeType = $Null
$timeStamp = $Null
$name = $Null
}
$today = Get-Date
$90Days = $today.adddays(-90)
#Gather all files other than 90 days and deletes them.
$RulesLogFile = Get-ChildItem "D:\WatcherService\RulesBackup" -Include *.filter -ErrorAction SilentlyContinue | where { $_.lastwritetime -le $90Days -and $_.psIsContainer -eq $false }
if ($RulesLogFile -ne $null)
{
foreach ($Rule in $RulesLogFile)
{
[string]$Rulefullfilename = $Rule.fullname
Remove-Item $Rulefullfilename -ErrorAction SilentlyContinue | out-null
}
}
$RulesLogFile = $Null
$today = $Null
$90Days = $Null
$server = $Null
$folder = $Null
$filter = $Null
$fsw = $Null
}
}
}
catch
{
# Log exception in application log
Write-Host $_.Exception.Message
}
# Adjust sleep timing to determine how often your service becomes active
if ($global:bServicePaused -eq $true)
{
Start-Sleep -Seconds 45 # if the service is paused we sleep longer between checks
}
else
{
Start-Sleep -Seconds 30 # a lower number will make your service active more often and use more CPU cycles
}
}
$global:bServiceRunning = $false
}
function Stop-MyService
{
$global:bRunService = $false # Signal main loop to exit
$CountDown = 30 # Maximum wait for loop to exit
while ($global:bServiceRunning -and $Countdown -gt 0)
{
Start-Sleep -Seconds 1 # wait for your main loop to exit
$Countdown = $Countdown - 1
}
Get-Job | Stop-Job
Unregister-Event FileChanged
}
function Pause-MyService
{
# Service is being paused
# Save state
$global:bServicePaused = $true
# Note that the thread your PowerShell script is running on is not suspended on 'pause'.
# It is your responsibility in the service loop to pause processing until a 'continue' command is issued.
# It is recommended to sleep for longer periods between loop iterations when the service is paused.
# in order to prevent excessive CPU usage by simply waiting and looping.
}
function Continue-MyService
{
# Service is being continued from a paused state
# Restore any saved states if needed
$global:bServicePaused = $false
}
Our company was using a Sharepoint Calendar for all of the on call stuff. Which was very hard to manage especially if we got a new employee, swap on call weeks, etc. So I came up with an idea to create a webpage as well as sending an email every Tuesday (Our swap day) to notify everyone who is on call. It also provided the accurate dates on when you would be on call.
Above is the sample HTML file created by my script. I created a scheduled task to run this script nightly to keep the remote days updated. It also will rotate the user when Tuesday comes around to the bottom. You can change it to what ever you need. To run this, just create a CSV with the headers “Name”,”Email”,”Cell”,”Area” in the folder you are running the Powershell Script. If you have any problems just let me know!
[int]$DateCounter = 0
[int]$StartDateCounter = 0
[int]$CycleCount = 0
[String]$DayofWeek = (get-date).DayOfWeek
[Switch]$SendMail = $False
[String]$Runtime = get-date -Format g
Set-Location $PSScriptRoot
If ($DayOfWeek -ne "Tuesday")
{
$DoNotRotate = $True
}
$Website = @()
$NewList = @()
$Website += "<html>"
$Website += "<head>"
$Website += " <title>IT Engineering Schedule</title>"
$Website += "<style>"
$Website += "body {"
$Website += " color: #666;"
$Website += ' font-family: /*"Open Sans", */Arial, Helvetica, sans-serif;'
$Website += " line-height: 1.5;"
$Website += " font-size: 16px;"
$Website += "}"
$Website += ".container {"
$Website += " max-width: 800px;"
$Website += " width: 80%;"
$Website += " margin: 50px auto 0;"
$Website += "}"
$Website += ""
$Website += "a {"
$Website += " color: #0053a0;"
$Website += " text-decoration: none;"
$Website += ""
$Website += "}"
$Website += ".notice {"
$Website += " display: block;"
$Website += " font-weight: bold;"
$Website += " color: rgb(177, 39, 39);"
$Website += " border: 1px solid #ccc;"
$Website += " border-radius: 2px;"
$Website += " padding: .5em 5%;"
$Website += " background-color: #f1f1f1;"
$Website += " text-align: center;"
$Website += " font-size: 1.2em;"
$Website += "}"
$Website += ".calendar {"
$Website += " display:block;"
$Website += " text-align: left;"
$Website += " font-size: 1.5em;"
$Website += " font-weight: bold;"
$Website += " color: black;"
$Website += " background-color: #FFFFFFF;"
$Website += " -webkit-transition: background-color .1s ease-in-out;"
$Website += " -moz-transition: background-color .1s ease-in-out;"
$Website += " -o-transition: background-color .1s ease-in-out;"
$Website += " transition: background-color .1s ease-in-out;"
$Website += "}"
$Website += ".calendar:hover {"
$Website += " color: white;"
$Website += " background-color: #6F94B6;"
$Website += " cursor: pointer;"
$Website += " -webkit-transition: background-color .1s ease-in-out;"
$Website += " -moz-transition: background-color .1s ease-in-out;"
$Website += " -o-transition: background-color .1s ease-in-out;"
$Website += " transition: background-color .1s ease-in-out;"
$Website += "}"
$Website += ".calendar:active {"
$Website += " background-color: #A7BED2;"
$Website += " color: white;"
$Website += "}"
$Website += ""
$Website += "p, h3 span {"
$Website += " color: #989898;"
$Website += " font-size: 80%;"
$Website += "}"
$Website += ""
$Website += "@media all and (max-width: 600px) and (min-width: 0px) {"
$Website += " body {"
$Website += " font-size: 14px;"
$Website += " }"
$Website += " .container {"
$Website += " margin: 0 auto;"
$Website += " width: 95%;"
$Website += " }"
$Website += " .notice {"
$Website += " padding: .5em .5em;"
$Website += " }"
$Website += " .calendar {"
$Website += " font-size: 1em;"
$Website += " padding: 2%;"
$Website += " }"
$Website += "}"
$Website += ""
$Website += "li { background:white; }"
$Website += "li.odd { background: #f1f1f1; }"
$Website += "table {"
$Website += " width:100%;"
$Website += "}"
$Website += "table, th, td {"
$Website += " border-collapse: collapse;"
$Website += "}"
$Website += "th, td {"
$Website += " padding: 1px;"
$Website += " text-align: left;"
$Website += "}"
$Website += "table#t01 tr:nth-child(even)"
$Website += "{"
$Website += " background-color: #eee;"
$Website += "}"
$Website += "table#t01 tr:nth-child(odd)"
$Website += "{"
$Website += " background-color: #fff;"
$Website += "}"
$Website += "table#t01 th {"
$Website += ' background-color: #989898;'
$Website += " color: white;"
$Website += "}"
$Website += "</style>"
$Website += "</head>"
$Website += "<body>"
$Website += '<div class="container">'
$Website += '<p class="notice">IT Engineering Schedule. If you are Highlighted you are On Call :)</p>'
$Website += ""
$Website += "<h3>On Call List</h3>"
$Website += "<table>"
If ($DonotRotate)
{
$NewList = Import-Csv ".\OnCallList.csv"
}
Else
{
#Check if Already Rotated this Tuesday
[string]$TuesdayDate = get-date -Format MM.dd.y
$TuesdayCheck = Get-Content ".\rotatecheck.txt"
$CheckTuesday = $TuesdayCheck | Select-String $tuesdaydate
If ($CheckTuesday)
{
$NewList = Import-Csv ".\OnCallList.csv"
}
Else
{
#Append Date to File
Out-File -FilePath ".\rotatecheck.txt" -Append -InputObject $TuesdayDate
#Gather List of users
$ListofUsers = Import-Csv ".\OnCallList.csv"
foreach ($User in $ListofUsers)
{
#Move Top of the list to bottom
If ($User -eq $ListofUsers[0])
{
#Skip the user
}
Else
{
$NewList += $User
}
}
#Add the First user to bottom
$NewList += $ListofUsers[0]
$NewList | Export-Csv -path ".\OnCallList.csv" -NoTypeInformation -Force
$SendMail = $True
$OnCallUser = $NewList[0].Name
$Subject = "$OnCallUser is on call this week"
$OnCallEmail = $NewList[0].Email
}
}
Foreach ($OnCall in $NewList)
{
$Website += "<tr>"
if ($DonotRotate -and $CycleCount -eq 0)
{
$i = 0
do { $LastTuesday = (Get-Date).AddDays(--$i).Date }
until ($LastTuesday.DayofWeek -eq 'Tuesday')
$StartDate = $LastTuesday.ToString('MM-dd-yyyy')
$Plus6Days = $LastTuesday.AddDays(6).ToString('MM-dd-yyyy')
$Website += '<td> <font color="Red">' + "$($OnCall.Name)" + "</td>"
$Website += '<td> <font color="Red">' + "$($OnCall.Cell)" + "</td>"
$Website += '<td> <font color="Red">' + "$($OnCall.Area)" + "</td>"
$Website += '<td> <font color="Red">' + "$StartDate - $Plus6Days" + "</td></font>"
[int]$Math = 7 + $i
$StartDateCounter = $StartDateCounter + $Math
}
Else
{
$DateCounter = $StartDateCounter + 6
$StartDate = (Get-Date).AddDays($StartDateCounter).ToString('MM-dd-yyyy')
$Plus6Days = (Get-Date).AddDays($DateCounter).ToString('MM-dd-yyyy')
If ($CycleCount -eq 0)
{
$Website += '<td> <font color="Red">' + "$($OnCall.Name)" + "</td>"
$Website += '<td> <font color="Red">' + "$($OnCall.Cell)" + "</td>"
$Website += '<td> <font color="Red">' + "$($OnCall.Area)" + "</td>"
$Website += '<td> <font color="Red">' + "$StartDate - $Plus6Days" + "</td></font>"
}
Else
{
$Website += "<td> $($OnCall.Name) </td>"
$Website += "<td> $($OnCall.Cell) </td>"
$Website += "<td> $($OnCall.Area) </td>"
$Website += "<td> $StartDate - $Plus6Days </td>"
}
$StartDateCounter = $StartDateCounter + 7
}
$Website += "</tr>"
$CycleCount++
}
$Website += ""
$Website += " </ul>"
$Website += " </table>"
$Website2 = $Website
$Website2 += ""
$Website2 += "<h3>Who's Remote Today?</h3>"
$Website2 += " <ul>"
$Website2 += ""
Switch ($DayofWeek)
{
"Tuesday"
{
$Website2 += "<li>Joe Shmoe</li>"
$Website2 += "<li>Jon Smith</li>"
$Website2 += "<li>Michael Jackson</li>"
}
"Wednesday"
{
$Website2 += "<li>R Kelly</li>"
$Website2 += "<li>Alejandro Perez</li>"
$Website2 += "<li>Anthony P</li>"
$Website2 += "<li>Lewis Clarke</li>"
$Website2 += "<li>Larry Bird</li>"
$Website2 += "<li>Michael Jordan</li"
}
"Thursday"
{
$Website2 += "<li>Ken Streetfighter</li>"
$Website2 += "<li>Cool Guy</li>"
$Website2 += "<li>Funny Alvarez</li>"
}
"Friday"
{
$Website2 += "<li>R Kelly</li>"
$Website2 += "<li>Alejandro Perez</li>"
$Website2 += "<li>Anthony P</li>"
}
default { $Website2 += "<li> None </li>" }
}
$Website2 += ""
$Website2 += " </ul>"
$Website2 += "<p><span>Note: The List rotates every Tuesday Morning. If you need to swap with anyone ask a person close to your rotation.<br>"
$Website2 += "An Email is Generated every Tuesday Morning. Script was Last Ran at $RunTime </span></p>"
$Website2 += ""
$Website2 += ""
$Website2 += "<br>"
$Website2 += "</body>"
$Website2 += "</html>"
$Website += "<p><span>Note: The List rotates every Tuesday Morning. If you need to swap with anyone ask a person close to your rotation.<br>"
$Website += "An Email is Generated every Tuesday Morning. Script was Last Ran at $RunTime </span></p>"
$Website += ""
$Website += ""
$Website += "<br>"
$Website += "</body>"
$Website += "</html>"
$website2 | Out-File ".\index.html" -Force
if ($SendMail)
{
$Body = $website | Out-String
Send-MailMessage -From ITEngineeringoncall@contoso.com -To "itengineering@contoso.com" -BodyAsHtml -Body $Body -SmtpServer smtp.wellstar.org -Subject $Subject
}
So, when I started my 2nd Job one of the main tasks was to migrate all the users from Exchange 2010 to the O365. Once all of that was completed, I planned on removing Exchange, however I found that it is recommended to keep at least 1 Exchange server to run Exchange commands to if you plan to keep your AD on Prem. If you live on the wild side. You can edit AD attributes manually / script to create users, and have DIRSync/ADConnect pick it up. But that would be a hassle, instead of just doing a New-RemoteMailbox command. So I built an Exchange 2016 Box using the Hybrid License, and decommed the others. At the time I was on CU7. The problem I was having is that the New-RemoteMailbox command did not have an option to create shared mailboxes directly to the cloud. So I had to create a normal mailbox and convert it or create the SharedMailbox on Prem and migrate it. However, I found out in the later CUs they added a parameter to allow Creation of Shared Mailbox directly to the cloud.
Note: This switch is available only in Exchange 2013 CU21 or later and Exchange 2016 CU10 or later. To use this switch, you also need to run setup.exe /PrepareAD. For more information, see KB4133605.
This is a Function I created for our O365 Admins to quickly create a mailbox, and also a group to assign the users that need Full/SendAS access to the mailbox. I might have some other proprietary code such as removing the internal domain, but you can comment that stuff out if needed, but it shouldn’t hurt if it stayed in there.
Function New-SharedMailbox
{
Param
(
[Parameter(Mandatory = $true)]
[String]$Name,
[Parameter(Mandatory = $true)]
[String]$DisplayName,
[Parameter(Mandatory = $true)]
[String]$OwnerEmail
)
$EmailAddress = $Name + '@Tenant.org'
$MBCheck = Get-Recipient $EmailAddress -ErrorAction SilentlyContinue
$Replace = '@tenant.int'
If ($MBCheck)
{
Write-Host "EmailAddress Is already in Use Please use a different Name"
}
Else
{
$RoutingAddress = $EmailAddress -replace ("Tenant.org", "Tenant.mail.onmicrosoft.com")
Do
{
$RoutingAddressCheck = Get-Recipient $RoutingAddress -ErrorAction SilentlyContinue
if ($RoutingAddressCheck)
{
[String]$RandomNumber = (10 .. 100) | Get-Random
[String]$RoutingAddress = $Name + $RandomNumber + '@Tenant.mail.onmicrosoft.com'
}
$RoutingAddressCheck = Get-Recipient $RoutingAddress -ErrorAction SilentlyContinue
}
Until($RoutingAddressCheck -eq $Null)
Write-Host "Enabling $Name to Cloud"
New-RemoteMailbox -Alias $Name -OnPremisesOrganizationalUnit "OU=Email,OU=Accounts,DC=tenant,DC=int" -shared -RemoteRoutingAddress $RoutingAddress -userprincipalname $EmailAddress -PrimarySmtpAddress $EmailAddress -DisplayName $DisplayName -name $Name
Write-Host "waiting on AD Replication 30 Seconds"
Sleep 30
$ProxyAddresses = (Get-ADUser $Name -Properties ProxyAddresses).ProxyAddresses
Foreach ($ProxyAddress in $ProxyAddresses)
{
If ($ProxyAddress -like "*tenant.int*")
{
Write-Host "Removing $Replace from $Name"
Set-ADUser $Name -Remove @{ Proxyaddresses = $ProxyAddress }
}
}
$GroupName = $Name + '-OW'
$GroupEmail = $GroupName + '@Tenant.org'
Write-Host "Creating Group $GroupName"
New-DistributionGroup -Name $GroupName -PrimarySmtpAddress $GroupEmail -ManagedBy $OwnerEmail -RequireSenderAuthenticationEnabled:$True -CopyOwnerToMember:$True -OrganizationalUnit "OU=USG,OU=Email,OU=Accounts,DC=tenant,DC=int" -Type Security
Write-Host "Hiding From Address List"
Sleep 15
Set-DistributionGroup $GroupName -HiddenFromAddressListsEnabled:$True
Write-Host "All Done.. Make sure to add the group with full / sendas on O365 after 30 min Sync" -ForegroundColor Green
}
}
At my healthcare organization a nursing leader needed to push alerts via email, to users Cell Phones. She had a list of phone numbers and names of the users (around 100). So when she sent me the list I asked if she could make another column with the providers next to them so I can automate it. Below is a script I created to create the Contacts. I also added comments on everything, to help a novice powershell user to better understand how everything works. Remember to Change the OU Location at the bottom of the script, Could probably make a new Parameter for that. Also, to run the command you would just copy and paste the function in Exchange Powershell then Run New-CellContact -FirstName John -LastName Doe -Provider Cricket -CellNumber 8774878777
Function New-CellContact
{
Param (
[parameter(Mandatory = $true)]
#This means if you run New-CellContact, It will need this value to Continue
[String]$FirstName,
[parameter(Mandatory = $true)]
[String]$LastName,
[parameter(Mandatory = $true)]
[ValidateSet("ATT", "Sprint", "Cricket", "Tmobile", "Verizon", "StraightTalk", "MetroPCS")]
#Validating Set means, when providing the cmdlet the data you will need to give it the one listed or it will fail
[String]$Provider,
[parameter(Mandatory = $true)]
[ValidatePattern("\d{10}")]
#Validating Pattern Uses RegEx This RegEx is saying there needs to be 10 Digits \d is Numbers anything more or less or with a letter will fail.
[String]$CellNumber
)
#This switch is a faster IF statement Basically if $Provider = ATT then $CarrierEmail = @mms.att.net
Switch ($Provider)
{
"ATT" { $CarrierEmail = "@mms.att.net" }
"Sprint" { $CarrierEmail = "@messaging.sprintpcs.com" }
"Cricket" { $CarrierEmail = "@mms.mycricket.com" }
"Tmobile" { $CarrierEmail = "@tmomail.net" }
"Verizon" { $CarrierEmail = "@vtext.com" }
"StraightTalk" { $CarrierEmail = "@vtext.com" }
"MetroPCS" { $CarrierEmail = "@mymetropcs.com" }
}
#This builds out the Alias for the AD Object
$Alias = 'Cell-Alerts-' + $FirstName + "-" + $LastName
#This builds out the email address so 7708278377@vtext.com
$ForwardingEmail = $CellNumber + $CarrierEmail
#Write-Host "Testing $Alias $ForwardingEmail" this is for testing
#This is the command to actually build the object using the parameters given and throw it in the correct OU
New-MailContact -Alias $Alias -DisplayName $Alias -Name $Alias -FirstName $FirstName -LastName $LastName -ExternalEmailAddress $ForwardingEmail -OrganizationalUnit "Contoso.int/Contacts"
}
Thanks for joining me! I am new to this so sorry if it sucks.
Good company in a journey makes the way seem shorter. — Izaak Walton
