Microsoft Graph API – Powershell, Download user Images

Microsoft Graph API – Powershell, Download user Images

In my previous post, we went over how to Grab user information from a client. Today we will be going over how to Download User Images with Graph API. This piece is very straightforward until you get to the graph link. There is a unique limitation to PowerShell quotes that I found a good workaround.

Ok, we start off with the loop like before. We are using the /Users API. Since this is a user-level item, you have a top loop through each user with the User Principal Name. This means your string will be inside double quotes “” instead of single quotes because you want PowerShell to read the value of the $($UPN). so far simple. The next part is the word photo. Once again, simple. Then the impact. the word $value has to be at the end. This means it’s going to drop whatever is instead value into the string. There are a few ways around this.

Option 1

Declare the variable beforehand. Simple and easy way to fix this problem.

$Value = '$value'
$UserPhotoLink = "https://graph.microsoft.com/v1.0/users/$($UPN)/photo/$value"

Options 2

Use the + symbol to add the string.

$UserPhotoLink = "https://graph.microsoft.com/v1.0/users/$($UPN)/photo/" + '$value'

Download User Images with Graph API

Now we have the link we need to download the image. Once again we are going to use the invoke-restmethod with our custom header like before. This time we are going to give the -outfile. Since not everyone has an image, I am also going to set the error action to silently continue.

Invoke-RestMethod -Method get -Headers $Header -Uri $UserPhotoLink -ErrorAction SilentlyContinue -OutFile "$DropPath\$($UPN).jpg" 

That’s it. The for loop will allow you to download all of your user’s images with the UPN as the file name. I hope this has been helpful.

More Information:

Microsoft Graph API – Powershell, Download user Images

Microsoft Graph API PowerShell

In the last blog, We talked about how to create a registered app with Graph API permissions. This app’s main purpose is to become the base for an employee directory through Powershell. If you haven’t read it yet, you can here. Today’s blog is about how to interact with the app with PowerShell. Here are the pieces of information you will need from the Application.

  1. The Tenant’s ID
  2. The Application ID
  3. The Secret Key

If you have those three pieces of information, we can build a script to grab all the users. The app has “User.Read.All” permissions. What this means is that the app can pull all the information inside the azure ad that is directly linked to the user’s Azure AD account. This does not include things like SharePoint, mail, etc, directory, teams, etc. You have to grant permission for those items. But items like name, usernames, images, and much more is accessible as read-only.

The first thing we want to do is put the required information into variables to be used over again and again.

$AppID = "XXXXXXXX-XXXX-xxxx-xxxx-xxxxxxxxxxxx"
$AppKey = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
$ClientID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

Next, we want to make a variable that will host the clientID inside the Oauth URL string. We use this string to authenticate later we want to grab the access token.

$Token = "https://login.microsoftonline.com/$($ClientID)/oauth2/v2.0/token"

The body of the access token requires a redirect url. You can set this as “http://localhost” if you want. We really don’t care unless the app has a redirected URL assigned to it. That’s for another day. So, the next step is to build the body. We do this with a basic hash table. Inside this table, we need client_id, client_secret, redirect_url, grant_type, and the scope.

$Body = @{
        client_id     = "$AppID"
        client_secret = "$AppKey"
        redirect_url  = "$redirect_url"
        grant_type    = "client_credentials"
        scope         = "https://graph.microsoft.com/.default"
}

Now we have the Token URL, and the body. We need to get the access token. We do this with a post method to the Token URL using the body information. Then we place that token in its own variable to be used in the header.

$request = Invoke-RestMethod -Uri $token -Body $Body -Method Post
$Access_Token = $request.access_token

Now we need to create the header. We are using the Authorization inside the header. We bear the access token inside the header.

$Header = @{
        Authorization = "Bearer $($Access_Token)"
}

Up to this point, we have been building the header for the next command. Now we need to switch gears and create the query URL for the graph. You can build out the string inside the graph explorer (Link) and read up on the documentation. The permissions give us access to the users’ information. You can read up on the query statement at this link.

