Powershell HTML Reports

Powershell HTML Reports

From time to time, you will be asked to create a web report with data that can only be easily obtained by PowerShell. There are hundreds of modules out there that can do this, which is very nice. This blog isn’t about those modules, but instead some tricks I have learned over the years. When it comes to custom HTML reports, these are very customized from data sources like Azure, Active Directory, even SLQlite services. If all possible I like to use the Universal dashboard. But the UD doesn’t translate to emails or IIS pages very well. Let’s get started with the here string.

Here-String

$HereString =@"
Information
"@

A here-string is used to create a block of text. In the example above you see the variable HereString is equal to @””@. This is very important. The @ symbol indicates the start and stops of a here-string. You can place variables inside the here-string as well. This means you can build variables outside of the here-string and plump them into the string. Here is an example of our base for the website.

$HTML = @"
<html>
<head>
<title>Employee List</title>
<style>
</style>
</head>
<body>
</body>
</html>
"@

From this template, we can add CSS coding, powershell tables, lists and much more. The next important item we need to talk about is the convertto-html commandlet.

Convertto-html

This command is the heart of creating tables out of PowerShell information. For example, you want to pull all enabled users from the employees OU and display their displaynames, samaccountnames, department, and title in order. That’s a simple get-aduser command.

$users = Get-ADUser -Filter {enabled -eq $true} `
-SearchBase "OU=hospice Users,DC=Hospice,DC=Local" `
-Properties displayname,samaccountname,title,department `
| Select-Object Displayname,Samaccountname,Title,Department `
| sort-object Displayname

Now we need to convert this array of data into a useable table. This is where convertto-html comes into place. We can tell the command to create a full website just for the tabled information or we can tell it to produce a single table using the as flag and the fragment flag.

$UsersToHTML = $users | ConvertTo-Html -as Table -Fragment

Now we have a user table that we can drop into our Here-string from above. The Here-string is the last thing you will create during this custom process.

$HTML = @"
<html>
<head>
<title>Employee List</title>
<style>
</style>
</head>
<body>
$UsersToHTML
</body>
</html>
"@

This is pretty much the basics of how you would create a site of all users and their user names. You export that site using the >> command. So it would look like this:

$HTML >> <Pathtohtml>.html

Going Deeper – Employee Directory With Images

Let’s go a little deeper by making an employee Directory with pictures. In this scenario, we will assume that the active directory has thumbnail photos inside of it. This Scenario is going to be based on an IIS server and everything is set up for a basic HTML page. I will as

Requirements:

  • If a property is not present in the AD account, null or blank, do not display that property.
  • All users must have a photo or a placeholder.
  • The table must be in alphbetical order by the displayname.
  • Required Properities:
    • Photo
    • Display Name
    • Username
    • Job Title
    • Department
    • Mobile Phone
    • Office Phone
    • Fax
    • Manager

The first thing we need to do is grab the data that we need from the active directory. We do that with the get-aduser command.

