by David | Feb 5, 2024 | Information Technology, PowerShell
Today we are going to go over how to create hundreds of users at once using PowerShell in active Directory. This is great for building a home lab to test things out with. To learn how to build your own AD lab, you can look over this video. Towards the end of this video he shows you how to do the same thing, but, today, I am going to show you a simple way to get unique information. This way you can use PowerShell to Create Bulk Users in your Active Directory.
The Script
$DomainOU = "DC=therandomadmin,dc=com"
$Domain = "therandomadmin.com"
$Users = import-csv C:\temp\ITCompany.csv
$OUs = $users | Group-Object -Property StateFull | Select-Object -ExpandProperty Name
New-ADOrganizationalUnit -Name "Employees" -Path "$DomainOU"
$EmployeePath = "OU=Employees,$($DomainOU)"
foreach ($OU in $OUs) {
New-ADOrganizationalUnit -Name $OU -Path $EmployeePath
}
foreach ($user in $Users) {
$Param = @{
#Name
GivenName = $User.GivenName
Surname = $User.Surname
DisplayName = "$($User.GivenName) $($User.MiddleInitial) $($User.Surname)"
Name = "$($User.GivenName) $($User.MiddleInitial) $($User.Surname)"
Description = "$($user.City) - $($User.Color) - $($user.Occupation)"
#Email and Usernames
EmailAddress = "$($User.GivenName).$($User.MiddleInitial).$($User.Surname)@$($Domain)"
UserPrincipalName = "$($User.GivenName).$($User.MiddleInitial).$($User.Surname)@$($Domain)"
SamAccountName = "$($User.GivenName).$($User.MiddleInitial).$($User.Surname)"
#Contact Info
StreetAddress = $user.StreetAddress
City = $user.City
State = $user.State
Country = $user.Country
HomePhone = $user.TelephoneNumber
#Company Info
Company = "DPB"
Department = $user.Color
Title = $user.Occupation
EmployeeID = $user.Number
EmployeeNumber = $user.NationalID.replace("-",'')
Division = $user.State
#Account Data
Enabled = $true
ChangePasswordAtLogon = $false
AccountPassword = ConvertTo-SecureString -String "$($user.Password)@$($user.NationalID)" -AsPlainText -Force
Path = "OU=$($User.StateFull),$EmployeePath"
#Command
ErrorAction = "SilentlyContinue"
Verbose = $true
}
try {
New-ADUser @Param
} catch {
Write-Error "$($User.GivenName) $($User.MiddleInitial) $($User.Surname)"
}
}
Bulk User File?
This script is very dependant on a csv file that magically seems to appear. Well, it doesn’t. The first thing we need is to get a CSV of bulk users to create bulk users. To do this, you can navigate to a site called fake name generator. This site allows you to quickly generate user information to use to build your site.
- Navigate to the https://www.fakenamegenerator.com/.
- Click Order in Bulk
- Check the I agree check box
- Select Common Sepearted (CSV) and the compression is zip.
- Then select your country. I selected American.
- Note: Some languages will cause issues with AD due to unique characters. If you do select this, make sure to correct for it.
- Select your country of choice. I choose the US.
- Select the age and gender ranges. You can keep this standard
- Then I selected All on the included fields.
- Select how many you want and enter email
- Note: A single OU doesn’t display more than 2000 users. This script creates sub OUs just for this case based on the zodaic signs.
- Then verify and place your order.
Once you have the file, we can get started explaining what we are going to do to Create Bulk Users in Active Directory with the Power of PowerShell.
The Breakdown
It’s time to break down this script. The first two lines are the domain information. I’m using therandomadmin.com as a example. The next is the Bulk Users csv. These are the lines you want to change if you want to use this in your own lab. The next line grabs the OUs names. We want the full state names in this case from the csv. Next we will create the Employees OU that will host all of the other OUs.
New-ADOrganizationalUnit -Name "Employees" -Path "$DomainOU"
Now we have the OU built, we will make a path for later. by dropping the Employees and the domain ou into it’s own variable. using this variable, we enter a foreach loop using the OUs. We want to build a new OU for each OU in the OUs.
foreach ($OU in $OUs) {
New-ADOrganizationalUnit -Name $OU -Path $EmployeePath
}
Next, we will go through the loop of users. In each loop, we want to build a splat. Splatting was covered here in a previous blog. In this splat, we are looking over the New-ADUser commandlet. Lets break it apart.
The Splat
GivenName = $User.GivenName
Surname = $User.Surname
DisplayName = "$($User.GivenName) $($User.MiddleInitial) $($User.Surname)"
Name = "$($User.GivenName) $($User.MiddleInitial) $($User.Surname)"
Description = "$($user.City) - $($User.Color) - $($user. Occupation)"
Using the csv file. We are using the Given name, Surname, and Middle Initial. Using this information, we make the display name, given name, sur name and the name. Then we use the city, color and occupation. The next part is we want to build the usernames.
EmailAddress = "$($User.GivenName).$($User.MiddleInitial).$($User.Surname)@$($Domain)"
UserPrincipalName = "$($User.GivenName).$($User.MiddleInitial).$($User.Surname)@$($Domain)"
SamAccountName = "$($User.GivenName).$($User.MiddleInitial).$($User.Surname)"
Using the same structure as the name, We just add dots and for the email, we just add the domain. Then we will grab the street address, city, state, country, and home phone.
StreetAddress = $user.StreetAddress
City = $user. City
State = $user. State
Country = $user. Country
HomePhone = $user.TelephoneNumber
Next we want to use do the company information. We want the department as the color, the Title will be the occupation, employee id will be the users number, the employee number would be the social and finally the division would be the state.
Company = "The Random Admin"
Department = $user. Color
Title = $user. Occupation
EmployeeID = $user. Number
EmployeeNumber = $user.NationalID.replace("-",'')
Division = $user. State
Now we have company information, we want to make account information. Things like being enabled, password changing, the password and finally the OU. We want to do the Full state name for the OU. This way it matches with the OUs we built before hand.
Enabled = $true
ChangePasswordAtLogon = $false
AccountPassword = ConvertTo-SecureString -String "$($user.Password)@$($user.NationalID)" -AsPlainText -Force
Path = "OU=$($User.StateFull),$EmployeePath"
Finally, we want to push though the command itself. These are the cmdletbinding() flag commands like verbose and error action.
ErrorAction = "SilentlyContinue"
Verbose = $true
Now the splat is done. It’s time to build the try catch with a useful error. By default the Error message is massive. So, making it easier with just the Name is very much more helpful. We will make sure to splat in the new-aduser information.
try {
New-ADUser @Param
} catch {
Write-Error "$($User.GivenName) $($User.MiddleInitial) $($User.Surname)"
}
That’s all for this script. It’s not hard, but it should allow you to create a lab quickly. You can download the CSV here if you wish.
What can we learn as a person today?
Unlike the God’s of old, we are not able to create new people in our lives to meet our needs. Instead, we have to find people. Like we pointed out last week, networking is massive. How we are to other with our networking is extremely important. Without networking, we tend to find ourselves in a hole. Imagine a giant hole in the ground with oiled up smooth metal walls and all you have to get out is a rope that is at the top of the hole. There is a lot that can happen here. The rope can stay there. Someone can throw you the rope.
Throwing the rope
Someone can throw you the rope and walk away. The rope will land in the hole with you. You can try to throw the rope out, but without something to cling to, the rope will just fall back down to you. This is like the man who says to just study for this exam or that exam. He threw you a rope, but hasn’t really done anything else.
Now image if someone secured that rope to something like a car or a rock and threw the other end to you. Now you have something to climb up with. This is the person who has given you resources to look into. For example, I hear you want to get into networking but have no experience. I’m going to say study the network plus exam and then tell you about professor messor on youtube. This is super helpful and most people can climb out of the hole with the rope. However, in this senerio, the wall’s are oiled up. Thus, footing is an issue.
Finally, we have the guy who ties the rope to his car, and throws you the other end. Then backs up with his car pulling you out of the hole. This would be a manager, or a senior member of an IT company taking a new person under their wing and leverging the company to help them learn new things. This is the kind of company, I would want to work with.
Final Thoughts
When you are working with people helping them with their career, some people just need the rope. Some people need the anchor and finally some needs to be pulled out of the hole. A lot of this is due to society and situations. Being aware of these facts can help you network with others better and grow your support team. Being aware of yourself allows you to know who you need as well. Do you need the truck? Do you need an anchor? What is it that we need to get you out of the holes that we find ourselves in? What can we be to others?
by David | Jan 9, 2024 | Information Technology, PowerShell
One of my favorite things with powershell is building out splat parameters for commands through the main parameter set. Today we are going to go over how that is done. We are going to do this through the Get-childitem and get-ACL. These are some mighty commands and they can help you find permission gaps quickly and easily. Let us Building Parameters for Commands together.
The Script
Function Get-ACLInfos {
[cmdletbinding()]
param (
[string]$FilePath,
[switch]$Recurse,
[switch]$Directory,
[switch]$File,
[string]$Filter,
[string]$Username
)
begin {
if (!(Test-Path -Path $FilePath)) {end}
$Param = @{
Path = $FilePath
}
if ($Recurse) {$Param | Add-Member -MemberType NoteProperty -Name "Recurse" -Value $true}
if ($Directory) {$Param | Add-Member -MemberType NoteProperty -Name "Directory" -Value $true}
if ($File) {$Param | Add-Member -MemberType NoteProperty -Name "File" -Value $true}
if ($PSBoundParameters.ContainsKey($Filter)) {$Param | Add-Member -MemberType NoteProperty -Name "Filter" -Value "$Filter"}
}
process {
$Items = Get-ChildItem @Param
$ACLinfo = foreach ($item in $Items) {
$ACLs = (Get-Acl -Path $item.FullName).access
foreach ($ACL in $ACLs) {
[pscustomobject][ordered]@{
Path = $item.FullName
FileSystemRight = $ACL.FileSystemRights
AccessControlType = $ACL.AccessControlType
IdentityReference = $ACL.IdentityReference
IsInherited = $ACL.IsInherited
InheritanceFlags = $ACL.InheritanceFlags
PropagationFlags = $ACL.PropagationFlags
}
}
}
}
end {
if ($PSBoundParameters.ContainsKey('Username')) {
$ACLinfo | Where-Object {$_.IdentityReference -contains $Username}
} else {
$ACLinfo
}
}
}
Building Parameters for Commands
This script is simple, it allows you to grab a directory’s ACL recursively. However, how it does it is kind of cool. Get-Childitem has so many different options, but using it within a function can take some of that power away. So, by passing the parameters that we want at the top level allows us to give that power back to the get-childitem. However, this could lead to a lot of if statements. Instead, we are going to build a splat parameter for our commands. I have covered splats in a previous blog post, but I wanted to point them out to express how much power they do have.
The Power of Splat
Splatting is a method in which you can build a parameter set for a command. The amazing part of splatting is the splat is a powershell object. This means you can add to it after the initial splat is started. To start a splat all one has to do is declare a variable with an object attached. The variable is declared with the $ and the object is a simple at symbol followed by curly brackets with an equal sign nestled in the middle. We can use objects anywhere in powershell. A custom PS object is the same way. If you wanted to, you can declare the object and give it an order with [pscustomobject][order]. However, that’s not always the best option as orders cause issue if things are not in that order later down the road.
At this point the powershell object is empty. From here we can start building parameters for our object. To do that we are going to use the add-member command. In our example, we are working with true and false statements. Our function parameters are mostly switches. I threw in some strings as well to give you examples of how to build with a string. The first check is to see if we have a recursive. This is simple to do. We ask if the recurse is true. Then we add the member.
$Param = @{
Path = $FilePath
}
if ($Recurse) {$Param | Add-Member -MemberType NoteProperty -Name "Recurse" -Value $true}
Building the Splat
The add-member starts us down the path of building our splat. The Add-member can give us a lot of different options on which way we want to add things. Here, we need to add the recurse flag to get-childitem. This is a note property inside the command. The best way I see note properties is a name with a value. That value can be null. Here we are adding the “Recurse” name and giving it a value of the boolean true. Thus, when we drop the splat into the command, the command will see a flag of recurse. We do this same method with the rest. Directory is a flag, and so is file.
Unlike the last three, the filter parameter is a string. We are going to use the same method using the note property. We want to give the name as a filter and the value will be our value from our command line. The difference here is we want to place that filter in a string. The next part of the filter is how we test the filter. Instead of doing a simple check to see if the value is true, we need to check the parameters. This is done through the value $PSboundParameters. We want to see which keys the power shell bound parameters are holding. What this means is when you do get-command -something “bob” we want to know if something exists. Then we are going to use that’s something, aka bob.
$Param = @{
Path = $FilePath
}
if ($Recurse) {$Param | Add-Member -MemberType NoteProperty -Name "Recurse" -Value $true}
if ($Directory) {$Param | Add-Member -MemberType NoteProperty -Name "Directory" -Value $true}
if ($File) {$Param | Add-Member -MemberType NoteProperty -Name "File" -Value $true}
if ($PSBoundParameters.ContainsKey($Filter)) {$Param | Add-Member -MemberType NoteProperty -Name "Filter" -Value "$Filter"}
Finishing the Splat
The next step is to use the splat. Which is very easy to do. To use a splat, instead of using the dollar sign, all you need to do is use the at symbol and the parameter. The Command should not have any other flags set as it all lives inside the splat. Building a parameter splat is super easy and makes life easier in the long run.
$Items = Get-ChildItem @Param
What can we learn as a person from splatting
As we go through our lives, we are a representation of a splat. We start off with a few things and over time we add and remove aspects from ourselves. Others add and remove aspects of us as well. Growing up in school, teacher pours so much of their selves into their students. As we get older, we have to do the same for ourselves. We have to add the note properties to our lives, or we will always stay the same. Today you add-member -membertype noteproperty “Splatting” -value “dang it’s cool” to yourself. Unlike a computer though, we have to practice to bring it close to ourselves. We have to conceptualize the idea.
As you go through life, you have to depend on your own splat. Not enjoying that splat, means you have to work on it. It takes action to do so. The thing that stops most people from taking that action is fear of the unknown or fear of dealing with the pain. As someone who has been working on himself for many years now, I can safely say, it’s ok not to be ok. It’s ok not to enjoy parts of your splat, but overall, your splat is who you are. So go out into this world and put your @ on things. Change up that splat if you don’t like parts of it. Just enjoy being yourself.
Additional Resources
by David | Sep 1, 2023 | Information Technology, PowerShell, Resources
Recently, I have been troubleshooting radius. Radius hasn’t changed in decades. I say this lightingly too. The network policy server is a feature that helps with connecting things like unifi wifi and more. Each radius connection produces a reason code. Today we want to look through the radius logs and get as much useful information without screaming at the screen. Radius logs are a little daunting. This is why many people use an SQL server for the radius logs. However, if you are not one of those people who can do this, the traditional radius logging works wonders. So, we will read radius logs with PowerShell.
Radius Logging
Before we start, we need to know what we are dealing with. The standard location for readius logs is: C:\Windows\System32\LogFiles. You can change this location as you see fit. I personally changed my locations to a c:\logs\radius location. This helps me find it quicker and generally, I don’t have trouble guessing what is what. You can set the radius log location by doing the following:
- Start Network Policy Server
- Click account
- Under Log File Properties click Change Log File Properties
- A box will pop up called “Log File Properties” Click on the “Log File” tab.
- This is where you can change your directory.
- Change your Format to DTS Compliant. As this script works best with it.
- I personally like smaller files when I am working with log searches. So I select the “When log file reaches this size:” I select 1 – 5 MB.
- Click ok
Now your log files will live wherever you told them. You will need to change the script around a little if you are using a different location than me.
The Script and Breakdown
$NPSLogs = Get-content -Path "C:\Logs\Radius\IN2308.log" | Select-Object -Last 6
foreach ($NPSLog in $NPSLogs) {
[pscustomobject][ordered]@{
TimeStamp = ([xml]$NPSLog).event.Timestamp.'#text'
ComputerName = ([xml]$NPSLog).event.'Computer-Name'.'#text'
Sources = ([xml]$NPSLog).event.'Event-Source'.'#text'
Username = ([xml]$NPSLog).event.'User-Name'.'#text'
CalledStationId = ([xml]$NPSLog).event.'Called-Station-Id'.'#text'
CallingStationId = ([xml]$NPSLog).event.'Calling-Station-Id'.'#text'
NasPortType = ([xml]$NPSLog).event.'NAS-Port-Type'.'#text'
NasIdentifier = ([xml]$NPSLog).event.'NAS-Identifier'.'#text'
NasPort = ([xml]$NPSLog).event.'NAS-Port'.'#text'
ClientIPAddress = ([xml]$NPSLog).event.'Client-IP-Address'.'#text'
ClientVendor = ([xml]$NPSLog).event.'Client-Vendor'.'#text'
ClientFriendlyName = ([xml]$NPSLog).event.'Client-Friendly-Name'.'#text'
ProxyPolicyName = ([xml]$NPSLog).event.'Proxy-Policy-Name'.'#text'
ProviderType = ([xml]$NPSLog).event.'Provider-Type'.'#text'
SamAccountName = ([xml]$NPSLog).event.'SAM-Account-Name'.'#text'
FQUsername = ([xml]$NPSLog).event.'Fully-Qualifed-User-Name'.'#text'
AuthType = ([xml]$NPSLog).event.'Authentication-Type'.'#text'
Class = ([xml]$NPSLog).event.Class.'#text'
PacketType = ([xml]$NPSLog).event.'Packet-Type'.'#text'
ReasonCode = ([xml]$NPSLog).event.'Reason-Code'.'#text'
TunnelClientEndpt = ([xml]$NPSLog).event.'Tunnel-Client-Endpt'.'#text'
}
}
As you can tell, this script needs to be used on the server in question. However, You could wrap this script into a nice wrapper. That would allow you to execute on remote machines. The breakdown is very simple on this one as well. DTS is an XML format. Thus, you just need to use the [XML] before any of the lines. The XML is formatted with the event, it’s name, and the text. It’s a very simple setup. From there I select what I want and give it in a pscustom object. That’s it. Its a very simple setup. That’s why we should always read radius logs with Powershell.
by David | Jul 17, 2023 | Information Technology, PowerShell
Often times I need to run a speed test on a remote machine. Doing a speed test tends to help you understand what’s going on with the end user’s computer. For example, currently, my ISP is having issues. I am getting around 6mbps down and 75 Mbps up. If I called in and said I couldn’t watch training videos… that’s why. so it’s essential to know the speeds. That’s why we want to do a speed test with Powershell.
The Script
#Tests if C:Temp Exists, If it doesn't, makes it.
$CTemp = "c:\Temp"
if (!(Test-Path $CTemp)) { New-Item -Path "c:\" -Name Temp -ItemType Directory }
if (!(Test-Path "$CTemp\SpeedTest")) { New-Item -Path "c:\" -Name Temp -ItemType Directory }
#Download the Speed Test Tool.
$URL = "https://install.speedtest.net/app/cli/ookla-speedtest-1.0.0-win64.zip"
$DownloadPath = "$CTemp\SpeedTest.Zip"
if (!(Test-Path $DownloadPath)) { Invoke-WebRequest -Uri $URL -OutFile $DownloadPath }
#Expand the Zip File
Expand-Archive $DownloadPath -DestinationPath "$CTemp\Speedtest" -Force
$test = & "C:\temp\Speedtest\speedtest.exe" --accept-license
$DownloadSpeed = [regex]::match(($test | where-object { $_ -like "*Download:*" }).trim(), '[0-9]+\.?[0-9]*').value
$uploadSpeed = [regex]::match(($test | where-object { $_ -like "*Upload:*" }).trim(), '[0-9]+\.?[0-9]*').value
$ISP = ($test | where-object { $_ -like "*ISP:*" }).trim().split(":")[1].trim()
$server = ($test | where-object { $_ -like "*Server:*" }).trim().split(":")[1].trim()
$SpeedTestURL = ($test | where-object { $_ -like "*Result URL:*" }).trim().split(" ")[2].trim()
[PSCustomObject]@{
Server = $server
ISP = $ISP
Download = $DownloadSpeed
Upload = $uploadSpeed
ResultsURL = $SpeedTestURL
}
The Breakdown of the Speed test with PowerShell
As we enter this script, you can see my laziness already. The first thing I do is make a string called ctemp for the “c:\temp”. This way I don’t have to type “c:\temp” repeatedly. Next, we test the path to see if c:\temp exists. If it doesn’t, we make it. We do this with the new-item command with the name as temp and the item type as a directory. We do the same thing with c:\temp\speedtest for later. If you really want to get fancy, you can replace the C: with an $env:systemdrive.
Building the folders
$CTemp = "c:\Temp"
if (!(Test-Path $CTemp)) { New-Item -Path "c:\" -Name Temp -ItemType Directory }
if (!(Test-Path "$CTemp\SpeedTest")) { New-Item -Path "c:\" -Name Temp -ItemType Directory }
The next step is to download the speed test application from ookla. We need the URL and where it’s going to be downloaded. That’s why the URL has the URL needed. We want to call the file speedtest.zip. Next, we test to see if we already downloaded it. If we didn’t, we download the file using the invoke web request and the out file tags.
Downloading the app
$URL = "https://install.speedtest.net/app/cli/ookla-speedtest-1.0.0-win64.zip"
$DownloadPath = "$CTemp\SpeedTest.Zip"
if (!(Test-Path $DownloadPath)) { Invoke-WebRequest -Uri $URL -OutFile $DownloadPath }
Once we have the file downloaded, we need to extract the Speed test files. We use the expand archive command to force the download to the c:\temp\speedtest folder that we made at the beginning. I use the force flag here to overwrite anything just in case the file was already there.
Expanding the drive
#Expand the Zip File
Expand-Archive $DownloadPath -DestinationPath "$CTemp\Speedtest" -Force
Next, we run the Speed test with PowerShell. In PowerShell, you can run apps directly inside the shell and input that data directly into the shell by using the & sign. With this software, we need to accept the license if we want to do this script without human interaction. Finally, we want to push its output into a variable. Since it’s a test, let us call it a test.
Running the Speed test with PowerShell
$test = & "$CTemp\Speedtest\speedtest.exe" --accept-license
The variable $test contains a few strings with the information we need. It’s time to work with those strings. The string contains the server, isp, download, upload, and jitter information. Here is an example output of what this little program produces:
Speedtest by Ookla
Server: Piedmont Redacted
ISP: Spectrum Business
Latency: 22.46 ms (0.12 ms jitter)
Download: 166.84 Mbps (data used: 217.5 MB)
Upload: 194.09 Mbps (data used: 224.6 MB)
Packet Loss: 0.0%
Result URL: redacted
The first item I want to extract is the download. The code is a little complex so lets start from the inside. We start off with our $test variable. We want to find the string line that contains “Download:” and then trim that up.
($test | where-object { $_ -like "*Download:*" }).trim()
Next, we need to wrap this inside a regex match. We are using the .net structure here using the [regex]:match. Don’t use matches, it will give you additional information. The match uses our variable and the regex to match with. The regex is the hardest part. So let’s take it apart.
The Regex
The first part [0-9] is searching for characters 0 – 9. However, it only looks at the first digit. The + looks for the rest until we reach a “.”. The “\” is before the “.” because “.” is a used variable in regex. If we stop here, we only get the first part of the download speed. We don’t get anything past the “.”. So we add the “?”. Once we do this it allows us to continue. At this point, we look for another [0-9]. Once again, it’s just the first character. We want everything past that so we add the “*”. Now we have the first match. This is the download speed. Once we have the regex information we ask for only the value. We do this with the upload speed as well.
$ISP = ($test | where-object { $_ -like "*ISP:*" }).trim().split(":")[1].trim()
$server = ($test | where-object { $_ -like "*Server:*" }).trim().split(":")[1].trim()
$SpeedTestURL = ($test | where-object { $_ -like "*Result URL:*" }).trim().split(" ")[2].trim()
Now, we need to work with The ISP. We search for the “ISP:” inside our test variable. Like before, we trim up the string. This removes the spaces at the start and end. Then we split this string with the “:”. The split is here because it makes sense. We select the second object from that split as it contains the text. The first, which is also 0, doesn’t contain the text. Now we have the string, we once again trim it up. There you go. That’s the ISP. We do the same thing with the server as the ISP. The results URL is a little different. We split the space and then select the third object.
Displaying the information
Finally, we create a PS custom object and place each piece of useful information into it. When you launch the script, this information will return and you can see it inside your rmm feedback. From here, you can add this information to your RMM tool in custom fields if you wish. Since I use Ninja at the moment, I will go over that later.
That’s it, Speed Test with PowerShell is at your fingertips. I hope you enjoy it.
Additional Resources
by David | Jul 10, 2023 | Information Technology, PowerShell
Last week we discussed sending emails with Graph API. You can read about it here. Today we will be taking that script and making it so it can be automated. On the backend, you will need to create an Azure App. You can read about how to do that here. The following code only works in Powershell 7 and above. Automating with Graph API works best in PowerShell 7. You will need to set up your App with Users.Read.All and Mail.Send as the minimal. levels.
The Script
import-module Microsoft.Graph.Users
Import-module Microsoft.Graph.Users.Actions
$EmailToSend = "A Cloud Email @ your domain"
$EmailToReceive = "Any Email"
$AppID = "This is your App ID"
$SecuredPassword = "This is your Password"
$tenantID = "This is your tenant ID"
$SecuredPasswordPassword = ConvertTo-SecureString -String $SecuredPassword -AsPlainText -Force
$ClientSecretCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $AppID, $SecuredPasswordPassword
Connect-MgGraph -TenantId $tenantID -ClientSecretCredential $ClientSecretCredential
#Connect-MgGraph -Scopes "User.Read.All, Mail.Send"
$users = Get-MgUser -filter "accountenabled eq false"
$ReturnString = ""
foreach ($user in $users) {
if ($null -ne (Get-MgUserLicenseDetail -UserId $user.Id)) {
[pscustomobject][ordered]@{
UPN = $user.UserPrincipalName
Licenses = (Get-MgUserLicenseDetail -UserId $user.id).SkuPartNumber -join ", "
}
$ReturnString = $ReturnString + "$($user.UserPrincipalName): $((Get-MgUserLicenseDetail -UserId $user.id).SkuPartNumber -join ", ")`n"
}
}
$body = @"
<html>
<header>Licenses</header>
<body>
<center>
<h1>Disabled Users</h1>
<h2>With Licenses</h2>
</center>
$ReturnString
</body>
</html>
"@
$params = @{
message = @{
subject = "Disabled Users with Licenses"
body = @{
contentType = "HTML"
content = $body
}
toRecipients = @(
@{
emailAddress = @{
address = $EmailToReceive
}
}
)
}
saveToSentItems = "false"
}
# A UPN can also be used as -UserId.
Send-MgUserMail -UserId $EmailToSend -BodyParameter $params
The Breakdown
This script is the same as last week’s except for how it connects and how you feed the email addresses. We are using the Client Secret Credential flag, which is only available in Powershell 7, to trigger the connect command. You need some basic information first. This information will allow Automating with Graph API to work.
$AppID = "This is your App ID"
$SecuredPassword = "This is your Password"
$tenantID = "This is your tenant ID"
The App is the application ID from the azure app you created. the tenant ID is also the tenant ID of the azure app you created. Remember, I stated to keep the secret key value. This is where you will use it. Place it in the Secure Password area. Next, we need to convert this information into a secure object.
$SecuredPasswordPassword = ConvertTo-SecureString -String $SecuredPassword -AsPlainText -Force
$ClientSecretCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $AppID, $SecuredPasswordPassword
Now, we need to convert the plain text to a secure string. We do this with the convertto-securestring command. We enter the string and force it with the force tag. Once we have done that, we want to create a credential object. We use the new-object command to create an automation pscredential object. We feed it the appID and the password we created above. This gives us the ps object that we will use for the next part.
Connect-MgGraph -TenantId $tenantID -ClientSecretCredential $ClientSecretCredential
Using the connect-mggraph command we connect to our tenant and pass the app id and password as a single object. This will connect us directly to Graph API. Later we will run this script through the task scheduler. The remainder of the script will stay the same. Finally, we supply the email addresses. Automating with Graph API couldn’t be easier. So Enjoy!
Additional Links
by David | Jul 5, 2023 | Information Technology, PowerShell
Last week we spoke about finding disabled users with licensing using PowerShell and graph API. Today, we will be expanding from that blog. We are going to send the results ourselves. Next week, we will create this into an automation using application rights and azure apps. However, today we will send Emails with Graph API and Powershell.
The Script
import-module Microsoft.Graph.Users
Import-module Microsoft.Graph.Users.Actions
Connect-MgGraph -Scopes "User.Read.All, Mail.Send"
$users = Get-MgUser -filter "accountenabled eq false"
$ReturnString = ""
foreach ($user in $users) {
if ($null -ne (Get-MgUserLicenseDetail -UserId $user.Id)) {
[pscustomobject][ordered]@{
UPN = $user.UserPrincipalName
Licenses = (Get-MgUserLicenseDetail -UserId $user.id).SkuPartNumber -join ", "
}
$ReturnString = $ReturnString + "$($user.UserPrincipalName): $((Get-MgUserLicenseDetail -UserId $user.id).SkuPartNumber -join ", ")`n"
}
}
$EmailSend = Read-Host "Email Address to send (Cloud Only)"
$Emailreceive = Read-Host "Email Address to Receive"
$body = @"
<html>
<header>Licenses</header>
<body>
<center>
<h1>Disabled Users</h1>
<h2>With Licenses</h2>
</center>
$ReturnString
</body>
</html>
"@
$params = @{
message = @{
subject = "Disabled Users with Licenses"
body = @{
contentType = "HTML"
content = $body
}
toRecipients = @(
@{
emailAddress = @{
address = $Emailreceive
}
}
)
}
saveToSentItems = "false"
}
# A UPN can also be used as -UserId.
Send-MgUserMail -UserId $EmailSend -BodyParameter $params
The Breakdown
The same
Like last week, we are using the Microsoft Graph PowerShell module. The first part of this code is the same as last week. So you can take a good read on that one. The only difference is our scope. We are adding mail.send. Mail.Send allows us to send emails from cloud users. It will not work with inactive, soft-deleted, or on-premise-hosted devices. Thus the connect-mggraph will look like the below
Connect-MgGraph -Scopes "User.Read.All, Mail.Send"
The only other thing we have added to the original script is a return string. We initialize the return string with a $returnstring = “” and then we build the string out. Using the same as before, we grab the SKU part number. Finally, we use the join. The difference is we wrap the command in a bubble, $(), for our string. Then we put the sting, the new information, and a line break, `n, into the string.
$ReturnString = $ReturnString + "$($user.UserPrincipalName): $((Get-MgUserLicenseDetail -UserId $user.id).SkuPartNumber -join ", ")`n"
Emails with Graph API
The first thing we want to know is Who we are sending the email to and Who is sending the email. The one sending the email has limitations. First, it can’t be an inactive user. The user can’t be in a soft-deleted state. Finally, it has to be hosted in the cloud. The person we are sending to has to have an email box.
The next part is where we create the email we are going to send. Remember that returnstring we made a few moments ago, it’s time to use that. We are using a here string. Here strings allows large string data like an HTML page, to be placed into a string. Here strings are set apart using the @ symbol. Take a look at the $body below.
$body = @"
<html>
<header>Licenses</header>
<body>
<center>
<h1>Disabled Users</h1>
<h2>With Licenses</h2>
</center>
$ReturnString
</body>
</html>
"@
Params
Please note that some PowerShell ide does not like the tabbing inside a here-string. The next part is the parameters of the email system. We are sending a message. Additional documentation can be found here. We are going to use the following tags, subject, body, to recipient, and save to sent items. All of these items are setup as a Json file as the API uses Json as well.
- Subject: What is the subject of the email
- Body: body contains the content type and the content. Here is where we will be using our $body.
- To Recipients: This is where the email addresses will go. We set up an email address and have it as an array.
- Save to Sent Items: Finally, we determine if we want this item to stay in our sent items.
$params = @{
message = @{
subject = "Disabled Users with Licenses"
body = @{
contentType = "HTML"
content = $body
}
toRecipients = @(
@{
emailAddress = @{
address = $Emailreceive
}
}
)
}
saveToSentItems = "false"
}
Finally, we use the send-mgusermail command. This is where we use the send email. It will be the UPN of the target user we want to send email from. The body parameter will be the parameters we just built. Once you do this, you will see the email come in accordingly. That’s how you can send Emails with Graph API.