In our example, we want to get a list of all users and these values.

  • First Name
  • Last Name
  • Display Name
  • Email Address
  • What they use to sign in with
  • Job Title
  • Account Enabled
  • Assigned Licenses.
$userInfoLink = 'https://graph.microsoft.com/v1.0/users?$select=givenname,surname,displayName,mail,userPrincipalName,jobtitle,accountenabled,assignedlicenses'

We are using the /users api to gather this information. Hint to the User.Read.All permissions we gave in the last blog post. Now, we grab the first piece of information using Invoke-RestMethod.

$PageInfo = Invoke-RestMethod -Headers $Header -Uri $UserInfoLink -method get 
$PageInfo.Value
$Userinfo = $PageInfo.Value

BAM! we have information. $PageInfo.Value will give you the first hundred records. Wait, only the first 100? Yep, Graph only presents the first 100 items. So, how do you handle that? You create a loop. how do we determine if we need a loop, we look to see if the value ‘@odata.nextlink’ exists. We do this with a Do While loop and the ‘@odata.nextlink’ information. The ‘@odata.nextlink’ is the next link request that will need to be executed. AKA the next page of the outcome. So each time the loop has a ‘@odata.nextlink’ the final loop will stop because it doesn’t have the link.

Do {
        $PageInfo = Invoke-RestMethod -Headers $Header -Uri $PageInfo.'@odata.nextlink' -Method Get
        $Userinfo += $PageInfo.Value 
} while (($PageInfo | Get-Member).name -contains '@odata.nextlink')

Notice we keep adding $PageInfo.Value to userinfo. This is building the userinfo array. This way we have the data we need. One of the requirements for this script is to have the Assigned user Information. So we need to add the sku part number of each license to the user. The problem is the assigned licenses gives back only the skuID. So what we can do is loop through the $UserInfo and add them accordingly. So we do this by starting a For Loop. Why a for loop? Because we will be adding a member property of the sku part numbers.

for ($I = 0; $I -lt $Userinfo.count; $I++) {
}

Inside this loop, we need to get the users’ licenses. We can do that with graphs as well. It’s still part of the user.read.all permissions as it still uses the /users api. We select the user by giving the UPN name. Then we ask for the license details with a /licenseDetails. I know super complex. Here is what the link will look like

for ($I = 0; $I -lt $Userinfo.count; $I++) {
        $LiceLink = "https://graph.microsoft.com/v1.0/users/$($UserInfo[-1].Userprincipalname)/licenseDetails"
}

Next we invoke the rest method again and using the link we generated pull in the information. I have never seen a user have more than 20 licenses. So, no need for a loop here.

for ($I = 0; $I -lt $Userinfo.count; $I++) {
        $LiceLink = "https://graph.microsoft.com/v1.0/users/$($UserInfo[-1].Userprincipalname)/licenseDetails"
        $UserLice = (Invoke-restmethod -Headers $Header -Uri $LiceLink -Method Get).value
}

Finally, we use the Add-Member feature to add a licensing information variable with the sku part number of each licenses inside that user’s account.

for ($I = 0; $I -lt $Userinfo.count; $I++) {
        $LiceLink = "https://graph.microsoft.com/v1.0/users/$($UserInfo[-1].Userprincipalname)/licenseDetails"
        $UserLice = (Invoke-restmethod -Headers $Header -Uri $LiceLink -Method Get).value
        $UserInfo[$i] | Add-Member -Name "LicenseInfo" -MemberType NoteProperty -Value $UserLice.skupartnumber
}

Each time the loop runs, it pulls that User’s UPN from the UserInfo array. Grabs the details of the license from graph and pulls only the sku part number. Then adds that part number to the license info inside the UserInfo array at that location. Let’s put it all together now.

The Script