$users = Get-ADUser -filter {enabled -eq $true} `
-Properties thumbnailPhoto,Displayname,SamAccountName,Title,Department,MobilePhone,OfficePhone,Fax,Manager

There will be two tables that we will be working with. The first table will be two columns and however many rows we will need. The first column will contain the 128×128 active directory thumbnail photo. The second column will contain the Users information. The second column will contain a table with two columns and 8 rows. The first column will be the name of the value and the second column will be the value itself.

I can do this in two ways. I can start the first table inside the here-string or I can create the table before the here-string. I’m going to create the table before the here-string. This way I have a clear image in my head of the table that I am inputting into the here-string. Notice the UserTable is a here-string as well. So we will refer to the Main HTML here-string as the HTML string from here on out.

$UserTable = @"
<table id="myTable">
  <tr class="header">
    <th style="width:20%;visibility: hidden;"></th>
    <th style="width:30%;visibility: hidden;"></th>
  </tr>
"@

Now we have a basic HTML table built. From here on out, we will be creating the rows for this table. Inside each row will be the two columns. The photo column and the data column. The data column will contain the table with the employee data on it. It’s time to start looping through that user’s object we created a while ago. We start off with a simple foreach loop. Inside the loop the logic will go as follows:

  • Grab the photo from the thumbnail property and save it to the image repo using the username.
  • Start the new row. Inside the first column of the new row, we check to see if that photo exists. If it does, then we we create an img tag of the file, if it doesn’t then we target the general image file.
  • The second column we blank out the values for each item.
  • Then we create the table.
  • Then we create if statements that creates each row in table 2. if the users object has the property in question for that user, we set the value and create the <tr> accordingly.
  • Finally we close off the second table and the row.
  • Then after all of the rows on table 1 is created, we close off the table.

Phew. This is going to be crazy.

The first part is to download the photo in question. If the user’s profile has a thumbnail photo we will download it from AD using the set-content command. (PS5)

foreach ($User in $Users) {
    $PhotoPath = "<path>/images/$($user.samaccountname).jpg"
    if ($Null -ne $user.thumbnailPhoto) {
        $user.thumbnailPhoto | Set-Content $PhotoPath -Encoding byte
    }
}

The next step is to determine if the file exists. If it does, we want to create the Image URL towards the new user’s image. If not, we want it to go to the placeholder image. All of this is still within the Loop in question.

if (Test-Path $PhotoPath) { 
    $ImageURL = "Images/$($user.UserPrincipalName).jpg" 
} else { 
    $ImageURL = "Images/Placeholder.png" 
}

Now we need to make a placeholder for each property that we will be using. We do this so the next time the loop processes, we have fresh variables.

$DisplayName = ""
$Username = ""
$JobTitle = ""
$Department = ""
$MobilePhone = ""
$OfficePhone = ""
$Fax = ""
$Manager = ""

Now we need to test these variables and fill them up with HTML code. Each one of these will be a row on the table2. The code below is the same for each of these. All you need to do is replace the <value> with the value in question. The one on the left is what you will replace. The one on the right is an example.

if ($Null -ne $User.<Value>) {
    $<value>= @"
    <tr>
      <td style='font-size:16px; text-align:justify; width:100px;'>
        <strong><value>:</strong>
      </td>
      <td style='vertical-align: top; text-align:left;'>
        $($User.<value>)
      </td>
    </tr>
"@
  } 
if ($Null -ne $User.OfficePhone) {
    $OfficePhone= @"
    <tr>
      <td style='font-size:16px; text-align:justify; width:100px;'>
        <strong>Office Phone:</strong>
      </td>
      <td style='vertical-align: top; text-align:left;'>
        $($User.OfficePhone)
      </td>
    </tr>
"@
  } 

Once you create a value for each of these items, it’s time to put it together into the User’s table. AKA table2. We start off by using the $usertable from above combined with a here-string. Remember we are now entering the first table again and creating each row. Here is what the code will look like.

  $UserTable = $UserTable + @"
    <tr>
        <td> <center><a href="$ImageURL" target="_blank"><img src='$ImageURL' style='width:125px; height:125px; border-radius:100px;'></a></center></td>
        <td>
            <table>
                $DisplayName
                $UserName
                $JobTitle
                $Department
                $MobilePhone
                $OfficePhone
                $Fax
                $Manager
            </table>
        </td>
    </tr>
"@

We are building this string. This is what you see the $usertable = $usertable + Here-String. All of this is the single row for the table in question. We loop through all of the users using this method and close out the Loop. Once the loop is closed out what we need to do next is close out the table. We do this by adding a final </table> to the end of the table in question.

$UserTable = $UserTable + "</table>"

The final part is placing this table inside an HTML website. It’s as simple as slipping $UserTable between the two bodies.

$HTML = @"
<html>
<head>
<title>Employee List</title>
<style>
</style>
</head>
<body>
$UserTable
</body>
</html>
"@

Now we have an HTML site. We can further edit this and create a nice CSS code with some java-scripts, but I’m not going to get fancy here. The final thing we have to do is export this html page.

$HTML > <pathtosite>\index.html

This process is basically building inside out. We started by the image and then the data. We built accordingly. Now the script itself.

The Script

param (
    $Filepath = (Read-Host "File Path for "),
    $ImagePath = (Read-Host "Image Folder")
)
$users = Get-ADUser -filter {enabled -eq $true} `
-Properties thumbnailPhoto,Displayname,SamAccountName,Title,Department,MobilePhone,OfficePhone,Fax,Manager

