Category Archives: Quick Learn

Practical examples of PowerShell concepts gathered from specific projects, forum replies, or general PowerShell use.

Extract LAPS Passwords and BitLocker Keys from Active Directory

https://i0.wp.com/cybersecurity-excellence-awards.com/wp-content/uploads/732220.png?w=625&ssl=1

The below PowerShell can be used to extract LAPS Passwords and BitLocker Keys from Active Directory. This was written on Friday, July 19, 2024, due to the CrowdStrike Outage: https://www.nytimes.com/2024/07/19/business/microsoft-outage-cause-azure-crowdstrike.html. If you choose to test this PowerShell, ensure that you update the argument for the SearchBase parameter to reflect your Active Directory domain.

function Get-LapsAndBitLocker {
    # Version 1.0.1
    Get-ADObject -Filter {objectclass -eq 'msFVE-RecoveryInformation'} -SearchBase 'DC=domain,DC=com' -Properties msFVE-RecoveryPassword |
        Select-Object -Property `
            @{N='ComputerName';E={$_.DistinguishedName.Split(',')[1].Split('=')[1]}},
            @{N='LapsPassword';E={(Get-ADComputer -Identity ($_.DistinguishedName.Split(',')[1].Split('=')[1]) -Properties ms-Mcs-AdmPwd).'ms-Mcs-AdmPwd'}},
            @{N='DateTime'    ;E={Get-Date -Date ($_.DistinguishedName.Split(',')[0].Split('{')[0].Split('=')[-1])}},
            @{N='BitLocker'   ;E={$_.'msFVE-RecoveryPassword'}} |
                Sort-Object -Property ComputerName
} # end function: Get-LapsAndBitLocker.

Explore the Command and Ensure the Efficiency

The best part of this entire process—the writing about PowerShell for coming up on nine years—has been the realization of my own mistakes while using PowerShell, and then sharing those. I have an upcoming post on this, but in between preparing and publishing that, I, yes me, made another mistake.

Let’s start with what I wrote first, which is included below. It’s a simple Get-ADComputer command. By now, we’ve all likely written plenty of these. Looks great right? Does it though?

Get-ADComputer -Filter * -Properties OperatingSystem,Description |
    Where-Object -Property OperatingSystem -like '*Server*' |
    Select-Object -Property Name, OperatingSystem |
    Sort-Object -Property OperatingSystem, Name

Before we move forward and determine what I did wrong, let’s use Measure-Command to see how long the execution takes for me on my machine.

Measure-Command -Expression {
    Get-ADComputer -Filter * -Properties OperatingSystem,Description |
        Where-Object -Property OperatingSystem -like '*Server*' |
        Select-Object -Property Name, OperatingSystem |
        Sort-Object -Property OperatingSystem, Name
}
Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 4
Milliseconds      : 105
Ticks             : 41055467
TotalDays         : 4.75179016203704E-05
TotalHours        : 0.00114042963888889
TotalMinutes      : 0.0684257783333333
TotalSeconds      : 4.1055467
TotalMilliseconds : 4105.5467

Four seconds. That doesn’t seem too long, but I’m guessing it is based on the way in which the command was written. I never really trust a single test, so let’s set up the above Measure-Command command to run 10 times consecutively using ForEach-Object.

1..10 | Foreach-Object {
    Measure-Command -Expression {
        Get-ADComputer -Filter * -Properties OperatingSystem,Description |
            Where-Object -Property OperatingSystem -like '*Server*' |
            Select-Object -Property Name, OperatingSystem |
            Sort-Object -Property OperatingSystem, Name
    }
} | Select-Object -Property Seconds, Milliseconds
Seconds Milliseconds
------- ------------
      5          370
      5          213
      5          311
      5          839
      4          500
      6          234
      5           50
      5          239
      4          656
      5          421

Multiple tests and this is closer to a full five seconds per invocation. I don’t know about you, but five seconds in PowerShell is an eternity. We use PowerShell for accuracy, sure, but we use it for efficiency, as well. The quicker the better; we shouldn’t limit ourselves. If you find a mistake then fix it.

Now, let’s use the Filter parameter the way it was intended to be used. Piping to Where-Object is always going to be slower than using the filtering options provided by the commands we use. Here’s our base command corrected.

Get-ADComputer -Filter {OperatingSystem -like '*Server*'} -Properties OperatingSystem, Description |
    Select-Object -Property Name, OperatingSystem |
    Sort-Object -Property OperatingSystem, Name

And here it is wrapped up to be tested 10 times such as we did previously.

1..10 | Foreach-Object {
    Measure-Command -Expression {
        Get-ADComputer -Filter {OperatingSystem -like '*Server*'} -Properties OperatingSystem, Description |
            Select-Object -Property Name, OperatingSystem |
            Sort-Object -Property OperatingSystem, Name
    }
} | Select-Object -Property Seconds, Milliseconds
Seconds Milliseconds
------- ------------
      1          501
      0          922
      0          907
      0          895
      0          930
      0          906
      1          790
      1          534
      1          284
      0          937

Maybe you’re not worried about those lost seconds. If those aren’t that important, then let it be the possibility that someone’s going to see your failure to be efficient. If I saw this—and again I made this mistake first—I would be concerned that the person writing this code didn’t better explore the command(s) they’re using. If you’re going to use a command, then know it well enough to know how well you’re using it.

Hash Table with Embedded Hash Table and Array

Sometimes I write something—as in some PowerShell—and I feel like, while I don’t need it right now and here today, I should put it somewhere. I just found a screenshot where I did that. Maybe it’ll help someone, and posting it here on my site will likely allow me to find it when I’ll need it, versus looking around on random computers for random screenshots. I would’ve never found this image were I actually looking for it.

I must’ve wondered, can I embed both a hash table and an array in a hashtable? I can. First, however, let’s start with a single, simple array, and a single simple hashtable. Let’s make sure we know these.

An array is a collection—remember that word, as you’ll hear it again in PowerShell (in place of “array”)—of items. In other languages, you might hear it referred to as a list. That’s not a bad visual. I write what I need at the grocery store on a Post-it note. I suppose it’s an array; it is a collection of items that I need at home, whether it be food, cleaning supplies, or something else. In the next example, we’ll create two identical arrays in two different manners—one without and one with the array sub-expression operator—and store them in two separate variables.

$ArrayWithoutOperator = 'e','f','g'
$ArrayWithOperator = @('e','f','g')
$ArrayWithoutOperator
e
f
g
$ArrayWithOperator
e
f
g

Let’s prove that at least one of these is an array. While I practically never use Format-Table, I’ve used it in this example to ensure all the output is packed as closely as possible to the left.

ArrayWithoutOperator.GetType() | Format-Table -AutoSize
IsPublic IsSerial Name     BaseType
-------- -------- ----     --------
True     True     Object[] System.Array

It is!

A hash table—and this is my favorite way in which to explain it—is an associative array. Huh? It’s still an array or a list, but now, each item has an association. Each item in the array contains a key and a value. Take a look, keeping in mind that PowerShell will label the keys with “Name” (while the value will be labeled with “Value”).

$Hashtable = @{'mid1' = 'c';'mid2' = 'd'}
$Hashtable
Name                           Value
----                           -----
mid2                           d
mid1                           c

Now, let’s recreate our array and our hash table inside of another hash table. I’m going to show this using both a single line and multiple lines. It’s important to see those semicolons.

@{'top1' = 'a';'top2' = 'b';'top3' = @{'mid1' = 'c';'mid2' = 'd'}; 'bottom1' = @('e','f','g')}

@{
    'top1'    = 'a'
    'top2'    = 'b'
    'top3'    = @{'mid1' = 'c';'mid2' = 'd'}
    'bottom1' = @('e','f','g')
}
Name                           Value
----                           -----
top2                           b
bottom1                        {e, f, g}
top3                           {[mid2, d], [mid1, c]}
top1                           a

That output is painful. As you may have been able to guess, based on the values, I wanted to see elements labeled using “top” at the top of this list, and the element using “bottom” at the bottom. This is how hash tables work in PowerShell in my experience: You’re not going to get it back the way you might expect. Therefore, you’re going to need to know about the ordered attribute which you can apply to a hash table to create an ordered dictionary.

[ordered]@{
    'top1'    = 'a'
    'top2'    = 'b'
    'top3'    = @{'mid1' = 'c';'mid2' = 'd'}
    'bottom1' = @('e','f','g')
}
Name                           Value
----                           -----
top1                           a
top2                           b
top3                           {[mid2, d], [mid1, c]}
bottom1                        {e, f, g}

Now that we have it in the order in which we expect, let’s pay a bit more attention to the associated values for each key. The key top3 is our embedded, or nested, hash table. We can see the representation in the value, where the nested keys and associated value are displayed as [mid2, d], [mid1, c] indicating a hash table (or associative array). Our value for the bottom1 key is e, f, g, indicating an array. And as easy as that, a hash table can be a container for additional hash tables and arrays.

Adding a Help Parameter to a Function

Edit: There’s a quick addition at the bottom of this post. I tried something else, it worked, and so I decided to include it.

I started writing a PowerShell function to replicate the work of an old executable called ICSWEEP. ICSWEEP “is a command-line utility to clear the Temporary Internet Files Cache and/or the TEMP files folder of ALL user profiles that are NOT in use when this command is executed.” ICSWEEP Information. I may or may not walk through the process of writing this function here; it’s to be determined. What I do want to discuss for sure, however, is the last switch in the above image. It’s /?.

We cannot add ? parameter to a function. Below is an image of the message when we attempt to do that in VS Code.

That’s right, right!? You do remember the $? variable. It stores the execution status of the last command as True or False. Instead of ? as a parameter, I used the value of Help. Therefore, someone can invoke my function and use -Help to get help with the function. But could they?

I sat there, wondering, can you create a parameter of a function so that it’ll return its own, comment-based help? It turns out you can. Here’s the early stage of this function. It consists of the comment-based help, a few parameters, and a small amount of code.

function Clear-UserTemporaryFile {
<#
.SYNOPSIS
    The Clear-UserTemporaryFile command ...
.DESCRIPTION
.PARAMETER <Parameter>
.EXAMPLE
.NOTES
    Name: Clear-UserTemporaryFile
    Author: Tommy Maynard
    Comments: --
    Last Edit: 12/13/2022 [1.0.0]
    Version: 1.0.0
#>
    Param (
        [Parameter()]
        [switch]$ALL,
        [Parameter()]
        [string]$TIF,
        [Parameter()]
        [string]$TMP,
        [Parameter()]
        [string]$SIZE,
        [Parameter()]
        [switch]$HELP
        
    )

    if ($HELP) {
       Get-Help -Name "$($MyInvocation.MyCommand.Name)"
       Exit
    }
}

In the above code, I added a simple if statement. It monitors whether or not the Help parameter is used when the function is invoked. If it is, it runs the Get-Help cmdlet against the name of the executing function—itself—using the $MyInvocation variable and then exits the function.

Clear-UserTemporaryFile -Help
NAME
    Clear-UserTemporaryFile

SYNOPSIS
    The Clear-UserTemporaryFile command ...

SYNTAX
    Clear-UserTemporaryFile [-ALL] [[-TIF] <String>] [[-TMP] <String>] [[-SIZE] <String>] [-HELP] [<CommonParameters>]

DESCRIPTION

RELATED LINKS

REMARKS
    To see the examples, type: "Get-Help Clear-UserTemporaryFile -Examples"
    For more information, type: "Get-Help Clear-UserTemporaryFile -Detailed"
    For technical information, type: "Get-Help Clear-UserTemporaryFile -Full"

At this point, some of you may already know where I’m going to go next. There was something else I had forgotten about every cmdlet and function written. They all have something in common. And what’s that? There is a way to return a command’s help using, of all things, the question mark! Take a look using the built-in Get-Verb and Get-Command cmdlets below.

Get-Verb -?
NAME
    Get-Verb

SYNTAX
    Get-Verb [[-Verb] <string[]>] [[-Group] {Common | Communications | Data | Diagnostic | Lifecycle | Other |
    Security}] [<CommonParameters>]

ALIASES
    None

REMARKS
    Get-Help cannot find the Help files for this cmdlet on this computer. It is displaying only partial help.
        -- To download and install Help files for the module that includes this cmdlet, use Update-Help.
        -- To view the Help topic for this cmdlet online, type: "Get-Help Get-Verb -Online" or
           go to https://go.microsoft.com/fwlink/?LinkID=2097026.
Get-Command -?
NAME
    Get-Command

SYNTAX
    Get-Command [[-ArgumentList] <Object[]>] [-Verb <string[]>] [-Noun <string[]>] [-Module <string[]>]
    [-FullyQualifiedModule <ModuleSpecification[]>] [-TotalCount <int>] [-Syntax] [-ShowCommandInfo] [-All]
    [-ListImported] [-ParameterName <string[]>] [-ParameterType <PSTypeName[]>] [<CommonParameters>]

    Get-Command [[-Name] <string[]>] [[-ArgumentList] <Object[]>] [-Module <string[]>] [-FullyQualifiedModule
    <ModuleSpecification[]>] [-CommandType {Alias | Function | Filter | Cmdlet | ExternalScript | Application | Script
    | Configuration | All}] [-TotalCount <int>] [-Syntax] [-ShowCommandInfo] [-All] [-ListImported] [-ParameterName
    <string[]>] [-ParameterType <PSTypeName[]>] [-UseFuzzyMatching] [-UseAbbreviationExpansion] [<CommonParameters>]

ALIASES
    gcm

REMARKS
    Get-Help cannot find the Help files for this cmdlet on this computer. It is displaying only partial help.
        -- To download and install Help files for the module that includes this cmdlet, use Update-Help.
        -- To view the Help topic for this cmdlet online, type: "Get-Help Get-Command -Online" or
           go to https://go.microsoft.com/fwlink/?LinkID=2096579.

This means I don’t even need a Help parameter. PowerShell already has this covered, and here’s the proof.

Clear-UserTemporaryFile -?
NAME
    Clear-UserTemporaryFile

SYNOPSIS
    The Clear-UserTemporaryFile command ...

SYNTAX
    Clear-UserTemporaryFile [-ALL] [[-TIF] <String>] [[-TMP] <String>] [[-SIZE] <String>] [-HELP] [<CommonParameters>]

DESCRIPTION

RELATED LINKS

REMARKS
    To see the examples, type: "Get-Help Clear-UserTemporaryFile -Examples"
    For more information, type: "Get-Help Clear-UserTemporaryFile -Detailed"
    For technical information, type: "Get-Help Clear-UserTemporaryFile -Full"

Edit: We know that we were able to use this PowerShell to return the help for our function,

Get-Help -Name "$($MyInvocation.MyCommand.Name)"

but we could’ve just as easily done this:

Invoke-Expression -Command "$($MyInvocation.MyCommand.Name) -?"

The first option invokes the Get-Help cmdlet against our function. The second option invokes the same function that’s already being invoked, adding a -? to the end of the complete command.

Minimum and Maximum Array Values

I determined something on Friday that I’m not sure I’ve needed before. I need to know which date in an array of dates, is the most recent—think the maximum value of the values in the array. Before we get to dates, let’s work with some simple-to-understand numeric values first.

In the below example, we’ll create a variable $Numbers and assign it five, out-of-order numerical values. Without much work, it’s easy to visually parse and determine the lowest value (the minimum value), and the highest value (the maximum value). One is the lowest and five is the highest.

$Numbers = 1,3,5,2,4
($Numbers | Measure-Object -Minimum).Minimum
1
($Numbers | Measure-Object -Maximum).Maximum
5

Let’s overwrite $Numbers with a different set of numbers—a larger set of random numbers. It’s not as easy to determine the smallest and largest of the numbers until we sort them.

$Numbers = Get-Random -Minimum 1 -Maximum 100 -Count 25
$Numbers
26
18
42
88
84
85
28
97
19
29
69
29
21
8
70
7
25
45
80
40
81
86
33
4
49
$Numbers | Sort-Object
4
7
8
18
19
21
25
26
28
29
29
33
40
42
45
49
69
70
80
81
84
85
86
88
97

Even though we can sort it, let’s return to Measure-Object and let it do the work for us. In the next two examples, you’ll notice that unlike the previous times I piped our array to Measure-Object, this time I didn’t only return the Minimum and Maximum properties. Instead, I returned all the properties in the object. They’re not all populated, but there’s a way to do that we’ll see soon enough.

$Numbers | Measure-Object -Minimum
Count             : 25
Average           :
Sum               :
Maximum           :
Minimum           : 4
StandardDeviation :
Property          :
$Numbers | Measure-Object -Maximum
Count             : 25
Average           :
Sum               :
Maximum           : 97
Minimum           :
StandardDeviation :
Property          :

Now, let’s try this with dates—both those that include times, and those that don’t. We’ll start by creating a $Date variable that contains five values. Once the values are assigned to our variable, we’ll display the contents of the array stored in our variable.

$Date = [datetime]'1/1/1975 02:05:48 PM',
[datetime]'2/2,1940',
[datetime]'1/1/1975 02:05:48 AM',
[datetime]'3/1/2022',
[datetime]'5/5/2080'
$Date
Wednesday, January 1, 1975 2:05:48 PM
Friday, February 2, 1940 12:00:00 AM
Wednesday, January 1, 1975 2:05:48 AM
Tuesday, March 1, 2022 12:00:00 AM
Sunday, May 5, 2080 12:00:00 AM

Let’s discuss the above dates before we move forward. We have one in 1940—the oldest—and one in 2080—the newest. These are the minimum and maximum, respectively. We have two on the same date in 1975. One happens at 2:05 in the morning and one at 2:05 in the afternoon. Which is the minimum and maximum of those values? You ought to be able to figure it out based on what you now know. Let’s work through some of the previous examples we did with our numerical array with this array. First, however, let’s ensure we’re working with an array—might as well.

$Date.GetType().BaseType.Name
Array

Ta-da. We’ll start by returning the minimum value, and then the maximum.

($Date | Measure-Object -Minimum).Minimum
Friday, February 2, 1940 12:00:00 AM
($Date | Measure-Object -Maximum).Maximum
Sunday, May 5, 2080 12:00:00 AM

Sure enough, 1940 is our minimum, and 2080 is our maximum. What happens when we sort the entire array? Will the two identical dates that include times sort properly? Let’s find out.

$Date | Sort-Object
Friday, February 2, 1940 12:00:00 AM
Wednesday, January 1, 1975 2:05:48 AM
Wednesday, January 1, 1975 2:05:48 PM
Tuesday, March 1, 2022 12:00:00 AM
Sunday, May 5, 2080 12:00:00 AM

They do. Now to put this to use! Perhaps I’ll be back with why I needed this. Before we wrap up though, let’s determine how to return more than just one property value at a time when using Measure-Object. Before that even, what happens when we don’t include any parameters?

$Numbers | Measure-Object
Count             : 25
Average           :
Sum               :
Maximum           :
Minimum           :
StandardDeviation :
Property          :

It only returns the Count property. We can use the AllStats parameter to return (just about) all of the properties at once.

$Numbers | Measure-Object -AllStats
Count             : 25
Average           : 46.52
Sum               : 1163
Maximum           : 97
Minimum           : 4
StandardDeviation : 29.7561198187308
Property          :

Until next time!

A Basic(ish) Active Directory Look-Up Script

It was just Saturday—it’s been a few weeks now, actually—that I wrote a post proclaiming to be back. Back to writing about PowerShell, that is. Why not take the first script I wrote in my new position and share it? It’s possible it has some concepts that might be helpful for readers.

The first thing I want to mention is that I hate writing scripts. Huh!? What I mean by that, is that I prefer to write functions. That didn’t happen my first time out in this position, as you’ll see, and I’m going to be okay with it. A one-off script may have a purpose. I stayed away from a function (and therefore a module [maybe], working with profile scripts [potentially], etc.). I’ll live with it.

Let’s break the script up into sections and explain what’s happening in each. Keep in mind that this isn’t perfect and there are places where I would make changes after having looked over this a couple of times. I’ll be sure to mention those toward the bottom of the post. Okay, let’s do this! I’ll put the script back together into a single, code block further below.

The beginning of the script creates two parameters: Properties and Output. Properties can accept multiple string values,—notice the []—and we’ll see that in the upcoming examples. Output can accept one string value of three predetermined values. Those are Console, GridView, and CsvFile; Console is the default parameter value.

Param (
    [Parameter()]
    [string[]]$Properties,
    [Parameter()]
    [ValidateSet('Console','GridView','CsvFile')]
    [string]$Output = 'Console'
)

Next, we create a path to a flat file called userlist.txt. This file will contain Active Directory Display Names (DisplayName). By using the $PSScriptRoot variable, all we have to do is keep our script and the text file in the same location/folder in order for it to work correctly.

Once the $Path variable is set, we attempt to run a Get-Content command against the values in the file, storing these in the $Userlist variable. If for some reason the file isn’t in place, the script will make use of the catch block of our try-catch to indicate to the user that the file can’t be located.

$Path = "$PSScriptRoot\userlist.txt"
try {
    $Userlist = Get-Content -Path $Path -ErrorAction Stop
} catch {
    Write-Error -Message "Unable to locate $Path."
}

Following that, we set our $TotalProperties variable to three Active Directory user properties we know we want. Then, if any values have been passed in using the Properties parameter, we combine those with the three properties already in the $TotalProperties variable.

$TotalProperties = @('DisplayName','EmployeeNumber','SamAccountName')
if ($Properties) {
    $TotalProperties = $TotalProperties + $Properties
}

Moving forward, we set up foreach language construct in order to loop through each user in the $Userlist variable. Remember, this variable was populated by our Get-Content command earlier. For each loop iteration, we are adding a PSCustomObject to our $Users—plural— variable. Normally, I wouldn’t store each value in a variable and instead just pump it out right there, but the script includes some output options that we’ll see next.

foreach ($User in $Userlist) {
    $Users += [PSCustomObject]@(
    Get-ADUser -Filter "DisplayName -eq '$User'" -Properties $TotalProperties |
        Select-Object -Property $TotalProperties
    )
}

Finally, we consider the output option the user either defaulted to, or didn’t. If they included the Output parameter with GridView, we pipe our $Users variable to OutGridView. If they included the Output parameter with CSVFile, we pipe out $Users variable to Export-Csv, saving them in a CSV file, and then open our saved CSV file. If they didn’t include the Output parameter, or they did with the Console value, then we display the results directly in the console. That’s it.

Switch ($Output) {
    GridView {$Users | Out-GridView -Title Users}
    CsvFile {
        $Users.GetEnumerator() |
        Export-Csv -NoTypeInformation -Path "$PSScriptRoot\$(Get-Date -Format FileDateTime -OutVariable NewFileOutput).csv"
        Invoke-Item -Path "$PSScriptRoot\$NewFileOutput.csv"
    }
    Default {$Users | Format-Table -AutoSize}
}

Although I took the base code from someone’s previously written script, this really is still much of a 1.0.0 version. Knowing that, there are some changes I might make; it’s not perfect, but it’ll get the job done.

  • While it’s not vital, I kind of wish I used a different variable name for $Path
    • It’s a path, sure, but to a specific file
    • Perhaps $FilePath, $UserFile, or $UserFilePath
      • It could’ve been more specific
  • Ensure properties passed in via the Properties parameter are valid for an Active Directory user
    • If someone sends in properties that don’t exist for an Active Directory user, it’s going to cause problems
      • Maybe check a known user, gather all the possible properties, and compare
      • (Or) Maybe wrap some error checking without having to do any property compare operation
  • Don’t use Default in the Switch language construct
    • It’s not necessary, as the Output parameter will only accept three possible values
    • Default could’ve been replaced with Console

Here are a few examples followed by the full PowerShell code in a single, code block.

I’ve redacted information from each of these images. There’s something that’s vital to know about each, however. In front of the full path (all the images but the last one), is the & operator. This is called the invocation or call operator. It informs PowerShell that everything after it should be treated as a command and that it’s not just a long, string value.

This example invokes the script without any parameters, pulling in two users from the userlist.txt file.

This example invokes the script and includes two additional Active Directory properties, which are then also included in the output.

This example does the same as the first one, however, it opens the results using the Out-GridView cmdlet.

This one opens the results using whatever program—Excel in my case—is associated with CSV files. This option is saving the file to disk, so keep that in mind, as it has no cleanup features.

This final example is included to show that it works when you’re inside the same directory as the script and text file. It also includes multiple parameters being included at the same time. You might know you can do it, but my at-work audience may not have—I’m not sure. As we’re in the directory with the script, you can actually see the inclusion of the invocation operator.

And finally, all the code in a single code block.

Param (
    [Parameter()]
    [string[]]$Properties,
    [Parameter()]
    [ValidateSet('Console','GridView','CsvFile')]
    [string]$Output = 'Console'
)

$Path = "$PSScriptRoot\userlist.txt"
try {
    $Userlist = Get-Content -Path $Path -ErrorAction Stop
} catch {
    Write-Error -Message "Unable to locate $Path."
}

$TotalProperties = @('DisplayName','EmployeeNumber','SamAccountName')
if ($Properties) {
    $TotalProperties = $TotalProperties + $Properties
}

foreach ($User in $Userlist) {
    $Users += [PSCustomObject]@(
        Get-ADUser -Filter "DisplayName -eq '$User'" -Properties $TotalProperties |
            Select-Object -Property $TotalProperties
    )
}

Switch ($Output) {
    GridView {$Users | Out-GridView -Title Users}
    CsvFile {
        $Users.GetEnumerator() |
            Export-Csv -NoTypeInformation -Path "$PSScriptRoot\$(Get-Date -Format FileDateTime -OutVariable NewFileOutput).csv"
        Invoke-Item -Path "$PSScriptRoot\$NewFileOutput.csv"
    }
    Default {$Users | Format-Table -AutoSize}
}

Coding Novice, APIs, and PowerShell

I read a recent post on the technical writing subreddit, “How proficient in coding do you have to be to write API Documentation?” I jumped in and posted, as technology is my jam, and writing is my passion.

The author wanted to know if they need to know how to program to make use of an API—an Application Programming Interface. I don’t think so. I have well over 10 years of learning and working with PowerShell, and I don’t think anyone needs that to use an API. Maybe there will be a few things to learn, but not all of it. Why would you even want to focus on one language, as so many can make API calls? I think time would be better spent learning and understanding APIs before learning any one language. The languages the thread’s author mentioned were “HTML, CSS, JavaScript, and/or Python.” I could get some hate for this, but when I learned HTML and CSS I never once thought it was a programming language. I find it strange when people think it is. It’s a formatting language, like markdown. Do this to that, place that there, make this bold, etc. There’s no looping or conditional logic—I just never thought of either of those two that way.

My thought was to teach a bit about APIs with some help—the video linked at the end of this paragraph is great—and then, assume the reader has absolutely no experience with PowerShell. It can easily be installed on any operating system: Windows, macOS, or Linux. Once you have it installed, if you plan to [1], then let’s watch the below video. It is quick and painless and uses an easily understood analogy for APIs, and requests and responses. I’m going to assume that you, as the reader, have watched the video when I begin the next paragraph.

[1] If you’re on Windows, you can use the built-in Windows PowerShell if you’d rather, and not download and install PowerShell (yes, they’re different). If you’re on a Mac you can use the Terminal. If you’re on Linux you can use its built-in terminal, as well. Again, this is if you don’t want to use PowerShell for some strange reason.

Regardless of how you move forward, you now know enough about APIs for now. It’s a way in which you can send a request to, and get a response from, a web service. I absolutely love when I hear that a company is API first. For instance, my understanding is that AWS, or Amazon Web Services (Amazon’s Cloud computing platform), is API first. They build the API, then they build a UI or a website that calls the APIs. Then, they can write PowerShell and other CLI (command-line interfaces) that interact with the exact same API. Everything uses the same backend to get its results. If everything is API-driven, then there are numerous ways to do the same thing, to gather the same information. If there’s an API to start a virtual computer in their cloud, then you could do it from Python, PowerShell, Bash, and from the AWS Management Console—the AWS website. There are probably plenty of other languages that can interface with the exact same API, so why focus on any one single language? This is the point. Expose something in such a way that the language or shell program making the request doesn’t matter.

Before we use PowerShell and the Invoke-RestMethod command, or Bash and the cURL command, or whatever else you’ve chosen, let’s install Postman [2]. It can feel a little overwhelming, even for me, but we’re only going to use a small portion of it. You may have to create an account these days, and that’s perfectly fine to do. Once it’s installed, open it, sign in, etc. and let’s get to the main screen. You’ll probably begin in some default workspace. In my installation of Postman, I created a new workspace just for things related to my site and named it tommymaynard.com.

[2] While I’ve never used it, it appears that there’s a web-based version of Postman. I’m sure it’ll require registration, but it won’t require download and installation. My instructions will be for the installed version, however, I do suspect they’ll work similarly. Your choice.

Toward the top-left should be a plus (+) that allows you to create a collection. Think of a collection as a folder where we can separate like requests. It’s much like Workspaces, as, at minimum, it’s a means of separation, as well.

When given an option to name it, call it “ipify.org.” Once the collection is created, click the greater-than sign (>) to open the collection. There ought to be a message that says, “The collection is empty. Add a request to start working.” Click “Add a request” and rename the request to “IP address.” Now, take the below URL and copy and paste it where it says, “Enter request URL.”

https://api.ipify.org

When that’s in place, click Send on the far right. This will place your public-facing IP address in the lower, response pane. Again, think request (top-pane) and response (lower-pane). We send the waiter in, the waiter works with whatever is back in the kitchen—we don’t care—and then something comes back to us, at the table.

How did I know about this API, right? There are so many of them. There’s probably at least one for everything. At some point, you, like me, might be irritated you actually have to go to some website to find something out. If I could use Postman, or PowerShell, to securely check my bank balance, I would. Same interface for everything, please. I hate navigating 15 totally different websites; everything’s in a different place. It’s so annoying. APIs mean we don’t have to care where someone put something on their website.

I went off on a small tangent there. Anyway, APIs are most often documented, and the one we just used, is no exception. Take a look at the ipfiy.com website. It’s nothing but a shrine to how this API works. When there, scroll down to the “Code samples” section. There are 29 —yes, twenty-nine—different code examples: PowerShell, Python, PHP, Bash, Ruby, NodeJS, Go, C#, and more. Why would anyone learn one language? Postman isn’t even a language; do you even need a language? Postman may be all you need to write API documentation. You don’t have to be a coder to do this.

Let’s move back up on the ipify.org web page and take a look at the API Usage section for IPv4. There’s an option to include a query parameter. See if you can spot what’s different in the below image from what we saw previously. Right-click on our ipify.org collection and choose “Add Request” and see if you can’t rename it to “IP address (JSON).” Once your Postman looks like the below image, hit Send. Your public-facing IP address should appear in the response pane, too.

This request will include a query parameter inside the address. This is notated with the question mark (?), followed by a key-value pair. In our instance, the key-value pair consists of format (the key) and json (the value). When this is run, we return more than just text, but instead, a JSON object. Some systems that might make this request, might prefer it’s already formatted in JSON. It’s a data structure; it’s how—and this is going to sound crazy—we want the data to be structured. Different systems want certain things a certain way and so, if the API allows for it, we can get it back (in our response), the way we want it, without any manipulation on our part. Think about your food in that restaurant. Wouldn’t you prefer the spices are added to the entree while it’s cooking versus you, doing it at the table? It’s more work for you, and it may not turn out as well.

My computer has Windows PowerShell, PowerShell, Git Bash, and using Windows Subsystem for Linux (WSL), Ubuntu as well. In the below images, we’ll run through making use of this API in nearly all of them—we’ll skip Windows PowerShell. Before we take a look at that, however, take a look at this.

It’s over the far-right of Postman. This amazing piece of software just gave you all you need to invoke this API in multiple languages, shells, etc. The cURL command is what can be used in the Mac Terminal or in a Linux Bash shell.

So, let’s try it. Here are a few API requests and responses from my machine. WSL is first, followed by Git Bash, followed by my favorite, PowerShell. The first two don’t use the format query parameter, while the last one does. A note about that after the images, however.

The PowerShell Invoke-RestMethod command was written to take returned JSON objects and automatically convert them into PowerShell objects. Therefore, the second command, ConvertTo-Json, is included to convert it back into JSON. So, that’s what’s happening there.

Why you’d learn an entire language to do this, I have no idea. Learn as much as you need now, and then go back and fill in what you want, and what becomes necessary to know at a later date. And, this is from someone that hates only knowing parts of things. I also understand time constraints and that there’s a better way to get up to speed with APIs than to learn an entire programming language, or two, or a couple of formatting languages. Focus on learning more about APIs for now; there’s plenty more to know than what we covered here.

Accounts Continued – Azure with PowerShell III

Posts in this Series

In the last post, we took a look at the Connect-AzAccount command, stored its output in a variable using the Outvariable parameter and explored it. I think we ought to go through a few of the other commands in the Az.Accounts module and see why we didn’t need to dig in such as we did. As I said previously, it was a good exercise, but it’s almost like the cmdlet developers wanted to provide us with an easier way to gather Azure account-related information. We’ll know more soon enough. First things first, let’s pull back only the Get commands from the Az.Accounts PowerShell module. Again, we’re going to use some formatting commands, but because we only want to view our results on the screen.

Get-Command -Module Az.Accounts -Verb Get | Format-Table -AutoSize
CommandType Name                         Version Source
----------- ----                         ------- ------
Alias       Get-AzDomain                 2.8.0   Az.Accounts
Cmdlet      Get-AzAccessToken            2.8.0   Az.Accounts
Cmdlet      Get-AzConfig                 2.8.0   Az.Accounts
Cmdlet      Get-AzContext                2.8.0   Az.Accounts
Cmdlet      Get-AzContextAutosaveSetting 2.8.0   Az.Accounts
Cmdlet      Get-AzDefault                2.8.0   Az.Accounts
Cmdlet      Get-AzEnvironment            2.8.0   Az.Accounts
Cmdlet      Get-AzSubscription           2.8.0   Az.Accounts
Cmdlet      Get-AzTenant                 2.8.0   Az.Accounts

If you read the last post, then commands such as Get-AzEnvironment and Get-AzContext might seem to make sense here. The nouns in those commands were the two, base properties in our $AzOutput variable. Let’s see what they return and compare it to what we saw in the output variable in the Accounts – Azure with PowerShell II post.

Get-Environment
Name              Resource Manager Url                  ActiveDirectory Authority          Type
----              --------------------                  -------------------------          ----
AzureGermanCloud  https://management.microsoftazure.de/ https://login.microsoftonline.de/  Built-in
AzureCloud        https://management.azure.com/         https://login.microsoftonline.com/ Built-in
AzureUSGovernment https://management.usgovcloudapi.net/ https://login.microsoftonline.us/  Built-in
AzureChinaCloud   https://management.chinacloudapi.cn/  https://login.chinacloudapi.cn/    Built-in

versus

$AzOutput.Environments
Key               Value
---               -----
AzureGermanCloud  AzureGermanCloud
AzureCloud        AzureCloud
AzureUSGovernment AzureUSGovernment
AzureChinaCloud   AzureChinaCloud

It’s clear, that by default the Get-Environment cmdlet returns the information we saw previously. Still, by default, it includes some additional information. While I didn’t show the output in the previous post, I did include $AzOutput.Environments.Values. This will give the same results as above, but let’s use what they’ve provided us. The same goes for using Get-AzContext even though it produces the same exact information as $AzOutput.Context. Whoa, whoa. It doesn’t, we lost the Name property using our output variable. The cmdlets, that we should use, create complete objects; again, let’s stick with these.

Get-AzContext | Format-List
Name               : Free Trial (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) - xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - tommymaynard@xxxxxx
Account            : tommymaynard@xx.xxx
Environment        : AzureCloud
Subscription       : xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Tenant             : xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
TokenCache         :
VersionProfile     :
ExtendedProperties : {}
$AzOutput.Context | Format-List
Name               :
Account            : tommymaynardxxxxxx
Environment        : AzureCloud
Subscription       : xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Tenant             : xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
TokenCache         :
VersionProfile     :
ExtendedProperties : {}

There was more to the Get started with Azure PowerShell document that I overlooked. Let’s head back there. If you’re new to PowerShell then it’s a must that you learn how to find commands using Get-Command and even Get-Module. Also, use Get-Help to explore information about a specific command. These are some of the early-on basics to learn when you’re new to PowerShell. No one knows what every command does; they just know how to find out. A portion of this page covered this topic, too.

One final thing I found on the sign into Azure section of the page, was the UseDeviceAuthentication parameter. Using this, we don’t have to supply a username and corresponding password, but instead, using this parameter generated a device code in PowerShell to use at a specific URL, which is all included in the image below.

Reading that, lead me to the Sign in with Azure PowerShell page. I should’ve known, there are plenty of other ways to authenticate to Azure, besides using the two interactive methods we’ve discussed so far. One is signing in with a service principal using password-based authentication and the other is certificate-based authentication. There’s also signing in using a managed identity. The rest you can explore at your leisure, but we should work through using a service principal using password-based authentication—why not!?

In the password-based authentication link above, the first thing to notice is the need to invoke the New-AzADServicePrincipal command. Before we invoke a command that will most likely make changes, such as a New- command likely would, we want, no we need, to learn more. First, in what module is the command included?

Get-Command -Name New-AzADServicePrincipal | Format-Table -AutoSize
CommandType Name                     Version Source
----------- ----                     ------- ------
Function    New-AzADServicePrincipal 6.0.0   Az.Resources

What’s the purpose of the Azure PowerShell Az.Resources module?

Get-Module -Name Az.Resources | Select-Object -Property Name,Description | Format-List
Name        : Az.Resources
Description : Microsoft Azure PowerShell - Azure Resource Manager and Active Directory cmdlets in Windows PowerShell and PowerShell Core.  Manages subscriptions, tenants, resource groups, deployment templates, providers, and resource permissions in Azure Resource
              Manager.  Provides cmdlets for managing resources generically across resource providers.
              For more information on Resource Manager, please visit the following: https://docs.microsoft.com/azure/azure-resource-manager/
              For more information on Active Directory, please visit the following: https://docs.microsoft.com/azure/active-directory/fundamentals/active-directory-whatis

What does the New-AzADServicePrincipal command do?

Get-Help -Name New-AzADServicePrincipal | Select-Object -Property Name,Synopsis
Name                     Synopsis
----                     --------
New-AzADServicePrincipal Adds new entity to servicePrincipals

Is there a Get- Version of the command (Get-AzADServicePrincipal)?

Get-Command -Name Get-AzADServicePrincipal | Format-Table -AutoSize
CommandType Name                     Version Source
----------- ----                     ------- ------
Function    Get-AzADServicePrincipal 6.0.0   Az.Resources

And if so, what output does it produce?

Get-AzADServicePrincipal | Select-Object -Property AppDisplayName
AppDisplayName
--------------
Office 365 Configure
Azure SQL Managed Instance to Microsoft.Network
Microsoft Graph
Microsoft Modern Contact Master
Azure Resource Graph
Billing RP
Jarvis Transaction Service
AIGraphClient
Microsoft_Azure_Support
Windows Azure Security Resource Provider
Azure SQL Database Backup To Azure Backup Vault
Azure Data Warehouse Polybase
Microsoft.Azure.ActiveDirectoryIUX
Microsoft Azure App Service
Policy Administration Service
Azure Portal
Azure SQL Virtual Network to Network Resource Provider
Azure Classic Portal
Azure Monitor System
Microsoft.SupportTicketSubmission
Azure ESTS Service
Windows Azure Active Directory
Azure Traffic Manager and DNS
Microsoft.Azure.GraphExplorer
Microsoft App Access Panel
Microsoft Azure Signup Portal
Signup
Microsoft.SMIT
Windows Azure Service Management API

Okay, we should feel as though we know a little bit more about our next move. First, however, check out the Azure Portal view of this output. The instructions can be found in the View the service principal subtopic. Sure enough, it looks almost exactly like the above output when we don’t use | Select-Object -Property AppDisplayName, and display the GUIDs.

First, we need to obtain and store our Tenant ID.

$TenantId = (Get-AzContext).Tenant.Id
$TenantId
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

Then we’ll use the New-AzADServicePrincipal command, among a few other commands to create a new Service Principal and get connected to Azure using it.

$ServicePrincipal = New-AzADServicePrincipal -DisplayName SPName
$ServicePrincipal
DisplayName Id                                   AppId
----------- --                                   -----
SPName      xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

After this step, we can go back into the Azure Portal and find a new entry. The command worked!

The $ServicePrincipal variable includes more information than its output displays by default. We should be getting used to that by now. It includes a PasswordCredentials property that includes several other nested properties.

$ServicePrincipal.PasswordCredentials
CustomKeyIdentifier DisplayName EndDateTime          Hint KeyId                                SecretText                               StartDateTime
------------------- ----------- -----------          ---- -----                                ----------                               -------------
                                5/30/2023 1:08:45 AM Qcw  xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx Qcw8Q......oRLla5 5/30/2022 1:08:45 AM

One of those nested properties is SecretText. That is our password and we’re going to use it to create a credential object—the combination of a user and its password.

$ServicePrincipal.PasswordCredentials.SecretText
Qcw8Q......oRLla5

As we’ll see, the username is the AppId’s GUID. When we supply both it and the password, as is done in the below PowerShell, we’ll have our complete PSCredential object.

$PSCredential = Get-Credential -UserName $ServicePrincipal.AppId
PowerShell credential request
Enter your credentials.
Password for user xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx: ****************************************

Using the PSCredential object, we can 100% authenticate to Azure using PowerShell in another manner. We haven’t researched it or discussed it yet—although we should have—but the purpose for why we’d use a service principal should be included. The first paragraph on the Create an Azure service principal with Azure PowerShell page explains it well. Read that. Back with more Azure PowerShell in time!

Connect-AzAccount -ServicePrincipal -Credential $PSCredential -Tenant $TenantId
WARNING: The provided service principal secret will be included in the 'AzureRmContext.json' file found in the user profile ( C:\Users\tommymaynard\.Azure ). Please ensure that this directory has appropriate protections.

Account                              SubscriptionName TenantId                             Environment
-------                              ---------------- --------                             -----------
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx                  xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx AzureCloud

Modifying the New-Guid Cmdlet

I’m deep in the weeds right now writing new content about my experience with Azure and Azure PowerShell. So, what a better time to have an idea, write a quick function, and then create its own post and publish it. Or, maybe not. It’s quick and easy and I think there are people for which this might be good. You can create a function that will run in place of a PowerShell cmdlet, in order to add additional features, take them away, or whatever other reason, or reasons, one might have. That said, and you may notice this momentarily, just because my function of the same name runs instead of the cmdlet, doesn’t mean there isn’t a way to run the original cmdlet.

As one might expect, GUIDs are used all over Azure for identification purposes. I’m running into them everywhere. I don’t want to publish the GUIDs associated with my Tenant, Subscription, etc., so I’ve been replacing mine with this: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. Well, since I use PowerShell to shave off all the seconds I can, I now have a function that will give me an X GUID–we’ll call it–if that’s what I want. We’ll start with an example of it being used, which will then be followed by the code to make that happen. If you’re curious why my New-Guid function runs in place of the cmdlet with the same name from the Microsoft.PowerShell.Utility PowerShell module, then read up on the PowerShell command precedence rules. This is what to remember about the order, however:

  1. Alias
  2. Function
  3. Cmdlet
  4. External executable files (programs and non-PowerShell scripts)
New-Guid

Guid
----
d6dde62e-46fc-4f37-b75a-c116f003a270
New-Guid -x

Guid
----
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

And, here’s the function that makes the above invocations a possibility. I’ll drop it in my profile script and then it’ll be available in my PowerShell session right when I need it, just like New-Guid already was.

function New-Guid {
    [CmdletBinding()]
    Param (
        [Parameter()]
        [switch]$x
    )

    if ($x)
    {
        [PSCustomObject]@{'Guid' = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'}
    }
    else
    {
        Microsoft.PowerShell.Utility\New-Guid
    }
}

If you’re like me, perhaps you just had an idea. Allow it to accept any single character and create the GUID with the character. Okay, back to what I should be doing with this time.