Function Get-GraphEmployeeReport {
    [cmdletbinding()]
    param (
        [string]$org,
        [string]$AppID = (Read-Host -Prompt "AppId"),
        [string]$AppKey = (Read-Host -Prompt "AppKey"),
        [string]$ClientID = (Read-Host -Prompt "ClientID"),
        [string]$redirect_url = "https://localhost",
        [string]$OutfilePath = "C:\FMIT\Reports\MFA",
        [switch]$Output
    )
    $Token = "https://login.microsoftonline.com/$($ClientID)/oauth2/v2.0/token"
    $Body = @{
        client_id     = "$AppID"
        client_secret = "$AppKey"
        redirect_url  = "$redirect_url"
        grant_type    = "client_credentials"
        scope         = "https://graph.microsoft.com/.default"
    }
    $request = Invoke-RestMethod -Uri $token -Body $Body -Method Post
    $Access_Token = $request.access_token
    
    $Header = @{
        Authorization = "Bearer $($Access_Token)"
    }
    $userInfoLink = 'https://graph.microsoft.com/v1.0/users?$select=givenname,surname,displayName,mail,userPrincipalName,jobtitle,accountenabled,assignedLicenses'
    $PageInfo = Invoke-RestMethod -Headers $Header -Uri $UserInfoLink -method get #| where-object { ($_.assignedLicenses.count -gt 0) -and ($_.accountEnabled -eq $true) }
    $Userinfo = $PageInfo.Value
    Do {
        $PageInfo = Invoke-RestMethod -Headers $Header -Uri $PageInfo.'@odata.nextlink' -Method Get
        $Userinfo += $PageInfo.Value 
    } while (($PageInfo | Get-Member).name -contains '@odata.nextlink')
    for ($I = 0; $I -lt $Userinfo.count; $I++) {
        $LiceLink = "https://graph.microsoft.com/v1.0/users/$($UserInfo[-1].Userprincipalname)/licenseDetails"
        $UserLice = (Invoke-restmethod -Headers $Header -Uri $LiceLink -Method Get).value
        $UserInfo[$i] | Add-Member -Name "LicenseInfo" -MemberType NoteProperty -Value $UserLice.skupartnumber
    }
    $Userinfo
}

That’s all it takes to get the information from the azure ad. You can export this data into an XML or JSON and integrate it with any other system that you like. This does work with Powershell universal as well. You can create a table that has the next button or just gather all the information at once and present it. The more times it looks the longer the information will take to populate. Using the User.Read.All, you are also able to pull photos with graph API.

If you have any more questions, feel free to ask.

Microsoft Graph API

Microsoft Graph API

Back in November Microsoft released v1.0 of graph API. Along with it is a large library of documentation. I have been hesitant about making a blog post about Graph for a while because the community seemed to be split on how PowerShell should interact with it. Now the dust is settling, I want to show you how I have worked with Graph API. This method allows for use without any modules. This means it translates to other apps as power automate. My approach is designed for security and separation of duty. However, it’s a long-winded process. In the next few blog posts, I will go over the Employee Directory Graph API I created for another company. The first step is to create a registered app. This post will go through the process of creating the App both manually and PowerShell.

Create an Azure App – Manual

The first step is to log into your Azure tenant. Then click on the Azure Active Directory on the left-hand side.

Then on the left-hand side, Click the App Registrations. You will be greeted by the owned applications. All manually added. If you add an app via PowerShell it will show up under all applications. Now click the New registration button.

Inside the Register An Application window, we will fill in the Name of the application. This is where you will select the supported account type. The way I do it is client by client. This the client owns the application. I don’t want the application to pull data from a different client. So, I radio check the “Account in this organizational directory only **** – single tenant. Then I click register.

After you click register, you will be brought to the application page. Here you want to gather some pieces of information and document them. You will want the Application ID and the Directory ID. Next, we will click the Certificate and Secrets on the left-hand side.

In the certificates and secrets screen, we want to click the New Client Secret. The reason we are doing this is that we want security. This will create that security. Without the key this generates others will not be able to access this application remotely.