$UserTable = @"
<table id="myTable">
  <tr class="header">
    <th style="width:20%;visibility: hidden;"></th>
    <th style="width:30%;visibility: hidden;"></th>
  </tr>
"@
foreach ($User in $Users) {
    $PhotoPath = "$ImagePath/$($user.samaccountname).jpg"
    if ($Null -ne $user.thumbnailPhoto) {
        $user.thumbnailPhoto | Set-Content $PhotoPath -Encoding byte
    }
    if (Test-Path $PhotoPath) { 
        $ImageURL = "Images/$($user.UserPrincipalName).jpg" 
    } else { 
        $ImageURL = "Images/Placeholder.png" 
    }
    $DisplayName = ""
    $Username = ""
    $JobTitle = ""
    $Department = ""
    $MobilePhone = ""
    $OfficePhone = ""
    $Fax = ""
    $Manager = ""
    if ($Null -ne $User.displayname) {
        $DisplayName= @"
    <tr>
        <td style='font-size:16px; text-align:justify; width:100px;'>
        <strong>Display Name:</strong>
        </td>
        <td style='vertical-align: top; text-align:left;'>
        $($User.displayname)
        </td>
    </tr>
"@
    } 
    if ($Null -ne $User.samaccountname) {
        $Username= @"
    <tr>
        <td style='font-size:16px; text-align:justify; width:100px;'>
        <strong>Username:</strong>
        </td>
        <td style='vertical-align: top; text-align:left;'>
        $($User.samaccountname)
        </td>
    </tr>
"@
    } 
    if ($Null -ne $User.title) {
        $JobTitle = @"
    <tr>
        <td style='font-size:16px; text-align:justify; width:100px;'>
        <strong>Job Title:</strong>
        </td>
        <td style='vertical-align: top; text-align:left;'>
        $($User.title)
        </td>
    </tr>
"@
    } 
    if ($Null -ne $User.Department) {
        $Department= @"
    <tr>
        <td style='font-size:16px; text-align:justify; width:100px;'>
        <strong>Department:</strong>
        </td>
        <td style='vertical-align: top; text-align:left;'>
        $($User.Department)
        </td>
    </tr>
"@
    } 
    if ($Null -ne $User.MobilePhone) {
        $MobilePhone = @"
    <tr>
        <td style='font-size:16px; text-align:justify; width:100px;'>
        <strong>Mobile Phone:</strong>
        </td>
        <td style='vertical-align: top; text-align:left;'>
        $($User.MobilePhone)
        </td>
    </tr>
"@
    } 
    if ($Null -ne $User.OfficePhone) {
        $OfficePhone= @"
    <tr>
        <td style='font-size:16px; text-align:justify; width:100px;'>
        <strong>Office Phone:</strong>
        </td>
        <td style='vertical-align: top; text-align:left;'>
        $($User.OfficePhone)
        </td>
    </tr>
"@
    } 
    if ($Null -ne $User.Fax) {
        $Fax = @"
    <tr>
        <td style='font-size:16px; text-align:justify; width:100px;'>
        <strong>Fax:</strong>
        </td>
        <td style='vertical-align: top; text-align:left;'>
        $($User.Fax)
        </td>
    </tr>
"@
    } 
    if ($Null -ne $User.Manager) {
        $ManagerInfo = $User.Manager.split(',')[0] -replace "CN=",''
        $Manager = @"
    <tr>
        <td style='font-size:16px; text-align:justify; width:100px;'>
        <strong>Manager:</strong>
        </td>
        <td style='vertical-align: top; text-align:left;'>
        $($ManagerInfo)
        </td>
    </tr>
"@
    } 
    $UserTable = $UserTable + @"
    <tr>
        <td> <center><a href="$ImageURL" target="_blank"><img src='$ImageURL' style='width:125px; height:125px; border-radius:100px;'></a></center></td>
        <td>
            <table>
                $DisplayName
                $UserName
                $JobTitle
                $Department
                $MobilePhone
                $OfficePhone
                $Fax
                $Manager
            </table>
        </td>
    </tr>
"@
}
$UserTable = $UserTable + "</table>"
$HTML = @"
<html>
<head>
<title>Employee List</title>
<style>
</style>
</head>
<body>
$UserTable
</body>
</html>
"@
$Html > $Filepath