What I like to do at this point is create the key with the Application name and the word Key. In this example, we are using Employee Directory Key. The maximum time span you can select is 24 months, aka 2 years. This means in 2 years, you will have to regenerate a key and update your applications accordingly. Once you select the time span you want this key to live, click add.

Now you have the Key created, this is the ONE AND ONLY CHANCE to grab the key value. After this there is no way that I am aware of that you can grab that key. So make sure you click the copy to clipboard button and store it somewhere safe. Best to be in the same location as the application ID and the Tenant ID.

Now you have created an Azure Registered Application manually, let us look at how to do it with PowerShell.

Create an Azure App – PowerShell

The Powershell process for this is much faster and less prone to human errors.

Connect-AzureAD
$AppName = (Read-Host -Prompt "Applicaiton Name")
$azureADAppReg = New-AzureADApplication -DisplayName $AppName -AvailableToOtherTenants $false
$start = (Get-Date)
$end = (Get-Date).AddYears(2)
$Info = New-AzureADApplicationPasswordCredential -CustomKeyIdentifier "($AppName) Key" -ObjectId $azureADAppReg.ObjectId -EndDate $end -StartDate $start
[pscustomobject]@{
    AppName  = $appname
    AppID    = $azureADAppReg.AppID
    Keyname  = "$Appname Key"
    KeyValue = $Info.Value
}
Disconnect-AzureAD

First we connect to Azure AD using the Connect-AzureAD commandlet. Then we grab the name of the application from the user with the read-host commandlet. Then we create the application using the New-azureadapplication command. We give it the name of the application we want. And we say it’s not available to other tenants. Make sure you place the return info into a variable to be used later. This will produce your app ID and object ID.

Next we setup the date and times for a year by creating a start and end time with get-date commands. We will use those two dates to create the secret key. The commandlet is new-azureadapplicationpasswordcredential. The Custom Key Identifier will be the application name plus the word key. We reference the Object ID from the previous azure command for the object ID. We tell it the end date and the start dates. This will produce the secret key. So make sure once again to put that invariable.

Next, we create a custom ps object to display our information. For the App name its the app name. For the AppID we want the first commands appid. For the key name, we use the app name and the word key. Finally, for the key-value, we use the second azure command value output.

Finally, we disconnect from azure AD with the command Disconnect-AzureAd.

Assign Graph API Permissions

Sadly, I have not found a PowerShell method to add graph API permissions. The reason I believe this is the case is because of the nature of these permissions. They must have consent. Basically, you have to click the button manually. So let us add the user.read.all permissions to our employee directory application. Go to the application in your tenant. On the left-hand side, click the API permissions button. Then click Add Permissions

The type of application permissions will appear. You will want to search out Graph API and click on it. Luckily it’s the first item on the list.

Once you click the Microsoft Graph permissions button. You will be greeted with a choice of Delegated Permissions or Application Permissions. Delegated permissions require a user to sign in to the account for the application to run. The Application permissions do not. If you want this to be an automated process, select application. Under select permissions search out the User.Read.All. This will allow the application to read user information like phone numbers, addresses, usernames, etc. It doesn’t give permissions to read emails or anything else like that. Check the box for the permissions you want and click Add permissions.

Once you add the permissions you will notice under the status, “Not Granted for”. This means the application can’t use those permissions yet. You will need to click Grant admin consent for… to add the permissions. Once you click that button, you will be prompted with a confirm, just click the confirm to allow access. Once you allow access, the status will turn green and you will be ready to go.

Now we have granted access, we are free to create with the application. Stay tuned for next week.

Powershell – Open File Dialog

Powershell – Open File Dialog

I was building a script for my manager the other day and inside the script, he wanted us to triple check the naming of a file path. He went as far as to have us copy the file path from the browser. Paste it, Then click rename on the file itself and copy that and paste it. It was extremely time-consuming and micromanagement. I finally got tired of it as it was about 5 file paths. Using the 80/20 rule, I made a function for this within the script that opened the file dialog box and allowed the user to select the file and have it programmatically push the names into the Variable.

The Script

function Get-SHDOpenFileDialog {
    [cmdletbinding()]
    param (
        [string]$InitialDirectory = "$Env:USERPROFILE",
        [string]$Title = "Please Select A file",
        [string]$Filter = "All files (*.*)| *.*",
        [switch]$MultiSelect
    )
    Add-Type -AssemblyName System.Windows.Forms
    $FileBrowser = New-Object System.Windows.Forms.OpenFileDialog 
    $FileBrowser.InitialDirectory = "$InitialDirectory"
    $FileBrowser.Filter = "$Filter"
    $FileBrowser.Title = "$Title"
    if ($MultiSelect) {
        $FileBrowser.Multiselect = $true
    } else {
        $FileBrowser.Multiselect = $false
    }

    $FileBrowser.ShowDialog((New-Object System.Windows.Forms.Form -Property @{TopMost = $true })) | Out-Null
    $FileBrowser.Filenames
    $FileBrowser.dispose()
}

The Breakdown

Let us break down this script and explain it a little better. Our parameters are as the following:

  • InitialDirectory [string]- This is the directory we will open the Open Dialog in. By default we use the user’s profile.
  • Title [string]- This will be the title on the Open Dialog box. We have this because when you execute the function mutiple times in a single script, this can give some direction.
  • Filter [string]- This is the filter that you will sort by. We have it defaulting to all files with all extensions. If you want to guide the user even more, you can change the filter to something like “XML Files (*.xml)| *.xml” for xml files.
  • Multiselect [switch]- This determines if you want to limit the system to a single file select, which is default or allow mulitple select. This way if you want to select 5 files at once, you can. This is more for the advanced user. If you want to guide the user, then don’t flip this switch.

Now we have our parameters, it’s time to build our assembly. We do this by adding the add-type command. We are loading the Forms assembly.

Add-Type -AssemblyName System.Windows.Forms

Now we have loaded the assembly, we need to create the dialog box. To do this we will create a new-object. System > Windows > Forms > OpenFileDialog. From there we build out the properties.

$FileBrowser = New-Object System.Windows.Forms.OpenFileDialog 

Now the object is created, we need to add the properties. The first three properties we are filling are from our parameters. The InitialDirectory, Filter, and Title.

$FileBrowser.InitialDirectory = $InitialDirectory
$FileBrowser.Filter = $Filter
$FileBrowser.Title = "$Title"

Then we ask if the MultiSelect flag is checked. If it is we set the property to true, if not, we set it to false.

if ($MultiSelect) {
    $FileBrowser.Multiselect = $true
} else {
    $FileBrowser.Multiselect = $false
}

Next, we need to show the dialog box. One thing I ran into was the dialog box would open behind my VS code. In VB.net we set forms to TopMost. I tried a few variations of this with the open file dialog box, but this dialog box doesn’t have the TopMost property. So, what we do is open a form inside the form to force the form to the front. The System > Windows > Forms > Form has the TopMost property. So, inside the showdialog method, we plop the new-object into it.

$FileBrowser.ShowDialog((New-Object System.Windows.Forms.Form -Property @{TopMost = $true }))

However, there is catch! This command outputs the button you click. So if you click cancel, you get a cancel as output, if you click a file and click ok, you get an OK and the file name as the next part of the array it produces. The way to resolve this is pipping the command into an out-null.

$FileBrowser.ShowDialog((New-Object System.Windows.Forms.Form -Property @{TopMost = $true })) | Out-Null

Now we have selected the file/files, we need to display them. The data is stored in the filenames property. So we use the $FileBrowser.Filenames to display that information.

$FileBrowser.Filenames

Finally, we need to dispose of the open file dialog. This way we don’t have hundreds of invisible forms clogging up our system.

$FileBrowser.dispose()

I hope that makes since. Here are some Examples

Examples

PS C:\> Get-SHDOpenFileDialog -Title "Select the required files" -MultiSelect -InitialDirectory c:\temp  