That’s all folk, yall have a great week!

Group Policy Troubleshooting – Stale DNS

Group Policy Troubleshooting – Stale DNS

This one was a fun one that really threw me for a loop. DNS is an issue no matter where you go. Recently facebook showed the world how DNS can take everything down. DNS in your domain is very important to keep alive and healthy. Having items sit in your DNS is deadly to your org. That is why something called DNS Scavenging exists. This story is a story about DNS and how it directly affected group policy.

Scenario – Wrong Server!

A client called and stated that group policy wasn’t applied to a single machine. He said he couldn’t even log into the machine with new accounts, just accounts that were on there from the day before. He went as far as to say that a user that just changed his password had to use his old password. Very interesting combo of items.

Who, What, Where, When, How

  • Who: Anyone using this computer. Users who never signed into the machine, and users who has signed in but changed their passwords recently.
  • What: Login with thier current passwords, Login, Group Policy not applying.
  • Where: This single machine, later on discovered another.
  • When: One week before the issue started. (After DHCP was edited)
  • How: When they log in.

When I first came in, I looked at the machine in question. I ran an IPconfig /all on the machine to get basic information. I marked down the IP, subnet, mac address, DNS servers, and the DHCP server. I then ran the gpresult /r and the command errored out saying the group policy server did not respond. Hum… I pinged the DNS I noted and the ping came back. I ran NSLookup on the dns server’s IP address to get a hostname. The hostname came back as “xxx-bobsmacbook”. Well now, that’s not the DNS server. I asked the client for the DNS server information. He gladly gave it to me. I RDPed into the DNS server. The DNS server was also the DHCP server and the AD server. All the fsmo roles were on this machine. Sigh… Ok, other than that, I deep-dived into DNS because the NSLookup came back as someone’s mac book. Sure enough, there was an entry into DNS from about 2 years before for bobsmacbook at the IP address the machine believed was the DNS server. Infact, every IP address in the subnet was inside there. Most of them were years old.

I looked at the client and asked why their DNS was so full of old records. He replied with, that’s our archive. It took everything in me not to facepalm. I mean, my hand moved instinctively to my face. After explaining the importance of DNS to the client, the client agreed to enable DNS Scavenging. Wouldn’t you know it, after the first rotation, the entire company started to move much quicker. Requests to the IIS server took only seconds instead of minutes. Copying files across the network just generally did better. NSlookup worked. The computer in question group policy was updated correctly. When DNS breaks, everything suffers. In this case, DNS was a young man covered in trash bags.

How to enable DNS Scavenging

DNS Scavenging is an windows feature that finds old stale records and removes them. This ensures environments with DHCP do not detect multiple devices based on bad/multiple DNS entries for the same device. Here are the steps to enable it.

  1. Start > Programs > Administrative tools > DNS > DNS Manager.
  2. Right click the DNS Server
  3. Click set Aging/Scavenging for all zones.
  4. Check box the “Scavenge Stale Resources Records
  5. Select the No-refresh and Refresh intervals totals combined equals to or is less than the DHCP lease. If the lease is 8 days, set the rates at 4 each.
  6. Click Ok.
  7. On the Server Aging/Scavenging Confirmation screen, check box the “Apply these settings to existing active directory intergrated zones.”
    1. Click ok
  8. (Optional) Right click the DNS server and click the “Scavenage State Resource Records” to start the process.

There you have it. The DNS records will be purged when the time comes. This allows DHCP to issue IP addresses with no problems and DNS stays clean.

As always, if you have any questions, feel free to ask.

Enable/Disable/Reset MFA with Powershell

Enable/Disable/Reset MFA with Powershell

How does one enable, disable, and reset a user’s MFA in Office 365? I was surprised by how much is required for enabling MFA.

Enable MFA