The Output:

C:\temp\tempshow.csv
C:\temp\Moo.txt

Example with Filter

PS C:\> Get-SHDOpenFileDialog -Title "Select the CSV file" -MultiSelect -InitialDirectory c:\temp -Filter "CSV Files (*.csv)| *.csv"

The Output:

C:\temp\tempshow.csv
C:\temp\ACC_Active.csv

Fact Prank with Powershell

Fact Prank with Powershell

I love a good prank. Sometimes pranks need to challenge us. I coded a good prank and it requires a scheduled task and a little bit of PowerShell. The idea behind the prank is the computer will alert you with a beep, then read a random fact to you. Then it closes off with a set of beeps. It looks at this over and over and over again. We will call this script facts.ps1.

Facts.ps1

$random = Get-Random -minimum 20 -maximum 60
Start-Sleep -s $random
[system.console]::beep(600, 2000)
Add-Type -AssemblyName System.Speech
$voice = New-Object System.Speech.Synthesis.SpeechSynthesizer
$Facts = (Invoke-WebRequest -Uri https://therandomadmin.com/nextcloud/index.php/s/9rmSoM2ppY5ggia/download).tostring() -split "[`n]"
$random = Get-Random $Facts
$voice.Speak($random)

Let’s break it down so you can appreciate the greatness of this command. The first thing we do is get a random number between 20 and 60 seconds. We do this so the anti-virus doesn’t attack it from starting by the scheduled task time. Then we start sleeping for that amount of time. Once we are done sleeping, we sound the alarm. The [System.Console]::beep(Pitch,Length) does this part.

Next, we import the speech assembly to read off the Fact. That is done by the Add-Type command. Then we set up the voice by using the new-object command and create the System > Speech > Synthesis > Speech Synthesizer. I remember back in high school this was amazing new technology. I wish it would grow.

Now we get the fact sheet from the web. You can download this file and host it wherever you want. I stole it from various sites myself. We grab it with the invoke-webrequest command. Then we convert it to a string. We split said string up by each new string. Now we have the information, we grab a random item by using the get-random command. Then, we dump that into the $voice we made a while ago. It speaks it out to the user. Finally, 3 final beeps to let the user know it’s done and over.

Now we need to take the next steps:

  • Get the script on the target computer
  • Setup a scheduled task to have it run.

To get it on the other persons computer you can put it into a next cloud and download it from there. Use the invoke-webrequest and download the file to the c:\temp\facts.ps1. Then we create two variables, taskname, and facts. The task name is going to be called “Facts” and the Facts needs to be wherever you downloaded the file.

Invoke-WebRequest -Uri "https://rndadhdman.com/index.php/s/mTA6YaDZ7YXAo9E/download" -OutFile c:\temp\Facts.ps1
$taskName = "Facts"
$Facts = "C:\temp\Facts.ps1"

Then we are going to set up action for our scheduled task with the New-ScheduledTaskAction commandlet. The execute will be powershell.exe. The arguments will be execution policy bypass, Noprofile, nologo, noninteractive, windows style will be hidden, and the file is of course the $facts.

$Action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-executionpolicy bypass -noprofile -nologo -noninteractive -WindowStyle Hidden -file $Facts -Force"

Next we create the trigger with the New-ScheduledTaskTrigger commandlet. This is where the fun can be exciting. We can do this one, daily, minutely, oh yeah. We only want to do this once, so we will select once in this case, but we want it to repeat every minute or so. We do that with the new-timespan commandlet.

$taskTrigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(10) -RepetitionInterval (New-TimeSpan -Minutes 1)

Finally, we put it together with the register-scheduledtask commandlet. The task name will be the $taskname, the action will be $action, and the trigger will be $tasktrigger.

Register-ScheduledTask -TaskName $taskName -Action $Action -Trigger $taskTrigger -Force

For the rest of the day, their computer will read off odd strange facts.

I hope you enjoyed it.

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!