The first thing we do is Get the User from the Get-MsolUser.

$user = Get-MsolUser -UserPrincipalName $UPN

Next, we create a Strong Authentication object using the New-Object.

$SAR = New-Object -TypeName Microsoft.Online.Administration.StrongAuthenticationRequirement

Now the object is created, you can review the object by using the Get-Member command. This object has 4 properties and 4 methods. We can now edit the properties. We will edit the RelyingParty and state.

 $sar.RelyingParty = "*"
 $sar.State = "Enabled"

Now we place the edited items into the user’s account.

$sarobject = @($sa)
Set-MsolUser -UserPrincipalName $user.Userprincipalname -StrongAuthenticationRequirements $sarobject

The Script

$user = Get-MsolUser -UserPrincipalName $UPN
$SAR = New-Object -TypeName Microsoft.Online.Administration.StrongAuthenticationRequiremen        $sar.RelyingParty = "*"
$sar.State = "Enabled"
$sarobject = @($sa)
Set-MsolUser -UserPrincipalName $user.Userprincipalname -StrongAuthenticationRequirements $sarobjec

Disable MFA

Disabling MFA is extremely easy compared to enabling it. It’s as simple as putting a blank object inside the strong authentication requirements flag.

Set-MsolUser -UserPrincipalName $user.Userprincipalname -StrongAuthenticationRequirements @()

Reset MFA

The last one is to reset the MFA. Microsoft created a commandlet just for this case. The command is Reset-MsolStrongAuthenticationMethodByUpn.

Reset-MsolStrongAuthenticationMethodByUpn -UserPrincipalName $user.Userprincipalname

I hope this helps out.

Group Policy Troubleshooting – Delegation

Group Policy Troubleshooting – Delegation

A while back, a client called and told me he made a few new group policies, and they were not working as expected. He stated some policies applied to the wrong users, while another didn’t apply at all to any users. He stated he set the security group correctly. When I hear, “The policy didn’t apply, or the policy is applying to the wrong person, I immediately think, delegation. Let’s look at the two policies and what broke them.

Scenario – Restricted Google Chrome Policy applying to everyone.

This client had special needs to restrict google chrome. This included items like the auto-fill feature to be turned off, prohibiting Chrome extensions, and more. This was user policy. He only wanted it to Restrict to the techs computers, and not the management nor the IT department.

Who, What, Where, When, How

  • Who: This was effecting all the users at once.
  • What: A chrome policy that restricted users in google chrome.
  • Where: Everywhere
  • When: After the client applied the google chrome policy.
  • How: Through Group Policy.

It was clear, group policy was the issue. So, I took a gander at the delegation of the policy he called “Restricted Chrome Policy”. The policy

Immediately, I saw that the tech group was not in the policy. In fact, this was the standard policy setup. However, one thing I have learned over the years, never to assume. So, I clicked the authenticated users and clicked the advanced button at the bottom of the delegation screen.

Showing Read is checked
Showing Apply is checked

In this case, I was right in my assumption. The authenticated user are every user on the domain. This is a large group and it’s required for any group policy to work. The thing about it though, if the “Apply group policy” is checked, it applies the group policy to everyone. (Unless another policy closing to the ad object applies or enabled that overwrites the policy in question. )

This is what I did. I unchecked the “Apply Group Policy” check box. Created a security group called SG_Policy_Chrome_Tech and added all the techs inside that group. Then I add that group to the delegation. I made sure the read was checked and apply group policy was checked. Then on an end user’s computer, I ran the gpupdate /force and the policy was applied correctly.

Scenario – Group Policy wasn’t applied at all.

The second policy was harder to track down as it was a password alert policy. (Something I will cover later). The policy was to prompt a user within 14 days that their password is going to expire. It is done with a simple logon script. Very simple script, very simple policy. They called it “Password Prompt”. The client discovered it wasn’t being applied when he completed a gpresult /r on an end-user and didn’t see it. It turns out that it was only meant for users and not service accounts.

Who, What, Where, When, How

  • Who: Users
  • What: Password Prompt is not applying
  • Where: Main OU level
  • When: Always
  • How: Hum…

By default, group policy management opens the policy where you can see the scope. I noticed right there something was missing. The client had set the user’s group “employees” but I didn’t see authenticated users. I went to the delegation tab, and I was right. There was no authenticated users group. I went through and added the authenticated users group, and made sure the “Apply this policy” wasn’t checked, and read was checked.

The reason we do this is we want everyone to read the policy, but not apply it. How can you apply something you know nothing about? The same concept is applied here. The computer account can’t read the policy if it doesn’t know it exists. The authenticated user allows that reading. Once set, and a good old gpupdate /force was applied. The password policy showed applied.

Another way you can go about doing this is by adding the domain computers group. As it is the computer that will need to read the policy. This is thanks to the June 14 2016 security update.

The takeaway

Delegation is important. The Authenticated user’s group is required in all group policies at a minimum of read. If you set the authenticated user’s group to read-only, you use a security group with apply in order to apply a policy. The scope screen only shows you what is being applied, and thus, you may not even notice the authenticated users.

I hope this has been helpful for you. As always, if you have questions reach out to me.

Test Microsoft Service Connections

Test Microsoft Service Connections

This past week I have had multiple terminals up with different clients on different terminals connected to different Microsoft services. I quickly realized that I needed to know if I was already connected or not to each Microsoft service. I knew that Get-PPSession was a key to this, but what surprised me was azure and msols didn’t have a PPSession to review. I use 4 different connections on a daily basis. The Msol, Exchange Online, Compliance, and Azure AD. Here is how you can quickly test each service to see if it’s connected.

Msol Service

With Msol we want to test a command to see if it works. We output that command to null. The command I found to be the quickest for this is Get-MsolDomain. Once we run the command we check to see how successful that command was with $?. I like to put things into arrays for future use. So, I dropped it into a $Results and then present those display those results.

Get-MsolDomain -Erroraction SilentlyContinue | out-null
$Results = $?
$Results

Exchange Online

With Exchange online, we can use the Get-PSSession. We search the connection uri for outlook.office365. We also look at the state to be opened. We then ask if how many sessions there are. if it’s greater than 0, then we are good to go. Believe it or not, this is a one-liner.

(Get-PSSession | Where-object { ($_.ConnectionURI -like "*outlook.office365.com*") -and ($_.State -like "Opened")}).count -gt 0

Compliance

With Compliance, it’s similar to the exchange online. We are using the Get-PSSession again. This time we are looking for the word compliance and the state open as well. Once again, this can be a one-liner.

(Get-PSSession | where-object { ($_.ConnectionUri -like "*compliance*") -and ($_.State -like "Opened") } ).count -gt 0 

Azure AD

Finally, we come to the Azure AD, Just like the Msol, we have to check it using a command. I have seen people use the Get-AzureADTenantDetail commandlet. So we will use that commandlet and pipe it into out-null. Then we confirm if it worked with the $? command.

Get-AzureADTenantDetail -ErrorAction SilentlyContinue | out-null
$Results = $?
$Results

The Function

Let’s combine it together into a function.

function Test-SHDo365ServiceConnection {
    <#
    .SYNOPSIS
        Tests to see if you are connected to verious of services. 
    .DESCRIPTION
        Tests to see if you are connected to Microsoft Online Services, Exhcange Online, Complience Center, and Azure AD.
    .PARAMETER MsolService
        [switch] - Connects to Microsoft Online Services
    .PARAMETER ExchangeOnline
        [switch] - Connects to Exchange Online
    .PARAMETER Complience
        [switch] - Connects to complience services
    .PARAMETER AzureAD
        [switch] - Connects to azure AD
    .EXAMPLE
        PS> Test-SHDo365ServiceConnection -MsolService 
        Gives a trur or False statement on weither connected or not. 
    .OUTPUTS
        [pscustomobject]
    .NOTES
        Author: David Bolding

    .LINK
        https://github.com/rndadhdman/PS_Super_Helpdesk
    #>
    [cmdletbinding()]
    param (
        [switch]$MsolService,
        [switch]$ExchangeOnline,
        [Switch]$Complience,
        [switch]$AzureAD
    )
    if ($MsolService) {
        Get-MsolDomain -Erroraction SilentlyContinue | out-null; $Results = $?
        [pscustomobject]@{
            Service   = "MsolService"
            Connected = $Results
        }
    }
    if ($ExchangeOnline) {
        $Results = (Get-PSSession | Where-object { ($_.ConnectionUri -like "*outlook.office365.com*") -and ($_.State -like "Opened")}).count -gt 0
        [pscustomobject]@{
            Service   = "ExchangeOnline"
            Connected = $Results
        }
    }
    if ($Complience) {
        $Results = (Get-PSSession | where-object { ($_.ConnectionUri -like "*compliance*") -and ($_.State -like "Opened") } ).count -gt 0 
        [pscustomobject]@{
            Service   = "Complience"
            Connected = $Results
        }
    }
    if ($AzureAD) {
        Get-AzureADTenantDetail -ErrorAction SilentlyContinue | out-null; $Results = $?
        [pscustomobject]@{
            Service   = "AzureAD"
            Connected = $Results
        }
    }
} #Review
Group Policy Troubleshooting – Internet Explorer

Group Policy Troubleshooting – Internet Explorer

Any IT professional will tell you, technology changes almost daily. Some things seem like they will never change then bam, it changes. For example the dependency on IE. In the 90s and most of the 20s it was IE. Firefox and Chrome started back then but didn’t really gain traction. Now here it is 2021, Google search no longer supports IE. Microsoft no longer supports IE. IE is a bad word in the security world. So it’s time to move away from IE. Most companies have done just that. Thousands of security-minded websites no longer develop for IE and purposely broken their sites on IE. The immortal IE was a problem this week.

Scenario – Always IE

A user called in and stated “Every time I go to ADP I get a message saying ‘Your browser isn’t supported.'” The browser was IE. ADP was a URL link on the desktop. URL links go to the default browser. The user stated that they changed the default browser to Google Chrome more than once, but it changes back after they restart the computer.

Who, What, Where, When, How

  • Who: The user was a standard user and located in the User OU in AD.
  • What: URL link is opening IE instead of google chrome.
  • Where: End user’s laptop.
  • When: Every time they reboot.
  • How: When they double click a url icon.

With this info gathered, I started troubleshooting. I changed the default browser to Google chrome. The user stated after a reboot it changes back. I rebooted the machine to see this behavior. Sure enough, the default app changed from Google Chrome to IE. The most common thing that changes the default apps around is Group Policy. I ran gpresult /r on the computer and saw a default app policy.

Hum…

I logged into the server and loaded the group policy. Sure enough, there was a default app policy. This policy lived at the top level of the domain as well. Default app policy lives under Computer > Policies > Administrative Templates > Windows Components > File Explorer > Default Associations Configuration File. They use an XML file on a share that can be read by everyone in the company. I looked into this file and saw .htm, .HTML, HTTP, and https were set to internet explorer.

At this point, I knew this was going to be a change request. I informed the client that this is a change request. Then I contacted the client’s leadership and acquired permission from the leadership via email. They responded with, YES PLEASE! This is my CYA that I needed. My assets were covered. Time to kick it into high gear.

Inside the default app XML file, I changed the .htm, .HTML, HTTP, and HTTPS progId to ChromeHTML and the ApplicationName to Google Chrome. This is what it looked like.

  <Association Identifier=".htm" ProgId="ChromeHTML" ApplicationName="Google Chrome" />
  <Association Identifier=".html" ProgId="ChromeHTML" ApplicationName="Google Chrome" />
  <Association Identifier="http" ProgId="ChromeHTML" ApplicationName="Google Chrome" />
  <Association Identifier="https" ProgId="ChromeHTML" ApplicationName="Google Chrome" />

I asked the user to reboot their computer and test if the URLs opened in google chrome. The user reported that it did. I called few other users in the org and tested with them. Normally I would create a new OU and test with a test box, but this client did not have a test box. So, why not test in production! Please don’t test in production unless you have to.

In the end, the problem was a Default App Policy. Fixing that fixed more than just this single user’s issue. The leadership was happy with the change and they enjoyed using their drag and drop URL icons.

As always, if you have questions, feel free to ask.