<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://courtneybodett.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://courtneybodett.com/" rel="alternate" type="text/html" /><updated>2026-02-02T16:50:04-08:00</updated><id>https://courtneybodett.com/feed.xml</id><title type="html">Greyout</title><subtitle>My fun with Powershell, and more</subtitle><author><name>Courtney Bodett</name></author><entry><title type="html">Project Mini Rack</title><link href="https://courtneybodett.com/Project_Mini_Rack/" rel="alternate" type="text/html" title="Project Mini Rack" /><published>2026-02-02T00:00:00-08:00</published><updated>2026-02-02T00:00:00-08:00</updated><id>https://courtneybodett.com/Project_Mini_Rack</id><content type="html" xml:base="https://courtneybodett.com/Project_Mini_Rack/"><![CDATA[<h1 id="homelab">Homelab</h1>
<p>Homelab <em>/ ˈhoʊm.læb /</em><br />
<strong>noun</strong></p>
<ol>
  <li>A personal IT environment where individuals can experiment, learn and test various technologies.</li>
  <li>A server (or multiple) in your home where you host applications and virtualized systems for testing and development or for home and personal usage.</li>
</ol>

<hr />

<p>When I look back and think about it it wasn’t long after my first sys admin job that I ended up with a homelab in my place.  There was lots of old hardware are work headed for recycling and it was pretty easy to build something running TrueNAS, PFSense, ESXi, Hyper-V, or just about anything.</p>

<p>I would run virtual machines of Windows Desktop, Windows Server, various Linux distros, deploy software I wanted to learn more about and try things in my own environment to get more comfortable with them.  Things would come and go, but some things stuck around.</p>

<p>Sometime in 2015 I set up Owncloud on a Dell desktop that was destined for the recycler and played around with it at work.  I liked having my own server to host my files on and appreciated the idea of breaking dependency on services from companies like Dropbox and Google.  In 2016 when Nextcloud forked off of Owncloud I rebuilt my little server with a mirrored pair of SSDs and added it to my homelab.</p>

<p>As the homelab changed over the years I eventually did a physical-to-virtual (PvE) conversion on the box and added the VM to a small ESXi host.  I kept postponing any significant changes to the homelab, telling myself that I wanted to get a short height rack to park under my desk at home and then I’d “build something” in it.  Then one day I stumbled in to <a href="'https://reddit.com/r/minilab'">r/minilab</a> and saw some of the really cool looking 10” racks people were building for their homelabs.  Sometime in the summer of 2025 I decided that this was the way forward.  I would build out a new 10” mini rack to run a Proxmox cluster and give me more room for deploying containers and virtual machines for my homelab.</p>

<h1 id="the-plan">The Plan</h1>
<p>Honestly I didn’t really have a plan.  I had not used Proxmox before but I’m familiar with hypervisors.  I kept my eyes on some used marketplaces for computers, specifically shopping for the mini form factor machines.  Ideally I wanted something like the HP computers with their Flex IO ports so I could add a 10Gb NIC to each machine.<br />
I also knew I wanted to use the <a href="'https://deskpi.com/products/deskpi-rackmate-tt-black-rackmount-mini-server-cabinet-for-mini-pc-network-servers-audio-and-video-equipment'">DeskPi Rackmate</a> rack.  Everything beyond that was likely to be some 3d printing with the help of my friend.</p>

<h1 id="plans-are-for-fools">Plans are for fools!</h1>
<p><img src="https://courtneybodett.com/assets/images/plansr4fools.gif" alt="mooninites" /><br />
Plans are great, but sometimes it’s good to “go with the flow” so to speak.  I responded to an add for some used HP g5 minis because I wanted to use the HP Flex IO ports to get an additional network card on the machine.  I wanted to make a hyperconverged Proxmox cluster by leveraging Ceph over a separate network.  However, the seller responded that they were actually G3s instead.  After some thought I decided I could just use some m.2 cards to add a 2.5Gb NIC to each machine, and at less than $300 shipped for 3x computers, the project had started.</p>

<p>Now I had 3x HP 600 G3 Desktop Minis with 11th Gen Intel Core i5 processors, 16GB of RAM and a 256GB NVME drive installed.  I added <a href="'https://www.amazon.com/dp/B0CKT3XXTT?ref=ppx_yo2ov_dt_b_fed_asin_title&amp;th=1'">these network cards</a> to each one and a 1TB 2.5 SATA SSD as well.  The SSDs were parts that were around.  Ordered the rack, and also a <a href="'https://www.amazon.com/dp/B0CPN6QSBF?th=1'">Tripp Lite</a> UPS that someone else had used on r/minilab with this rack so I knew it would fit in the bottom.  Added a small <a href="'https://www.amazon.com/dp/B0FKFNGTCT?th=1'">PDU</a> to give myself more ports in the back and the rack was off to a good start.<br />
<img src="https://courtneybodett.com/assets/images/Minilab_01.jpeg" alt="rack_pdu" /><br />
Here is the initial install of the m.2 network cards in place of the factory wifi cards.<br />
<img src="https://courtneybodett.com/assets/images/Minilab_02.jpeg" alt="networkcard" /><br />
Also seen in the background are the network switches I decided to go with: A 5-port Unifi 1Gb switch, <a href="'https://store.ui.com/us/en/products/usw-flex-mini'">Flex Mini</a> and its slightly bigger brother the 5-port Unifi 2.5Gb switch, <a href="'https://store.ui.com/us/en/products/usw-flex-2-5g-5'">Flex Mini 2.5G</a>.</p>

<h1 id="3d-printer-time">3d printer time</h1>
<p>Very much with the help of my friend I was able to get some stuff 3d printed to rack mount these components.  Here’s a bit of a breakdown:</p>
<ul>
  <li><a href="'https://www.printables.com/model/1334837-tinyminimicro-10-rack-mount'">Computer mount</a></li>
  <li><a href="'https://www.printables.com/model/1025246-ubiquiti-unifi-usw-flex-mini-25g-5-10-inch-rack-mo'">2.5Gb switch mount</a></li>
  <li><a href="'https://www.printables.com/model/1210493-10-inch-rack-unifi-usw-flex-mini'">Mini Flex mount</a></li>
</ul>

<p>This would leave me with some unused space at the bottom of the rack, so we decided to come up with a vent plate featuring my logo to fill the space.  Here was some final assembly on the computer mounts.<br />
<img src="https://courtneybodett.com/assets/images/Minilab_03.jpeg" alt="assembly1" /><br />
<img src="https://courtneybodett.com/assets/images/Minilab_04.jpeg" alt="assembly2" /><br />
Some test assembly for fitment.<br />
<img src="https://courtneybodett.com/assets/images/Minilab_05.jpeg" alt="assembly3" /><br />
And then the back…we don’t look at the back.<br />
<img src="https://courtneybodett.com/assets/images/Minilab_06.jpeg" alt="assembly4" /></p>

<p>To finish off the filler plate at the bottom I added some <a href="'https://www.amazon.com/dp/B07R4WWX4W?th=1'">wire mesh</a> I found on Amazon that was the perfect fit.<br />
Final fitment test with cabling.  Notice that the 1Gb switch got flipped when we realized it was upside down.  All the keystone jacks are installed as well.<br />
<img src="https://courtneybodett.com/assets/images/Minilab_07.jpeg" alt="assembly5" /></p>
<h1 id="time-to-deploy">Time to deploy</h1>
<p>One of the reasons I went with a 10” mini rack setup was so that it could sit on top of my desk instead of under it.<br />
<img src="https://courtneybodett.com/assets/images/Minilab_08.jpeg" alt="assembly6" /><br />
The red cable is connecting the 1Gb switch back to the rest of my network, and the additional blue cable that’s next to it is feeding in to my Pi-hole sitting on top of the rack.</p>

<p>Each computer got a fresh install of Proxmox v9.1.1 on the internal 256GB NVME drive.  I had some problems with the 2.5Gb NICs coming up.  Ultimately I removed the 2.5” drive trays so the ribbon cable wasn’t getting smooshed and that seemed to fix it for each computer.  Those computer mounts were pretty handy for all of that.</p>

<p>After getting a Proxmox cluster set up I installed ceph on each node, created an OSD on each one, and pretty soon I had an Ubuntu test virtual machine deployed on ceph storage.  I had to travel around the holidays and had to step away from the homelab at this point.  When I came back and signed in I had a nice warning letting me know that the 2.5” SATA drive in node 1 had failed.  Smartctl confirmed.<br />
The culprit:<br />
<img src="https://courtneybodett.com/assets/images/Minilab_09.jpeg" alt="assembly7" /><br />
Thankfully I had a 1TB Crucial SSD in my desktop being horribly underutilized for backups.  I swapped a 256GB drive I had laying around into the desktop and moved the 1TB drive in to node 1.  Created a new ceph OSD on node 1 and within a minute or so replication had repopulated the data on to the new drive.<br />
The Ubuntu VM was still able to boot and didn’t appear any worse for the wear.</p>

<p>Now that I had some time post holidays I got SMTP notifications set up, configured Network UPS Tools (NUT) on each node to make them UPS aware via a NUT server running on my Synology, and tested failover and UPS settings.<br />
As a test I tried migrating an existing VM from my ESXi host to the new Proxmox cluster and it went pretty smoothly.  However, for the other things left on my ESXi host I decided to deploy those services as Docker containers in the new rack.</p>

<p>Now I have over a dozen containers running in the homelab with services like Immich, qBittorrent, Nginx Proxy Manager and most importantly Nextcloud.  The one remaining constraint seems to be memory.  Ceph uses a decent amount of memory on it’s own. Each node idles at about 4GB of RAM with nothing else going on.  A single Debian VM with all of the containers on it puts a node at about 80% RAM utilization.  Ordinarily I would just upgrade each one of these nodes to 24GB or 32GB of RAM, but with the current pricing it’s not a priority.</p>
<h1 id="conclusion">Conclusion</h1>
<p>This whole project took a handful of months, which was helpful for spreading the cost out.  Just about anybody could pick up a used computer at this point and be on their way to a totally viable homelab.  The mini rack form factor is nice for space saving and if you’ve got access to a 3d printer the sky is the limit it seems like.</p>]]></content><author><name>Courtney Bodett</name></author><summary type="html"><![CDATA[Homelab Homelab / ˈhoʊm.læb / noun A personal IT environment where individuals can experiment, learn and test various technologies. A server (or multiple) in your home where you host applications and virtualized systems for testing and development or for home and personal usage.]]></summary></entry><entry><title type="html">Deniable Encryption</title><link href="https://courtneybodett.com/Deniable_Encryption/" rel="alternate" type="text/html" title="Deniable Encryption" /><published>2025-12-30T00:00:00-08:00</published><updated>2025-12-30T00:00:00-08:00</updated><id>https://courtneybodett.com/Deniable_Encryption</id><content type="html" xml:base="https://courtneybodett.com/Deniable_Encryption/"><![CDATA[<p>I remember years ago when I first made a bootable Kali thumb drive (‘BackTrack’ back then) with encrypted peristence there was an option to create a sort of “self destruct” where if you typed a special password the drive would be erased rather than unlocked.  Kali still offers this package: <a href="https://www.kali.org/tools/cryptsetup-nuke-password/">cryptsetup-nuke-password</a>.<br />
For some reason this ability still fascinates me.  The option to self desctruct your stuff at a moment’s notice.  I’ve always used Luks encryption on Linux devices but this specific “nuke password” setup doesn’t seem to exist much outside of Kali.<br />
I continued reading and doing some experimenting on an older laptop and eventually decided to set my Framework 13 laptop up with <em>deniable encryption</em>.</p>

<ul id="markdown-toc">
  <li><a href="#what-is-deniable-encryption" id="markdown-toc-what-is-deniable-encryption">What Is “Deniable Encryption”</a></li>
  <li><a href="#why-do-this" id="markdown-toc-why-do-this">Why Do This?</a></li>
  <li><a href="#i-use-arch-btw" id="markdown-toc-i-use-arch-btw">I use Arch btw…</a></li>
  <li><a href="#boot-arch-iso" id="markdown-toc-boot-arch-iso">Boot Arch ISO</a></li>
  <li><a href="#disk-setup" id="markdown-toc-disk-setup">Disk Setup</a>    <ul>
      <li><a href="#partition-disks" id="markdown-toc-partition-disks">Partition Disks</a></li>
      <li><a href="#shred-it" id="markdown-toc-shred-it">Shred It</a></li>
      <li><a href="#secure-it" id="markdown-toc-secure-it">Secure It</a></li>
      <li><a href="#format-the-partitions" id="markdown-toc-format-the-partitions">Format The Partitions</a>        <ul>
          <li><a href="#mount-partitions" id="markdown-toc-mount-partitions">Mount Partitions</a></li>
        </ul>
      </li>
    </ul>
  </li>
  <li><a href="#install-arch" id="markdown-toc-install-arch">Install Arch</a>    <ul>
      <li><a href="#pacstrap-and-chroot" id="markdown-toc-pacstrap-and-chroot">Pacstrap and Chroot</a></li>
      <li><a href="#create-user" id="markdown-toc-create-user">Create User</a></li>
      <li><a href="#locale-time-and-hostname-oh-my" id="markdown-toc-locale-time-and-hostname-oh-my">Locale, Time and Hostname Oh My</a></li>
      <li><a href="#mkinitcpio-and-boot" id="markdown-toc-mkinitcpio-and-boot">mkinitcpio and boot</a></li>
      <li><a href="#exit-and-reboot" id="markdown-toc-exit-and-reboot">Exit and Reboot</a></li>
    </ul>
  </li>
  <li><a href="#backup-the-luks-header" id="markdown-toc-backup-the-luks-header">Backup the Luks Header</a></li>
</ul>

<h1 id="what-is-deniable-encryption">What Is “Deniable Encryption”</h1>
<p>Ok, but what exactlyl does “deniable encryption” mean, and where’s my self destruct button?  <a href="https://en.wikipedia.org/wiki/Deniable_encryption">Wikipedia</a> has a decent page describing what deniable encryption means.  Basically for my context this means if anyone were to inspect the laptop or the drive itself there would be no evidence that the drive contained any data or even any encrypted data.<br />
It turns out that with Luks encryption, by default, the first 16MB of a targeted partition for a Luks container is used to store the Luks header and the keyslots for unlocking the Luks container.  The “nuke password” option from Kali operates by deleting the keys from the Luks header.  This is unrecoverable and basically turns all of your encrypted data into pure entropy.  However, the Luks header still exists and by its existence implies that there <em>was</em> encrypted data there.<br />
Luks has the option to work with a detached header, whereby you create the header and store it somewhere else.  This could be a file, or another disk partition.  I experimented with putting both the boot partition and the Luks header on a removable USB drive and using the entire internal drive as a Luks container.  With the USB drive removed there is no bootable partition, and the internal drive looks like random data from the first bit to the end.</p>

<h1 id="why-do-this">Why Do This?</h1>
<p>Mostly for fun and because I like to “try” things in order to learn them.  My threat model does not include needing to be able to “nuke” my laptop setup and have plausible deniability about whether or not there was ever any data on it.  I’m also going to use this post as a bit of a guide for myself for installing Arch Linux.  After the disk and Luks setup, the rest of the steps are pretty much the same.</p>

<h1 id="i-use-arch-btw">I use Arch btw…</h1>
<p>I have mostly used Ubuntu since first getting in to Linux and had only really heard of Arch, but never seen it.  It’s been over 2 years since I first tried Arch and I’ve come to really enjoy it as a distro.  It makes you responsible for every aspect of the computer and puts you in control of it all, for better or for worse.  I’ve learned a lot more about Linux in general and the Arch Wiki has become my go-to resource for pretty much all Linux questions.</p>

<h1 id="boot-arch-iso">Boot Arch ISO</h1>
<p>The first step is to boot the Arch Linux ISO.  It’s always best to go out and get the most current one from Arch as they publish a new version every month.  I’ve been using <a href="https://www.ventoy.net/en/index.html">Ventoy</a> for a while to consolidate dozens of different ISOs on to one physical media.  With a new version of the Arch ISO copied over to my Ventoy drive I’ll boot my Framework 13 and mash F12 to get the one-time boot menu.  Select my Ventoy drive, and from the menu select the Arch ISO.</p>

<h1 id="disk-setup">Disk Setup</h1>
<p>Once the Arch installer finishes booting the first step is to identify the disks we’ll be working with.  In my case I’ve got an internal NVME drive and a Framework 250GB expansion card occupying one of the expansion card slots.  This registers as a USB drive and will end up containing the boot partition as well as the detached Luks header.<br />
Using the <code class="language-plaintext highlighter-rouge">lsblk</code> command to list the current disks:</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh">root@archiso ~ <span class="c"># lsblk</span>
NAME       MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
loop0        7:0    0 971.6M  1 loop /run/archiso/airootfs
sda          8:0    0 232.9G  0 disk
sdb          8:16   1 119.5G  0 disk
├─sdb1       8:17   1 119.5G  0 part
│ └─ventoy 253:0    0   1.4G  1 dm
└─sdb2       8:18   1    32M  0 part
nvme0n1    259:0    0 465.8G  0 disk
root@archiso ~ <span class="c">#</span></code></pre></figure>

<p>In this example <code class="language-plaintext highlighter-rouge">/dev/sda</code> is the expansion drive (250GB) and <code class="language-plaintext highlighter-rouge">/dev/nvme0n1</code> is the internal NVME drive.  <code class="language-plaintext highlighter-rouge">/dev/sdb</code> is the USB drive for Ventoy that Arch is booted off of.</p>
<h2 id="partition-disks">Partition Disks</h2>
<p>Start with the expansion/removable drive.</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh">fdisk /dev/sda</code></pre></figure>

<p>Then <code class="language-plaintext highlighter-rouge">g</code> to start an empty GPT style partition table.<br />
<code class="language-plaintext highlighter-rouge">n</code> to start a new partition, default number and first sector. For size set it to <code class="language-plaintext highlighter-rouge">+1GB</code> to make it a 1GB partition for boot.<br />
<code class="language-plaintext highlighter-rouge">n</code> again to a new partition, default number (2) and first sector. For size set it to <code class="language-plaintext highlighter-rouge">+17MB</code>.  This will be where the detached Luks header is stored.
<code class="language-plaintext highlighter-rouge">n</code> one last time to start a third partition with default first and last sector.  It’s a 250GB drive, figure it’d be nice to be able to use that space for something.<br />
Lastly, need to set the type for each partition with the <code class="language-plaintext highlighter-rouge">t</code> command, and choose the following values.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Partition 1 = '1' for an EFI partition.
Partition 2 = '23' for a "Linux root (x86-64)" type
Partition 3 = '23' for a "Linux root (x86-64)" type
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">w</code> to write the changes to disk.  At this point you could create a partition on the NVME drive, but I’ve found that you can omit this step and just target the whole drive with cryptsetup.  In this way, there isn’t even a partition listed when looking at the drive.<br />
Otherwise I’d create an empty GPT partition table on the NVME drive, create 1 partition with default first and last sectors and type <code class="language-plaintext highlighter-rouge">23</code>.</p>
<h2 id="shred-it">Shred It</h2>
<p>Plausibly deniable encryption wouldn’t quite work if we took a blank disk and started a Luks container on it.  The encrypted data only occupies so many blocks on the device and the rest is zeros.  A sufficiently skilled adversary might be able to determine certain things about the computer based presence, size and location of the used and unused blocks.  It’s going to take a bit of time but I’m going to run <code class="language-plaintext highlighter-rouge">shred</code> against the drive first to overwrite every bit position with random data.</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh">root@archiso ~ <span class="c"># shred -n 1 /dev/nvme0n1 -v</span></code></pre></figure>

<p>This will make one pass of random data to the entire NVME drive with the <code class="language-plaintext highlighter-rouge">-v</code> providing verbose output.</p>
<h2 id="secure-it">Secure It</h2>
<p>Now the actual disk encryption can happen.</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh">root@archiso ~ <span class="c"># cryptsetup luksFormat --use-random -h sha512 /dev/nvme0n1 --header=/dev/sda2</span>

WARNING!
<span class="o">========</span>
This will overwrite data on /dev/sda2 irrevocably.

Are you sure? <span class="o">(</span>Type <span class="s1">'yes'</span> <span class="k">in </span>capital letters<span class="o">)</span>: YES
Enter passphrase <span class="k">for</span> /dev/sda2:
Verify passphrase:
cryptsetup luksFormat <span class="nt">--use-random</span> <span class="nt">-h</span> sha512 /dev/nvme0n1 <span class="nt">--header</span><span class="o">=</span>/dev/sda2  24.77s user 0.35s system 130% cpu 19.216 total</code></pre></figure>

<p>Here the <code class="language-plaintext highlighter-rouge">--use-random</code> specifies which random number generator to use.
<code class="language-plaintext highlighter-rouge">-h sha512</code> is the hash algorithm to be used and the iterations is left at default.<br />
Then the target drive/partition location followed by the location for the detached header with <code class="language-plaintext highlighter-rouge">--header=/dev/sda2</code>.</p>

<p>Next we’ll open the encrypted container</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh">root@archiso ~ <span class="c"># cryptsetup luksOpen /dev/nvme0n1 crypt --header=/dev/sda2</span>
Enter passphrase <span class="k">for</span> /dev/nvme0n1:
cryptsetup luksOpen /dev/nvme0n1 crypt <span class="nt">--header</span><span class="o">=</span>/dev/sda2  7.24s user 0.07s system 134% cpu 5.450 total</code></pre></figure>

<p>Now <code class="language-plaintext highlighter-rouge">lsblk</code> will confirm we have a new partition representing our unlocked Luks container.</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh">root@archiso ~ <span class="c"># lsblk</span>
NAME       MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS
loop0        7:0    0 971.6M  1 loop  /run/archiso/airootfs
sda          8:0    0 232.9G  0 disk
├─sda1       8:1    0     1G  0 part
├─sda2       8:2    0    17M  0 part
└─sda3       8:3    0 231.9G  0 part
sdb          8:16   1 119.5G  0 disk
├─sdb1       8:17   1 119.5G  0 part
│ └─ventoy 253:0    0   1.4G  1 dm
└─sdb2       8:18   1    32M  0 part
nvme0n1    259:0    0 465.8G  0 disk
└─crypt    253:1    0 465.8G  0 crypt
root@archiso ~ <span class="c">#</span></code></pre></figure>

<h2 id="format-the-partitions">Format The Partitions</h2>
<p>Next up we need file systems on these partitions so we can use them.<br />
<code class="language-plaintext highlighter-rouge">mkfs.fat -F32 /dev/sda1</code> to turn the 1GB partition on our external drive in to a FAT32 boot partition.<br />
<code class="language-plaintext highlighter-rouge">mkfs.ext4 /dev/mapper/crypt</code> to turn our unlocked Luks container in to an Ext4 partition to hold our Arch Linux install.</p>
<h3 id="mount-partitions">Mount Partitions</h3>
<p>Run these commands to mount the boot and root partitions within the live Arch ISO environment.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mount /dev/mapper/crypt /mnt
<span class="nb">mkdir</span> <span class="nt">-p</span> /mnt/boot
mount /dev/sda1 /mnt/boot
</code></pre></div></div>
<h1 id="install-arch">Install Arch</h1>
<p>Parts of this will be slightly different due to the detached Luks header encryption setup, but we’ll carry on as if this is a normal install.</p>
<h2 id="pacstrap-and-chroot">Pacstrap and Chroot</h2>
<p>Use pacstrap to load some initial packages in to the new blank disk and then arch-chroot for further steps.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pacstrap /mnt base base-devel linux linux-firmware intel-ucode git vim <span class="nb">sudo </span>networkmanager
</code></pre></div></div>
<p>Generate fstab.<br />
<code class="language-plaintext highlighter-rouge">genfstab -U /mnt &gt;&gt; /mnt/etc/fstab</code><br />
chroot into system.
` arch-chroot /mnt`</p>
<h2 id="create-user">Create User</h2>
<p>Create a user account for the new installation.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>useradd <span class="nt">-m</span> greyed
passwd greyed
</code></pre></div></div>
<p>Add the user to the wheel group, which will determine who has access to sudo.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gpasswd <span class="nt">-a</span> greyed wheel
</code></pre></div></div>
<p>Run visudo, which will open a file where you can add wheel users to sudoers. Uncomment this relevant section:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">## Uncomment to allow members of group wheel to execute any command</span>
%wheel <span class="nv">ALL</span><span class="o">=(</span>ALL<span class="o">)</span> ALL
</code></pre></div></div>
<p>If you want to increase the time period a sudo auth is good for and change the default editor for sudo now is a good time to add these to visudo as well.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Defaults    <span class="nv">timestamp_timeout</span><span class="o">=</span>5
Defaults    <span class="nv">editor</span><span class="o">=</span>/usr/bin/vim
</code></pre></div></div>
<h2 id="locale-time-and-hostname-oh-my">Locale, Time and Hostname Oh My</h2>
<p>Set time locale (choose a relevant locale):</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">ln</span> <span class="nt">-sf</span> /usr/share/zoneinfo/US/Pacific /etc/localtime
</code></pre></div></div>

<p>Set clock:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>hwclock <span class="nt">--systohc</span>
</code></pre></div></div>

<p>Uncomment <code class="language-plaintext highlighter-rouge">en_US.UTF-8 UTF-8</code> <code class="language-plaintext highlighter-rouge">en_US ISO-8859-1</code> or whatever localizations you need in <code class="language-plaintext highlighter-rouge">/etc/locale.gen</code>. Now run:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>locale-gen
</code></pre></div></div>

<p>Create locale config file:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>locale <span class="o">&gt;</span> /etc/locale.conf
</code></pre></div></div>

<p>Set the lang variable in the above file (Choose the language code that is relevant to you):</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">LANG</span><span class="o">=</span>en_US.UTF-8
<span class="nv">LANGUAGE</span><span class="o">=</span>en_US
</code></pre></div></div>

<p>Add an hostname (any hostname of your choice as one line in the file. eg. <code class="language-plaintext highlighter-rouge">myhostname</code>):</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vim /etc/hostname
</code></pre></div></div>
<p>Update <code class="language-plaintext highlighter-rouge">/etc/hosts</code> to contain (replace <code class="language-plaintext highlighter-rouge">myhostname</code> with the host name you used above):</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>127.0.1.1   myhostname.localdomain  myhostname
</code></pre></div></div>
<h2 id="mkinitcpio-and-boot">mkinitcpio and boot</h2>
<p>Edit mkinitcpio.conf to add the right kernel hooks for unlocking the disk with a detached header</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HOOKS=(base systemd autodetect modconf kms keyboard sd-vconsole plymouth sd-encrypt block filesystems fsck)
</code></pre></div></div>
<p>Note this has ‘plymouth’ included for a no splash boot, but it can be added later.</p>

<p>Then create the file /etc/crypttab.initramfs to specify that my detached header was on a separate drive.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>crypt /dev/nvme0n1 none header=/dev/disk/by-partuuid/15f1a4e5-940b-4a14-b48d-c7af015597d6
</code></pre></div></div>
<p>where “crypt” is the name of the mapped decrypted Luks container. The next argument is the location of the Luks container, and then the header is specified using the partition UUID.  The <code class="language-plaintext highlighter-rouge">blkid</code> command can return can return the details for a specific drive</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>blkid /dev/sda2
/dev/sda2: UUID="f818c840-81ae-4085-a7c1-d44bf62833f1" TYPE="crypto_LUKS" PARTUUID="15f1a4e5-940b-4a14-b48d-c7af015597d6"
</code></pre></div></div>
<p>Then you can use that PARTUUID in the crypttab file</p>

<p>Regenerate the initramfs:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mkinitcpio <span class="nt">-p</span> linux
</code></pre></div></div>

<p>Install a bootloader:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bootctl <span class="nt">--path</span><span class="o">=</span>/boot/ <span class="nb">install</span>
</code></pre></div></div>

<p>Create bootloader. Edit <code class="language-plaintext highlighter-rouge">/boot/loader/loader.conf</code>. Replace the file’s contents with:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>default arch
timeout 0
</code></pre></div></div>
<p>Now to make the loader entry to support booting this detached Luks header setup
file at /boot/loader/entries/arch.conf</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>title Arch Linux
version 6.18.2
linux /vmlinuz-linux
initrd /intel-ucode.img
initrd /initramfs-linux.img
options cryptdevice=/dev/nvme0n1:crypt root=/dev/mapper/crypt rw quiet splash

</code></pre></div></div>

<p>The cryptdevice should normally be specified by partition UUID but because we targeted the entire NVME drive, there is no partition UUID.  This is followed by the name of the opened Luks container, how it will be mapped and then the final <code class="language-plaintext highlighter-rouge">quiet splash</code> parts are for a Plymouth setup.</p>

<p>A final check of the <code class="language-plaintext highlighter-rouge">/etc/fstab</code> file to make sure it’s using the correct UUIDs</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Static information about the filesystems.
# See fstab(5) for details.

# &lt;file system&gt; &lt;dir&gt; &lt;type&gt; &lt;options&gt; &lt;dump&gt; &lt;pass&gt;
# /dev/mapper/crypt
UUID=a4d5cd3a-7edc-454d-8830-990c75df61b1   /           ext4        rw,relatime 0 1

# /dev/sda1
PARTUUID=5fcda863-68ff-4e36-8955-4bf32e8bc667    /boot       vfat        rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,utf8,errors=remount-ro   0 2
</code></pre></div></div>
<p>the first is the UUID, from blkid, of the /dev/mapper/crypt disk and then the boot partition referenced as a partition UUID since it might change otherwise at every boot.  This also allows us to have a clone of the removable drive as a backup.  More on this later.</p>
<h2 id="exit-and-reboot">Exit and Reboot</h2>
<p>Exit the arch-chroot
<code class="language-plaintext highlighter-rouge">exit</code>
and unmount the drives<br />
<code class="language-plaintext highlighter-rouge">umount -R /mnt</code></p>

<p>after first login, setup Network Manager</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>systemctl start NetworkManager
systemctl <span class="nb">enable </span>NetworkManager
</code></pre></div></div>
<p>Voila! that’s a basic Arch setup.</p>
<h1 id="backup-the-luks-header">Backup the Luks Header</h1>
<p>As discussed, without the Luks header the data on the encrypted drive is completely unrecoverable.  If the external drive that our detatched Luks header was on failed, went missing, or got destroyed we would have no other option but to start from scratch.  The Luks header can be backed up to a file at any time with a syntax like this.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">sudo </span>cryptsetup luksHeaderBackup /dev/nvme0n1 <span class="nt">--header-backup-file</span> headerbackup.img
</code></pre></div></div>
<p>Then that file can be stored safely somewher else.<br />
Additionally every time the kernel is updated the boot partition will get a new initramfs.  If we only cloned the Luks header to a new, similar drive this ‘backup’ drive would eventually end up being unbootable as the kernel version deviated from the rest of the system.  Running <code class="language-plaintext highlighter-rouge">dd</code> every so often on a 250GB drive would be tedious, error prone, and time consuming.<br />
I wrote a small bash script that simplifies this for me.  I can plug in my other Framework 250GB expansion drive and run this script to keep a functional backup for myself.</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh"><span class="c">#!/bin/bash</span>
<span class="c">#for cloning the current boot disk to a target disk containing the same partitions</span>


<span class="c"># Must be run as root</span>
<span class="k">if</span> <span class="o">[[</span> <span class="nv">$EUID</span> <span class="nt">-ne</span> 0 <span class="o">]]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"This script must be run as root (sudo)."</span> 1&gt;&amp;2
    <span class="nb">exit </span>1
<span class="k">fi

</span><span class="nv">boot_dev</span><span class="o">=</span><span class="si">$(</span>findmnt <span class="nt">-no</span> SOURCE /boot<span class="si">)</span>
<span class="nv">boot_disk</span><span class="o">=</span><span class="si">$(</span>lsblk <span class="nt">-no</span> PKNAME <span class="s2">"</span><span class="nv">$boot_dev</span><span class="s2">"</span><span class="si">)</span>
<span class="nv">source_disk</span><span class="o">=</span><span class="s2">"/dev/</span><span class="nv">$boot_disk</span><span class="s2">"</span>
<span class="nb">echo</span> <span class="s2">"Source Disk: </span><span class="nv">$source_disk</span><span class="s2">"</span>
<span class="nv">UUID</span><span class="o">=</span><span class="si">$(</span>blkid <span class="nt">-s</span> UUID <span class="nt">-o</span> value <span class="s2">"</span><span class="nv">$boot_dev</span><span class="s2">"</span><span class="si">)</span>
<span class="nv">duplicate</span><span class="o">=</span><span class="si">$(</span>blkid <span class="nt">-s</span> UUID | <span class="nb">grep</span> <span class="s2">"</span><span class="nv">$UUID</span><span class="s2">"</span> | <span class="nb">grep</span> <span class="nt">-v</span> <span class="s2">"^</span><span class="nv">$boot_dev</span><span class="s2">"</span><span class="si">)</span>

<span class="k">if</span> <span class="o">[[</span> <span class="nt">-n</span> <span class="nv">$duplicate</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"Target disk found:"</span>
    <span class="nb">echo</span> <span class="s2">"    </span><span class="nv">$duplicate</span><span class="s2">"</span>
    <span class="nv">clone_dev</span><span class="o">=</span><span class="si">$(</span>blkid <span class="nt">-s</span> UUID | <span class="nb">grep</span> <span class="s2">"</span><span class="nv">$UUID</span><span class="s2">"</span> | <span class="nb">grep</span> <span class="nt">-v</span> <span class="s2">"^</span><span class="nv">$boot_dev</span><span class="s2">"</span> | <span class="nb">awk</span> <span class="nt">-F</span>: <span class="s1">'{print $1}'</span><span class="si">)</span>
    <span class="nv">clone_disk</span><span class="o">=</span><span class="si">$(</span>lsblk <span class="nt">-no</span> PKNAME <span class="s2">"</span><span class="nv">$clone_dev</span><span class="s2">"</span><span class="si">)</span>
    <span class="nv">target_disk</span><span class="o">=</span><span class="s2">"/dev/</span><span class="nv">$clone_disk</span><span class="s2">"</span>
    <span class="nb">echo</span> <span class="s2">"Source: </span><span class="nv">$source_disk</span><span class="s2">"</span>
    <span class="nb">echo</span> <span class="s2">"Target: </span><span class="nv">$target_disk</span><span class="s2">"</span>
    <span class="nb">echo</span> <span class="s2">"---------------------"</span>
    <span class="nb">read</span> <span class="nt">-p</span> <span class="s2">"Check kernel versions before cloning? (yes/no): "</span> answer1

    <span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$answer1</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">"yes"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
        </span><span class="nv">mntdir</span><span class="o">=</span><span class="s2">"/mnt/targetboot"</span>
        <span class="nb">mkdir</span> <span class="nv">$mntdir</span>
        mount <span class="s2">"</span><span class="k">${</span><span class="nv">target_disk</span><span class="k">}</span><span class="s2">1"</span> <span class="nv">$mntdir</span>
        <span class="nv">source_ver</span><span class="o">=</span><span class="si">$(</span>lsinitcpio /boot/initramfs-linux.img <span class="nt">-a</span> | <span class="nb">grep</span> <span class="s1">'Kernel'</span> | <span class="nb">awk</span> <span class="nt">-F</span>: <span class="s1">'{print $2}'</span><span class="si">)</span>
        <span class="nv">target_ver</span><span class="o">=</span><span class="si">$(</span>lsinitcpio <span class="s2">"</span><span class="k">${</span><span class="nv">mntdir</span><span class="k">}</span><span class="s2">/initramfs-linux.img"</span> <span class="nt">-a</span> | <span class="nb">grep</span> <span class="s1">'Kernel'</span> | <span class="nb">awk</span> <span class="nt">-F</span>: <span class="s1">'{print $2}'</span><span class="si">)</span>
        umount <span class="nv">$mntdir</span>
        <span class="nb">rmdir</span> <span class="nv">$mntdir</span>
        <span class="nb">echo</span> <span class="s2">"Source Kernel Version:</span><span class="nv">$source_ver</span><span class="s2">"</span>
        <span class="nb">echo</span> <span class="s2">"Target Kernel Version:</span><span class="nv">$target_ver</span><span class="s2">"</span>
        <span class="nb">echo</span> <span class="s2">"---------------------"</span>
    <span class="k">fi

    if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$source_disk</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$target_disk</span><span class="s2">"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
        </span><span class="nb">echo</span> <span class="s2">"Error: source and target disks are the same!"</span>
        <span class="nb">exit </span>1
    <span class="k">fi

    </span><span class="nb">read</span> <span class="nt">-p</span> <span class="s2">"This will completely overwrite </span><span class="nv">$target_disk</span><span class="s2">. Continue? (yes/no): "</span> answer2

    <span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$answer2</span><span class="s2">"</span> <span class="o">!=</span> <span class="s2">"yes"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
        </span><span class="nb">echo</span> <span class="s2">"Clone aborted."</span>
        <span class="nb">exit </span>1
    <span class="k">fi

    </span><span class="nb">dd </span><span class="k">if</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">source_disk</span><span class="k">}</span><span class="s2">1"</span> <span class="nv">of</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">target_disk</span><span class="k">}</span><span class="s2">1"</span> <span class="nv">bs</span><span class="o">=</span>4M <span class="nv">status</span><span class="o">=</span>progress
    <span class="nb">dd </span><span class="k">if</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">source_disk</span><span class="k">}</span><span class="s2">2"</span> <span class="nv">of</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">target_disk</span><span class="k">}</span><span class="s2">2"</span> <span class="nv">bs</span><span class="o">=</span>4M <span class="nv">status</span><span class="o">=</span>progress
<span class="k">else
    </span><span class="nb">echo</span> <span class="s2">"Target disk NOT found."</span>
<span class="k">fi</span></code></pre></figure>]]></content><author><name>Courtney Bodett</name></author><summary type="html"><![CDATA[I remember years ago when I first made a bootable Kali thumb drive (‘BackTrack’ back then) with encrypted peristence there was an option to create a sort of “self destruct” where if you typed a special password the drive would be erased rather than unlocked. Kali still offers this package: cryptsetup-nuke-password. For some reason this ability still fascinates me. The option to self desctruct your stuff at a moment’s notice. I’ve always used Luks encryption on Linux devices but this specific “nuke password” setup doesn’t seem to exist much outside of Kali. I continued reading and doing some experimenting on an older laptop and eventually decided to set my Framework 13 laptop up with deniable encryption.]]></summary></entry><entry><title type="html">Small Modules Part 2</title><link href="https://courtneybodett.com/Small-Modules-Part2/" rel="alternate" type="text/html" title="Small Modules Part 2" /><published>2025-12-08T00:00:00-08:00</published><updated>2025-12-08T00:00:00-08:00</updated><id>https://courtneybodett.com/Small-Modules-Part2</id><content type="html" xml:base="https://courtneybodett.com/Small-Modules-Part2/"><![CDATA[<h2 id="natural-language-passphrases">Natural Language Passphrases</h2>
<p>Previously I talked about making small modules as an excuse to practice making tools and focus on making something that works well.  I ended by saying I might try turning my <a href="'https://www.powershellgallery.com/packages/New-NaturalLanguagePassword'">‘New-NaturalLanguagePassword Script’</a> in to a module.<br />
I followed through on this not long after making that post with the first version being published in March and the most recent version being from May.  If you want to skip right to checking it out I invite you to visit the Github page for it: <a href="'https://github.com/grey0ut/PSPhrase'">‘PSPhrase’</a><br />
I wanted it to work on PowerShell v5.1 or newer, as well as Windows, Linux or Mac.  I also wanted to leverage the <a href="'https://github.com/gaelcolas/Sampler'">‘Sampler Module’</a> for the development.</p>
<h2 id="the-checklist">The Checklist</h2>
<p>The first thing I like to do before I even open an IDE is to write down the goals for what this thing will do.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- Retain all existing configuration options from the script  
- Store word lists outside of the ps1 files  
- Allow for saving parameters/values as 'configuration'
</code></pre></div></div>
<p>It would be easy enough to retain all of the existing parameters from the script as I already wrote them.  In the script the adjective and noun wordlists are stored directly in the script as a hashtable with numbers as keys corresponding to dice rolls.  This is an artifact of originally being based on diceware passwords.  Since I’m doing my own thing now I decided that words can just be stored in simple text files with one word per line.  This simplifies maintaining the wordlists and doesn’t constrain us to number ranges that align with 6 sided dice rolls.<br />
Lastly, I wanted the end user to be able to save parameters and values as their default preference and have it persist between sessions.  I know that <a href="'https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parameters_default_values?view=powershell-7.5'">‘Paramter Default Values’</a> exist but I wanted to tailor this specifically for use with the module.</p>
<h2 id="whats-in-a-name">What’s In a Name?</h2>
<p><img src="https://courtneybodett.com/assets/images/Small_Modules_02.png" alt="MyNameIs" /><br />
After listening to <a href="'https://startautomating.com/'">James Brundage</a> talk about how important a good name/branding is for a module at the ‘24’ PowerShell Summit I really like to start with that.  The script name is very long and quite literal.  This module would be generating passwords.  Not just passwords, but passphrases specifically.  I like the idea of trying to incorporate ‘PS’ in to the name of the module if it works and it didn’t take very long to think of ‘PSPhrase’.  This would mean I could name the primary function <code class="language-plaintext highlighter-rouge">Get-PSPhrase</code>.  That’s a lot easier to type than <code class="language-plaintext highlighter-rouge">New-NaturalLanguagePassword</code>.</p>

<h2 id="starting-with-sampler-module">Starting with Sampler Module</h2>
<p>The first thing was to start a new project with SamplerModule.  I’m not going to go in to great detail here, but the resulting structure will look something like this:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.
├── Assets
│   └── PSPhrase.svg
├── build.ps1
├── build.yaml
├── CHANGELOG.md
├── LICENSE
├── output
│   ├── CHANGELOG.md
│   ├── module
│   ├── ReleaseNotes.md
│   ├── RequiredModules
│   └── testResults
├── README.md
├── RequiredModules.psd1
├── Resolve-Dependency.ps1
├── Resolve-Dependency.psd1
├── source
│   ├── Data
│   ├── Private
│   ├── PSPhrase.psd1
│   ├── PSPhrase.psm1
│   └── Public
└── tests
    ├── QA
    └── Unit
</code></pre></div></div>
<p>Most of my work will be happening in the <code class="language-plaintext highlighter-rouge">source</code> directory.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.
├── Data
│   ├── Adjectives.txt
│   └── Nouns.txt
├── Private
│   ├── Get-RandomInt.ps1
│   └── Initialize-Dictionary.ps1
├── PSPhrase.psd1
├── PSPhrase.psm1
└── Public
    ├── Get-PSPhrase.ps1
    ├── Get-PSPhraseSetting.ps1
    └── Set-PSPhraseSetting.ps1
</code></pre></div></div>
<p>My <code class="language-plaintext highlighter-rouge">Data</code> directory will house the txt files for the dictionary words and will get shipped with the module in the same way.  The <code class="language-plaintext highlighter-rouge">Private</code> and <code class="language-plaintext highlighter-rouge">Public</code> directories containing functions will all get built in to a single monolithic .psm1 file with only the Public functions getting exported.</p>
<h2 id="existing-configuration-options">Existing Configuration Options</h2>
<p>The parameters present in the <code class="language-plaintext highlighter-rouge">New-NaturalLanaugePassword</code> script were pretty much taken word for word for the new <code class="language-plaintext highlighter-rouge">Get-PSPhrase</code> function.  The <code class="language-plaintext highlighter-rouge">Shortcut</code> parameter was dropped since that was a script specific thing, and the <code class="language-plaintext highlighter-rouge">Pairs</code> parameter now has a ValidateRange set for between 1 and 100.<br />
Pretty simple.</p>

<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="kr">param</span><span class="w"> </span><span class="p">(</span><span class="w">
    </span><span class="p">[</span><span class="n">ValidateRange</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="mi">100</span><span class="p">)]</span><span class="w">
    </span><span class="p">[</span><span class="n">Int32</span><span class="p">]</span><span class="nv">$Pairs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w">
    </span><span class="p">[</span><span class="n">Switch</span><span class="p">]</span><span class="nv">$TitleCase</span><span class="p">,</span><span class="w">
    </span><span class="p">[</span><span class="n">Switch</span><span class="p">]</span><span class="nv">$Substitution</span><span class="p">,</span><span class="w">
    </span><span class="p">[</span><span class="n">String</span><span class="p">]</span><span class="nv">$Append</span><span class="p">,</span><span class="w">
    </span><span class="p">[</span><span class="n">String</span><span class="p">]</span><span class="nv">$Prepend</span><span class="p">,</span><span class="w">
    </span><span class="p">[</span><span class="n">String</span><span class="p">]</span><span class="nv">$Delimiter</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">' '</span><span class="p">,</span><span class="w">
    </span><span class="p">[</span><span class="n">ValidateRange</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="mi">500</span><span class="p">)]</span><span class="w">
    </span><span class="p">[</span><span class="n">Int32</span><span class="p">]</span><span class="nv">$Count</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
    </span><span class="p">[</span><span class="n">Switch</span><span class="p">]</span><span class="nv">$IncludeNumber</span><span class="p">,</span><span class="w">
    </span><span class="p">[</span><span class="n">Switch</span><span class="p">]</span><span class="nv">$IncludeSymbol</span><span class="w">
</span><span class="p">)</span></code></pre></figure>

<h2 id="store-wordlists-outside-of-ps1-files">Store Wordlists Outside of ps1 Files</h2>
<p>Making txt files containing the words was easy enough, and it allowed me to search for some more words to include so that technically this is more word options than are available in the script.  I really liked the use of hashtables in the original script because it made retrieving words super fast.  Since the wordlists would be stored outside of the actual module (.ps1/.psm1) I’m now introducing a file read operation plus hashtable creation.  I experimented with this a bit using Get-Content and after some testing I found that using the .NET <code class="language-plaintext highlighter-rouge">System.IO.File</code> class was much faster at reading in the file.  A quick loop to assign a number to the word starting at 1 and going as high as there are words and we’ve got the first private function.</p>

<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="kr">function</span><span class="w"> </span><span class="nf">Initialize-Dictionary</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="cm">&lt;#
    </span><span class="cs">.SYNOPSIS</span><span class="cm">
    Creates ordered hashtables of wordlists for faster retrieval with random number generator
    </span><span class="cs">.DESCRIPTION</span><span class="cm">
    Reads in a text file containing one word per line and creates an ordered dictionary starting the keys with '1' and incrementing up from there
    for each word added.  Then using a RNG a corresponding key can be called and the associated value (word) can be retrieved very quickly
    </span><span class="cs">.PARAMETER</span><span class="cm"> Type
    Whether to load the Nouns or Adjectives list
    </span><span class="cs">.EXAMPLE</span><span class="cm">
    $Nouns = Initialize-Dictionary -Type Nouns

    will turn $Nouns in to a hashtable containing all the words from the Nouns.txt file
    #&gt;</span><span class="w">
    </span><span class="p">[</span><span class="n">Cmdletbinding</span><span class="p">()]</span><span class="w">
    </span><span class="p">[</span><span class="n">OutputType</span><span class="p">([</span><span class="n">System.Collections.Specialized.OrderedDictionary</span><span class="p">])]</span><span class="w">
    </span><span class="kr">param</span><span class="w"> </span><span class="p">(</span><span class="w">
        </span><span class="p">[</span><span class="n">ValidateSet</span><span class="p">(</span><span class="s2">"Nouns"</span><span class="p">,</span><span class="s2">"Adjectives"</span><span class="p">)]</span><span class="w">
        </span><span class="p">[</span><span class="n">String</span><span class="p">]</span><span class="nv">$Type</span><span class="w">
    </span><span class="p">)</span><span class="w">

    </span><span class="nv">$File</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">switch</span><span class="w"> </span><span class="p">(</span><span class="nv">$Type</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="s2">"Nouns"</span><span class="w"> </span><span class="p">{</span><span class="s2">"Nouns.txt"</span><span class="p">}</span><span class="w">
        </span><span class="s2">"Adjectives"</span><span class="w"> </span><span class="p">{</span><span class="s2">"Adjectives.txt"</span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="nv">$WordListPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Join-Path</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="p">(</span><span class="bp">$PSScriptRoot</span><span class="p">)</span><span class="w"> </span><span class="nt">-ChildPath</span><span class="w"> </span><span class="s2">"Data/</span><span class="nv">$File</span><span class="s2">"</span><span class="w">

    </span><span class="nv">$Words</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">System.IO.File</span><span class="p">]::</span><span class="n">ReadAllLines</span><span class="p">(</span><span class="nv">$WordListPath</span><span class="p">)</span><span class="w">
    </span><span class="nv">$Dictionary</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">Ordered</span><span class="p">]@{}</span><span class="w">
    </span><span class="nv">$Number</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="w">
    </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Word</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$Words</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$Dictionary</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="nv">$Number</span><span class="p">,</span><span class="w"> </span><span class="nv">$Word</span><span class="p">)</span><span class="w">
        </span><span class="nv">$Number</span><span class="o">++</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="nv">$Dictionary</span><span class="w">
</span><span class="p">}</span></code></pre></figure>

<p>The <code class="language-plaintext highlighter-rouge">Type</code> parameter just helps dictate which txt file to read in. This way the same function works for both the list of adjectives and nouns.  Skipping ahead a bit, but for reference it takes just a handful of milliseconds for Get-PSPhrase to generate a single passphrase which involves reading in both of these txt files, creating hashtables, generating random numbers, and combining words. I think that’s plenty fast enough.</p>

<p>Now that we have  way to create our hashtables we also need a way to randomly select a word from the hashtables.  Since I’m not strictly sticking to the idea of dice rolls anymore I can focus on just generating a random number bewteen 1 and however many words there are. Rather than using the provided <code class="language-plaintext highlighter-rouge">Get-Random</code> cmdlet I wanted to make sure this was a really good RNG to ensure high entropy.  Again we’re going with a .NET method for this.</p>

<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="kr">Function</span><span class="w"> </span><span class="nf">Get-RandomInt</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="cm">&lt;#
    </span><span class="cs">.SYNOPSIS</span><span class="cm">
    More robust method of getting a random number than Get-Random
    </span><span class="cs">.DESCRIPTION</span><span class="cm">
    Leverages the .NET RNGCryptoServiceProvider to retrieve a random number
    </span><span class="cs">.PARAMETER</span><span class="cm"> Minimum
    Minimum number for range of random number generation
    </span><span class="cs">.PARAMETER</span><span class="cm"> Maximum
    Maximum number for range of random number generation
    </span><span class="cs">.EXAMPLE</span><span class="cm">
    PS&gt; Get-RanomInt -Minimum 1 -Maximum 1000
    838

    will return a random number from between 1 and 1000
    #&gt;</span><span class="w">
    </span><span class="kr">param</span><span class="w"> </span><span class="p">(</span><span class="w">
        </span><span class="p">[</span><span class="n">UInt32</span><span class="p">]</span><span class="nv">$Minimum</span><span class="p">,</span><span class="w">
        </span><span class="p">[</span><span class="n">UInt32</span><span class="p">]</span><span class="nv">$Maximum</span><span class="w">
    </span><span class="p">)</span><span class="w">

    </span><span class="nv">$Difference</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Maximum</span><span class="o">-</span><span class="nv">$Minimum</span><span class="o">+</span><span class="mi">1</span><span class="w">
    </span><span class="p">[</span><span class="n">Byte</span><span class="p">[]]</span><span class="nv">$Bytes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="o">..</span><span class="mi">4</span><span class="w">
    </span><span class="p">[</span><span class="n">System.Security.Cryptography.RNGCryptoServiceProvider</span><span class="p">]::</span><span class="n">Create</span><span class="p">()</span><span class="o">.</span><span class="nf">GetBytes</span><span class="p">(</span><span class="nv">$Bytes</span><span class="p">)</span><span class="w">
    </span><span class="p">[</span><span class="n">Int32</span><span class="p">]</span><span class="nv">$Integer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">System.BitConverter</span><span class="p">]::</span><span class="n">ToUInt32</span><span class="p">((</span><span class="nv">$Bytes</span><span class="p">),</span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="nv">$Difference</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">1</span><span class="w">
    </span><span class="nv">$Integer</span><span class="w">
</span><span class="p">}</span></code></pre></figure>

<p>This creates a 4 byte array to feed to the RNDCryptoServiceProvider for randomization and then converts the byte array in to an integer to work with.  Now the relevant section of Get-PSPhrase can look like this:</p>

<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="nv">$NounsHash</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Initialize-Dictionary</span><span class="w"> </span><span class="nt">-Type</span><span class="w"> </span><span class="nx">Nouns</span><span class="w">
</span><span class="nv">$AdjectivesHash</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Initialize-Dictionary</span><span class="w"> </span><span class="nt">-Type</span><span class="w"> </span><span class="nx">Adjectives</span><span class="w">

</span><span class="nv">$Passphrases</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="o">..</span><span class="nv">$Settings</span><span class="o">.</span><span class="nf">Count</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ForEach-Object</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="p">[</span><span class="n">System.Collections.ArrayList</span><span class="p">]</span><span class="nv">$WordArray</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="o">..</span><span class="nv">$Settings</span><span class="o">.</span><span class="nf">Pairs</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ForEach-Object</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$Number</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-RandomInt</span><span class="w"> </span><span class="nt">-Minimum</span><span class="w"> </span><span class="nx">1</span><span class="w"> </span><span class="nt">-Maximum</span><span class="w"> </span><span class="nv">$AdjectivesHash</span><span class="o">.</span><span class="nf">Count</span><span class="w">
        </span><span class="nv">$AdjectivesHash</span><span class="o">.</span><span class="nv">$Number</span><span class="w">
        </span><span class="nv">$Number</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-RandomInt</span><span class="w"> </span><span class="nt">-Minimum</span><span class="w"> </span><span class="nx">1</span><span class="w"> </span><span class="nt">-Maximum</span><span class="w"> </span><span class="nv">$NounsHash</span><span class="o">.</span><span class="nf">Count</span><span class="w">
        </span><span class="nv">$NounsHash</span><span class="o">.</span><span class="nv">$Number</span><span class="w">
    </span><span class="p">}</span></code></pre></figure>

<p>Read the text files and create the wordlist hashtables.  The first section is a little confusing but we create a number sequence using the <code class="language-plaintext highlighter-rouge">1..5</code> syntax.  If the ‘Count’ from settings is 10 this will create the numbers 1 through 10.  These are piped to <code class="language-plaintext highlighter-rouge">ForEach-Object</code> but rather than do anything with the objects themselves I’m just using it as a way to repeat next operation however many times is dicated by ‘Count’.<br />
The same technique is used for the number of pairs of words to be generated for the passphrase.  Then it’s simply generate a random number, get an adjective, generate a random number, get a noun.  These are output and captured by an ArrayList called <code class="language-plaintext highlighter-rouge">$WordArray</code>.  That’s it, that’s the meat and potatos.</p>
<h2 id="allow-saving-preferences">Allow Saving Preferences</h2>
<p>There’s quite a few parameters that allow you to control the output of <code class="language-plaintext highlighter-rouge">Get-PSPhrase</code> and if you use every single one it can be quite a bit to type. Example:</p>

<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="n">PS</span><span class="err">&gt;</span><span class="w"> </span><span class="nx">Get-PSPhrase</span><span class="w"> </span><span class="nt">-Count</span><span class="w"> </span><span class="nx">10</span><span class="w"> </span><span class="nt">-Pairs</span><span class="w"> </span><span class="nx">2</span><span class="w"> </span><span class="nt">-TitleCase</span><span class="w"> </span><span class="nt">-Substitution</span><span class="w"> </span><span class="nt">-Append</span><span class="w"> </span><span class="s1">'1'</span><span class="w"> </span><span class="nt">-Prepend</span><span class="w"> </span><span class="s1">'!'</span><span class="w"> </span><span class="nt">-Delimiter</span><span class="w"> </span><span class="s1">'-'</span><span class="w"> </span><span class="nt">-IncludeNumber</span><span class="w"> </span><span class="nt">-IncludeSymbol</span></code></pre></figure>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>!-L0c0-3Fl3$h-#P3rf3ct-T3nt-1
!-1Unt1dy$-R3j3ct-$udd3n-Clump-1
!-V3n3r@t3d7-C0mm3nt$-*Cl0$3-T1ngl3-1
!-$n1v3l1ng%-3ng1n38-W3ll-D0cum3nt3d-Pur1t@n-1
!-#B1g-Pr@nk3r-Bubbly5-G0bl1n$-1
!-$Pr3w@r-Bl0@t-Prud3nt-20c3@n-1
!-@n0th3r*-B00m-0bl0ng-6H0m3-1
!-!Up$3t-W0m3n-1Gr@nd10$3-$ph3r3$-1
!-Bl0@t3d$-N0t3$-N0t3d8-R@nch-1
!-%L3th@l2-Cr3w$-Ch@rr3d-G1rl$-1
</code></pre></div></div>
<p>Imagine you want to run Get-PSPhrase like that every single time because that’s your preference.  You could copy that out somewhere and paste it everytime you want use it.  You could wrap it in a function definition you keep in your profile.  You could even put it in a script and then execute that.  I wanted people to be able to save their preference for parameters and values <strong>and</strong> still overwrite that or use other combinations.</p>

<p>The first step was figuring out where to store these preferences, with consideration that I wanted this to be cross platform.  The module could also be installed system wide for all users, or just for a single user.  What I focused on was storing the preferences somewhere specific to the current user’s ‘Home’.  Detecting if the current computer is Windows, Mac or Linux was the first step and dictates where the settings file will be written:</p>

<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="c"># check to see if we're on Windows or not</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="bp">$IsWindows</span><span class="w"> </span><span class="o">-or</span><span class="w"> </span><span class="nv">$</span><span class="nn">ENV</span><span class="p">:</span><span class="nv">OS</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$Windows</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
</span><span class="p">}</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$Windows</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Windows</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$SettingsPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Join-Path</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$</span><span class="nn">Env</span><span class="p">:</span><span class="nv">APPDATA</span><span class="w"> </span><span class="nt">-ChildPath</span><span class="w"> </span><span class="s2">"PSPhrase\Settings.json"</span><span class="w">
</span><span class="p">}</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$SettingsPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Join-Path</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="p">([</span><span class="n">Environment</span><span class="p">]::</span><span class="n">GetEnvironmentVariable</span><span class="p">(</span><span class="s2">"HOME"</span><span class="p">))</span><span class="w"> </span><span class="nt">-ChildPath</span><span class="w"> </span><span class="s2">".local/share/powershell/Modules/PSPhrase/Settings.json"</span><span class="w">
</span><span class="p">}</span></code></pre></figure>

<p>I decided to go with a json file as it made it easy to write to and read from and still deal with objects.  The function <code class="language-plaintext highlighter-rouge">Set-PSPhraseSetting</code> would then have the same parameter options as <code class="language-plaintext highlighter-rouge">Get-PSPhrase</code> but would instead save these parameters to disk.  The called parameters and values are stored in a hashtable called ‘$Settings’ with each parameter name being a key, and the provided value the corresponding value.  The settings hashtable is then converted to json and written to disk.  A switch parameter of <code class="language-plaintext highlighter-rouge">Defaults</code> is also included which will actually just delete the json file so that Get-PSPhrase goes back to its defaults.<br />
The <code class="language-plaintext highlighter-rouge">Get-PSPhraseSetting</code> function is pretty straight forward.  It does the same check for platform type to determine where the json file should be, checks for it, and if found it reads it in, converts it from json and spits out the saved settings.</p>

<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="n">PS</span><span class="err">&gt;</span><span class="w"> </span><span class="nx">Get-PSPhraseSetting</span><span class="w">

</span><span class="n">Count</span><span class="w"> </span><span class="nx">IncludeNumber</span><span class="w">
</span><span class="o">-----</span><span class="w"> </span><span class="o">-------------</span><span class="w">
   </span><span class="mi">20</span><span class="w">          </span><span class="n">True</span></code></pre></figure>

<p>Standard PowerShell formatting rules apply; if 4 or less properties it will return a table.  More than that and it will return a list.</p>

<p>Now the tricky part was making it so you could call <code class="language-plaintext highlighter-rouge">Get-PSPhrase</code> by itself, leveraging the saved preferences, <em>or</em> with additional (or the same) parameters and still have it work. I should have started writing this blog post closer to when I was authoring the module just so I could tell you all the ways that <strong>don’t</strong> work.  Instead here’s what ended up working for me:</p>

<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="nv">$Settings</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">Ordered</span><span class="p">]@{</span><span class="w">
    </span><span class="nx">Pairs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Pairs</span><span class="w">
    </span><span class="nx">Count</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Count</span><span class="w">
    </span><span class="nx">Delimiter</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Delimiter</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$DefaultSettings</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-PSPhraseSetting</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Setting</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$DefaultSettings</span><span class="o">.</span><span class="nf">PSObject</span><span class="o">.</span><span class="nf">Properties</span><span class="o">.</span><span class="nf">Name</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">-not</span><span class="p">(</span><span class="bp">$PSBoundParameters</span><span class="o">.</span><span class="nf">ContainsKey</span><span class="p">(</span><span class="nv">$Setting</span><span class="p">)))</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nv">$Settings</span><span class="o">.</span><span class="nv">$Setting</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$DefaultSettings</span><span class="o">.</span><span class="nv">$Setting</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span></code></pre></figure>

<p>Taken from inside Get-PSPhrase.  The first thing we do is start our <code class="language-plaintext highlighter-rouge">$Settings</code> hashtable to store settings.  Since ‘Pairs’, ‘Count’, and ‘Delimeter’ all have default values in the parameter statement we establish those right off the bat.  Then, leveraging our existing public function that returns a PSCustomObject of any saved settings we combine a check for saved settings with the definition of a variable called <code class="language-plaintext highlighter-rouge">$DefaultSettings</code> should they exist.  Then the logic after that is something like this:  For each setting found in saved settings, <strong>if</strong> that setting was not explicitly called during the execution of <code class="language-plaintext highlighter-rouge">Get-PSPhrase</code> then update our <code class="language-plaintext highlighter-rouge">$Settings</code> hashtable with that setting name and its value.  This works to overwrite those existing default settings if necessary, while also creating new settings entries in the hashtable if they don’t exist.</p>

<p>After that it’s more or less the same logic I used before for string manipulation.  I use a switch block on the keys in the <code class="language-plaintext highlighter-rouge">$Settings</code> hashtable and it executes on whatever is present.</p>

<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="nv">$CultureObj</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">Get-Culture</span><span class="p">)</span><span class="o">.</span><span class="nf">TextInfo</span><span class="w">

</span><span class="kr">switch</span><span class="w"> </span><span class="p">(</span><span class="nv">$Settings</span><span class="o">.</span><span class="nf">Keys</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="s1">'TitleCase'</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$WordArray</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$WordArray</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ForEach-Object</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nv">$CultureObj</span><span class="o">.</span><span class="nf">ToTitleCase</span><span class="p">(</span><span class="bp">$_</span><span class="p">)</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="s1">'Substitution'</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$WordArray</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$WordArray</span><span class="w"> </span><span class="o">-replace</span><span class="w"> </span><span class="s2">"e"</span><span class="p">,</span><span class="s2">"3"</span><span class="w"> </span><span class="o">-replace</span><span class="w"> </span><span class="s2">"a"</span><span class="p">,</span><span class="s2">"@"</span><span class="w"> </span><span class="o">-replace</span><span class="w"> </span><span class="s2">"o"</span><span class="p">,</span><span class="s2">"0"</span><span class="w"> </span><span class="o">-replace</span><span class="w"> </span><span class="s2">"s"</span><span class="p">,</span><span class="s2">"$"</span><span class="w"> </span><span class="o">-replace</span><span class="w"> </span><span class="s2">"i"</span><span class="p">,</span><span class="s2">"1"</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="s1">'IncludeNumber'</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$RandomNumber</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Random</span><span class="w"> </span><span class="nt">-Minimum</span><span class="w"> </span><span class="nx">0</span><span class="w"> </span><span class="nt">-Maximum</span><span class="w"> </span><span class="nx">9</span><span class="w">
        </span><span class="nv">$WordIndex</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$WordArray</span><span class="o">.</span><span class="nf">IndexOf</span><span class="p">((</span><span class="nv">$WordArray</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Get-Random</span><span class="p">))</span><span class="w">
        </span><span class="nv">$Position</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">,(</span><span class="nv">$WordArray</span><span class="p">[</span><span class="nv">$WordIndex</span><span class="p">]</span><span class="o">.</span><span class="nf">Length</span><span class="p">)</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Get-Random</span><span class="w">
        </span><span class="nv">$WordArray</span><span class="p">[</span><span class="nv">$WordIndex</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$WordArray</span><span class="p">[</span><span class="nv">$WordIndex</span><span class="p">]</span><span class="o">.</span><span class="nf">Insert</span><span class="p">(</span><span class="nv">$Position</span><span class="p">,</span><span class="w"> </span><span class="nv">$RandomNumber</span><span class="p">)</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="s1">'IncludeSymbol'</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$RandomSymbol</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'!'</span><span class="p">,</span><span class="w"> </span><span class="s1">'@'</span><span class="p">,</span><span class="w"> </span><span class="s1">'#'</span><span class="p">,</span><span class="w"> </span><span class="s1">'$'</span><span class="p">,</span><span class="w"> </span><span class="s1">'%'</span><span class="p">,</span><span class="w"> </span><span class="s1">'*'</span><span class="p">,</span><span class="w"> </span><span class="s1">'?'</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Get-Random</span><span class="w">
        </span><span class="nv">$WordIndex</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$WordArray</span><span class="o">.</span><span class="nf">IndexOf</span><span class="p">((</span><span class="nv">$WordArray</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Get-Random</span><span class="p">))</span><span class="w">
        </span><span class="nv">$Position</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">,(</span><span class="nv">$WordArray</span><span class="p">[</span><span class="nv">$WordIndex</span><span class="p">]</span><span class="o">.</span><span class="nf">Length</span><span class="p">)</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Get-Random</span><span class="w">
        </span><span class="nv">$WordArray</span><span class="p">[</span><span class="nv">$WordIndex</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$WordArray</span><span class="p">[</span><span class="nv">$WordIndex</span><span class="p">]</span><span class="o">.</span><span class="nf">Insert</span><span class="p">(</span><span class="nv">$Position</span><span class="p">,</span><span class="w"> </span><span class="nv">$RandomSymbol</span><span class="p">)</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="s1">'Append'</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="p">[</span><span class="n">Void</span><span class="p">]</span><span class="nv">$WordArray</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="nv">$Settings</span><span class="o">.</span><span class="nf">Append</span><span class="p">)</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="s1">'Prepend'</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$WordArray</span><span class="o">.</span><span class="nf">Insert</span><span class="p">(</span><span class="nx">0</span><span class="p">,</span><span class="w"> </span><span class="nv">$Settings</span><span class="o">.</span><span class="nf">Prepend</span><span class="p">)</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="nv">$WordArray</span><span class="w"> </span><span class="o">-join</span><span class="w"> </span><span class="nv">$Settings</span><span class="o">.</span><span class="nf">Delimiter</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="nv">$Passphrases</span></code></pre></figure>

<p>The order is somewhat important here as well, and is why the <code class="language-plaintext highlighter-rouge">$Settings</code> hashtable is ordered. We can’t perform the TitleCase or Substitution operations after the Append and Prepend operations as it could take affect on those values, which are intended to be static.</p>
<h2 id="conclusion">Conclusion</h2>
<p>I had fun getting more comfortable with the SamplerModule.  Writing QA and Unit tests was new to me and I felt like I spent a lot of time just figuring out how tests work.  But in the end I’m happy with how the module turned out and I’ve got a pretty good flow for any changes that need to take place in the future.</p>]]></content><author><name>Courtney Bodett</name></author><summary type="html"><![CDATA[Natural Language Passphrases Previously I talked about making small modules as an excuse to practice making tools and focus on making something that works well. I ended by saying I might try turning my ‘New-NaturalLanguagePassword Script’ in to a module. I followed through on this not long after making that post with the first version being published in March and the most recent version being from May. If you want to skip right to checking it out I invite you to visit the Github page for it: ‘PSPhrase’ I wanted it to work on PowerShell v5.1 or newer, as well as Windows, Linux or Mac. I also wanted to leverage the ‘Sampler Module’ for the development. The Checklist The first thing I like to do before I even open an IDE is to write down the goals for what this thing will do. - Retain all existing configuration options from the script - Store word lists outside of the ps1 files - Allow for saving parameters/values as 'configuration' It would be easy enough to retain all of the existing parameters from the script as I already wrote them. In the script the adjective and noun wordlists are stored directly in the script as a hashtable with numbers as keys corresponding to dice rolls. This is an artifact of originally being based on diceware passwords. Since I’m doing my own thing now I decided that words can just be stored in simple text files with one word per line. This simplifies maintaining the wordlists and doesn’t constrain us to number ranges that align with 6 sided dice rolls. Lastly, I wanted the end user to be able to save parameters and values as their default preference and have it persist between sessions. I know that ‘Paramter Default Values’ exist but I wanted to tailor this specifically for use with the module. What’s In a Name? After listening to James Brundage talk about how important a good name/branding is for a module at the ‘24’ PowerShell Summit I really like to start with that. The script name is very long and quite literal. This module would be generating passwords. Not just passwords, but passphrases specifically. I like the idea of trying to incorporate ‘PS’ in to the name of the module if it works and it didn’t take very long to think of ‘PSPhrase’. This would mean I could name the primary function Get-PSPhrase. That’s a lot easier to type than New-NaturalLanguagePassword.]]></summary></entry><entry><title type="html">Small Modules</title><link href="https://courtneybodett.com/Small-Modules/" rel="alternate" type="text/html" title="Small Modules" /><published>2025-03-01T00:00:00-08:00</published><updated>2025-03-01T00:00:00-08:00</updated><id>https://courtneybodett.com/Small-Modules</id><content type="html" xml:base="https://courtneybodett.com/Small-Modules/"><![CDATA[<h2 id="size-doesnt-matter">Size Doesn’t Matter</h2>
<p>When I made my first PowerShell module it was mostly an experiment to see if I could understand how they worked.  It was simply <code class="language-plaintext highlighter-rouge">tools.psm1</code> and it lived in <code class="language-plaintext highlighter-rouge">$HOME\Documents\PowerShell\Modules\Tools</code>.  That was all it took to move some function definitions out of my $Profile and in to a place where they would automatically load when called.<br />
This would later grow in to a bigger collection of daily use functions for me and my team.  I think at its peak there were around 44 functions, a couple of class definitions, and a few format files.  Some of the modules you may be familiar with (e.g. ImportExcel, Posh-SSH, PowerCLI) can have upwards of 70 public functions.  These modules represent tremendous effort and provide huge value to the greater PowerShell community.  But not every module needs to be a monolith of code.</p>

<p>Something that Kevin Marquette wrote about in <a href="&quot;https://powershellexplained.com/2019-04-11-Powershell-Building-Micro-Modules/&quot;">his blog</a> that I found very enouraging was the concept of building “micro modules.”  Just because a module exports 1 or 2 functions doesn’t mean that it’s not valuable.<br />
What I really like about a module is the ability to package up PowerShell functions, and any requirements, in to something that’s easy to install and use from the CLI.  You can define classes and format files to go along with your module and the process of getting the module doesn’t change one bit.  I could write a script to accomplish a lot of the same end result but here’s my general philosphy on PowerShell code:</p>
<ul>
  <li>Cmdlets are used in the CLI interactively to perform tasks</li>
  <li>Custom functions are used in the CLI interactively to perform tasks. They expand on existing cmdlets or fill a more custom need.</li>
  <li>Scripts are an orchestration/automation tool used to perform a set of actions, sometimes unattended</li>
  <li>Modules are a deliverable way to install functions (and functionality) on to a system</li>
</ul>

<p>Maybe you’ve written a logging function that you want to incorporate in to a bunch of scripts you’ve written that might run unattended.  It could be just 40 lines of code or so, and you could include it at the top of all your scripts. Maybe it lives in a separate .ps1 file and you dot source it at script execution.  By comparison the latter option would make it easier to write changes to your logging module.  <strong>OR</strong> you turn your logging function in to a module and publish it in a repository.  Now it’s installable on any machine that you need it on, and you can use built in cmdlets for module management like <code class="language-plaintext highlighter-rouge">Update-Module</code> to handle changes.</p>

<p>If you’re writing PowerShell functions, and you’re putting them in to modules, you’re making tools.  If that tool happens to do one thing really well that sounds like a pretty good tool to me.  I don’t need a screwdriver that’s also a flashlight, a radio, and a spatula.  I need a screwdriver.  Preferably one that’s so good I never have to think about it. Go ahead and turn that function, or two, in to a shippable module.</p>

<h2 id="some-examples">Some Examples</h2>
<p>I wrote about my <a href="https://courtneybodett.com/ProtectStrings">ProtectStrings</a> module before and while it took me a lot of work to get it to where I want, it’s ultimately two functions: Protect-String, and Unprotect-String.  The other 7 functions are just about supporting the master password in some way.</p>

<p>I also had a weird scenario at work where we needed to change the network profile of a WiFi network from “Public” to “Private” while <em>not</em> connected to that network.  This required editing the registry.  To make it easier to do I wrote a function called Set-NetworkProfile that would handle the edits.  If you’re making a Set-* function I think it’s usually appropriate to make a Get-* one as well and think about how they might be used together.<br />
Pretty quickly this turned in to a small module with 3 public functions, 1 private, and a format file for forcing the output to be a table.  The module does one thing, and does it well and because of the fact that it’s a module it’s easy to deploy to machines, and allowed me to hide a private function from the end-user and control the output.  You can see more about it <a href="&quot;https://github.com/grey0ut/NetworkProfile&quot;">here</a>.</p>

<p>Another recent one was born from a very silly use case.  A peer wanted help putting an easter egg in his large script the involved some ASCII art and some other stuff.  He didn’t want people to be able to open the script in an editor and see this so the goal was obfuscation.  I suggested base64 at first but the resulting block of text was quite large.  We did a little bit of research and ended up using the <a href="&quot;https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.deflatestream?view=net-9.0&quot;">deflatestream</a> .NET class to compress the data and then expand it during script execution when the easter egg was triggered.  I got back to him a little bit later to show him I had written <code class="language-plaintext highlighter-rouge">Compress-String</code> and <code class="language-plaintext highlighter-rouge">Expand-String</code> and was working on making Compress-String accept pipeline input.  At the time neither of us could think of a single thing to do with these functions so I stowed them away and forgot about them.<br />
Months later I found them again and decided that they should be in a module and spent way too much time trying to think of a clever name instead of doing anything with the code. I recall one of the names I picked already belonged to an existing project on Github but I noticed that they hadn’t properly handled pipeline input so this told me I still had something to offer.<br />
<a href="&quot;https://github.com/grey0ut/ComPrS/&quot;">ComPrS</a> is live now after finding another legitimate use case for it.  Microsoft published a detection script for a vulnerability and the script calls out to Github to download a CSV of hashes and check against them.  The script was appreciated but we had a lot of endpoints to run it on and many of them didn’t have the ability to connect to Github.  In the interest of portability I put the data right in the script, adding over 3700 lines to it.  As an exercise I included a copy of my Expand-String function in the script and compressed the reference data using Compress-String and ConvertTo-HereString.  This shortened the addition to the script significantly but not anythign earth shattering.</p>

<h2 id="ok-so-what">OK so what?</h2>
<p><img src="https://courtneybodett.com/assets/images/Small_Modules_01.png" alt="AustinPowers" /><br />
Building a small module is a great way to focus on making a tool that does something well, and to get you in the headspace of a tool maker.  Asking yourself questions about how other people might use this tool or what functionality they might want out of it.  Try to make it as applicable as possible for its use case.</p>

<p>Also, it’s a great way to keep learning PowerShell and get in to module making without feeling intimidated.</p>

<p>Now I think I want to turn my script “New-NaturalLanguagePassword” in to a module so that it’s easier to install and use from the command line.
<img src="https://courtneybodett.com/assets/images/Invoke_History_01.png" alt="Ihr1" /></p>]]></content><author><name>Courtney Bodett</name></author><summary type="html"><![CDATA[Size Doesn’t Matter When I made my first PowerShell module it was mostly an experiment to see if I could understand how they worked. It was simply tools.psm1 and it lived in $HOME\Documents\PowerShell\Modules\Tools. That was all it took to move some function definitions out of my $Profile and in to a place where they would automatically load when called. This would later grow in to a bigger collection of daily use functions for me and my team. I think at its peak there were around 44 functions, a couple of class definitions, and a few format files. Some of the modules you may be familiar with (e.g. ImportExcel, Posh-SSH, PowerCLI) can have upwards of 70 public functions. These modules represent tremendous effort and provide huge value to the greater PowerShell community. But not every module needs to be a monolith of code. Something that Kevin Marquette wrote about in his blog that I found very enouraging was the concept of building “micro modules.” Just because a module exports 1 or 2 functions doesn’t mean that it’s not valuable. What I really like about a module is the ability to package up PowerShell functions, and any requirements, in to something that’s easy to install and use from the CLI. You can define classes and format files to go along with your module and the process of getting the module doesn’t change one bit. I could write a script to accomplish a lot of the same end result but here’s my general philosphy on PowerShell code: Cmdlets are used in the CLI interactively to perform tasks Custom functions are used in the CLI interactively to perform tasks. They expand on existing cmdlets or fill a more custom need. Scripts are an orchestration/automation tool used to perform a set of actions, sometimes unattended Modules are a deliverable way to install functions (and functionality) on to a system Maybe you’ve written a logging function that you want to incorporate in to a bunch of scripts you’ve written that might run unattended. It could be just 40 lines of code or so, and you could include it at the top of all your scripts. Maybe it lives in a separate .ps1 file and you dot source it at script execution. By comparison the latter option would make it easier to write changes to your logging module. OR you turn your logging function in to a module and publish it in a repository. Now it’s installable on any machine that you need it on, and you can use built in cmdlets for module management like Update-Module to handle changes. If you’re writing PowerShell functions, and you’re putting them in to modules, you’re making tools. If that tool happens to do one thing really well that sounds like a pretty good tool to me. I don’t need a screwdriver that’s also a flashlight, a radio, and a spatula. I need a screwdriver. Preferably one that’s so good I never have to think about it. Go ahead and turn that function, or two, in to a shippable module.]]></summary></entry><entry><title type="html">Quick Tip Invoke-History</title><link href="https://courtneybodett.com/QuickTipHistory/" rel="alternate" type="text/html" title="Quick Tip Invoke-History" /><published>2024-08-24T00:00:00-07:00</published><updated>2024-08-24T00:00:00-07:00</updated><id>https://courtneybodett.com/QuickTipHistory</id><content type="html" xml:base="https://courtneybodett.com/QuickTipHistory/"><![CDATA[<p>The more time I spend living in the CLI the more I appreciate learning and adopting shorthand for operations.  In Powershell the aliases for Where-Object and ForEach-Object have become second nature.  Using up arrow to repeat the previous command and add more to it a near constant occurence.  One situation I find myself in quite a bit however is running a command in Powershell, and then finding that based on the output I’d actually like to re-run that command an get the value from a property instead.  On the keyboard I’ve been using for the past couple of years I would typically just hit up-arrow to repeat the last command, <code class="language-plaintext highlighter-rouge">Home</code> to put my cursor on the beginning of the line, type a <code class="language-plaintext highlighter-rouge">(</code> then <code class="language-plaintext highlighter-rouge">End</code> a closing <code class="language-plaintext highlighter-rouge">)</code> then use dot notation to call the property I wanted the value of.</p>

<p>As an example let’s say I run a script and see what the output looks like:
<img src="https://courtneybodett.com/assets/images/Invoke_History_01.png" alt="Ihr1" /><br />
From this I observe the output and decide that I’d like to add some parameters.  No problem, I’ll just up-arrow to repeat the command and add the parameters to the end:<br />
<img src="https://courtneybodett.com/assets/images/Invoke_History_02.png" alt="Ihr2" /><br />
Great, but if I wanted to call a property/method on that output object I’d either have to pipe it to another cmdlet or wrap it in parenthesis and then call the property I want with the dot shortcut. I.e.:<br />
<img src="https://courtneybodett.com/assets/images/Invoke_History_03.png" alt="Ihr3" /><br />
Seems simple enough. Home key, parenthesis, End key, parenthesis, dot, property name.  But, my new keyboard is a 60% and doesn’t have dedicated arrow keys requiring that I hold another key to access a layer that has arrow keys on it.</p>
<h1 id="invoke-history-has-entered-the-chat">Invoke-History Has Entered the Chat</h1>
<p>I remebered that along with Get-History there was Invoke-History and its alias of ‘r’.  I’ve previously used this similarly to repeating commands in bash.  Get-History, find the number of the command, then use <code class="language-plaintext highlighter-rouge">r &lt;num&gt;</code> to repeat:<br />
<img src="https://courtneybodett.com/assets/images/Invoke_History_04.png" alt="Ihr4" /><br />
Through experimentation I found that calling <code class="language-plaintext highlighter-rouge">r</code> by itself repeats the previous command by default.<br />
<img src="https://courtneybodett.com/assets/images/Invoke_History_05.png" alt="Ihr5" /><br />
The next thing I tried was wrapping ‘r’ in an expression to see if I could then use the dot shortcut to retrieve a property or method:<br />
<img src="https://courtneybodett.com/assets/images/Invoke_History_06.png" alt="Ihr6" /><br />
And shazam! Now instead of needing arrow keys or Home/End keys I can start from a fresh prompt and type <code class="language-plaintext highlighter-rouge">(r)</code> following by whatever it was I wanted to do on the previous operation.<br />
<img src="https://courtneybodett.com/assets/images/mind-blown.gif" alt="MindBlown" /></p>]]></content><author><name>Courtney Bodett</name></author><summary type="html"><![CDATA[The more time I spend living in the CLI the more I appreciate learning and adopting shorthand for operations. In Powershell the aliases for Where-Object and ForEach-Object have become second nature. Using up arrow to repeat the previous command and add more to it a near constant occurence. One situation I find myself in quite a bit however is running a command in Powershell, and then finding that based on the output I’d actually like to re-run that command an get the value from a property instead. On the keyboard I’ve been using for the past couple of years I would typically just hit up-arrow to repeat the last command, Home to put my cursor on the beginning of the line, type a ( then End a closing ) then use dot notation to call the property I wanted the value of. As an example let’s say I run a script and see what the output looks like: From this I observe the output and decide that I’d like to add some parameters. No problem, I’ll just up-arrow to repeat the command and add the parameters to the end: Great, but if I wanted to call a property/method on that output object I’d either have to pipe it to another cmdlet or wrap it in parenthesis and then call the property I want with the dot shortcut. I.e.: Seems simple enough. Home key, parenthesis, End key, parenthesis, dot, property name. But, my new keyboard is a 60% and doesn’t have dedicated arrow keys requiring that I hold another key to access a layer that has arrow keys on it. Invoke-History Has Entered the Chat I remebered that along with Get-History there was Invoke-History and its alias of ‘r’. I’ve previously used this similarly to repeating commands in bash. Get-History, find the number of the command, then use r &lt;num&gt; to repeat: Through experimentation I found that calling r by itself repeats the previous command by default. The next thing I tried was wrapping ‘r’ in an expression to see if I could then use the dot shortcut to retrieve a property or method: And shazam! Now instead of needing arrow keys or Home/End keys I can start from a fresh prompt and type (r) following by whatever it was I wanted to do on the previous operation.]]></summary></entry><entry><title type="html">Powershell Summit 2024</title><link href="https://courtneybodett.com/Summit-2024/" rel="alternate" type="text/html" title="Powershell Summit 2024" /><published>2024-04-12T00:00:00-07:00</published><updated>2024-04-12T00:00:00-07:00</updated><id>https://courtneybodett.com/Summit-2024</id><content type="html" xml:base="https://courtneybodett.com/Summit-2024/"><![CDATA[<p>I got the opportunity this week to attend the 2024 <a href="https://www.powershellsummit.org/">Powershell Summit</a> in Bellevue Washington.  If you have an opportunity to go to this, whether you’re brand new to Powershell or a steely-eyed veteran, I highly recommend it.</p>

<p>Beyond the individual sessions and workshops, the conversations that are had throughout the day in hallways, at tables and even at dinner are invaluable.  I am still a bit overwhelmed but I managed to spend some time since the conference updating my ProtectStrings module.  I wanted to clean up some of the code and also update it to be cross platform.  After the conference I no longer view Powershell as strictly a Windows shell.  Despite having Powershell 7.x installed on my Linux computer I still wrote most of my stuff on a Windows machine and never thought much about using it on Linux.<br />
After what I saw at the conference I’ve got a renewed mindset focused on tool making and compatibility.  I hope I get the chance to attend next year as well.</p>]]></content><author><name>Courtney Bodett</name></author><summary type="html"><![CDATA[I got the opportunity this week to attend the 2024 Powershell Summit in Bellevue Washington. If you have an opportunity to go to this, whether you’re brand new to Powershell or a steely-eyed veteran, I highly recommend it. Beyond the individual sessions and workshops, the conversations that are had throughout the day in hallways, at tables and even at dinner are invaluable. I am still a bit overwhelmed but I managed to spend some time since the conference updating my ProtectStrings module. I wanted to clean up some of the code and also update it to be cross platform. After the conference I no longer view Powershell as strictly a Windows shell. Despite having Powershell 7.x installed on my Linux computer I still wrote most of my stuff on a Windows machine and never thought much about using it on Linux. After what I saw at the conference I’ve got a renewed mindset focused on tool making and compatibility. I hope I get the chance to attend next year as well.]]></summary></entry><entry><title type="html">Reset Expiration Clock</title><link href="https://courtneybodett.com/Reset_Expiration/" rel="alternate" type="text/html" title="Reset Expiration Clock" /><published>2023-11-04T00:00:00-07:00</published><updated>2023-11-04T00:00:00-07:00</updated><id>https://courtneybodett.com/Reset_Expiration</id><content type="html" xml:base="https://courtneybodett.com/Reset_Expiration/"><![CDATA[<p>With more and more people working remotely there’s been a huge uptick in VPN usage.  A lot of organizations have had to completely rethink some of their previous IT policies and procedures.  Some things that used to be simple are now slightly more complicated.</p>

<p>One thing I wasn’t aware of, being so far removed from front line customer support at work, was that a lot of our user’s passwords were expiring while they were working remote.  With an expired password, they couldn’t connect to the VPN, and without connecting to the VPN they couldn’t update their password.  Unfortunately self-service password reset is not within our control because that’s the obvious answer.  In some cases users were being told to come in to the nearest office so they could sign in to their computer on network, and then update their password.  In other cases the help desk was resetting their password and dictating it to them over the phone. But, more often than not the help desk was asking for the user’s current password, and resetting it in AD to that.  Obviously this is all really bad (especially that last one), but there wasn’t an available solution to stop this from happening.  I read that there was a way with Powershell to essentially reset the password expiration clock on a user account to push the date out.  If your password expired yesterday, and the domain policy was a 90 day password, then “resetting” it would change your expiration date to 90 days from now.  This would make the user’s currently configured password valid again and prevent any form of password sharing.  Then the user could manually initiate a password change once they were up and running again.</p>

<h1 id="the-pwdlastset-attribute">The pwdLastSet Attribute</h1>

<p>The pwdLastSet Attribute in Active Directory contains a record of the last time the account’s password was set.  Here is the definition from <a href="https://learn.microsoft.com/en-us/windows/win32/adschema/a-pwdlastset">Microsoft</a>:</p>
<blockquote>
  <p>“The date and time that the password for this account was last changed. This value is stored as a large integer that represents the number of 100 nanosecond intervals since January 1, 1601 (UTC). If this value is set to 0 and the User-Account-Control attribute does not contain the UF_DONT_EXPIRE_PASSWD flag, then the user must set the password at the next logon.”</p>
</blockquote>

<p>There’s also the PasswordLastSet attribute which is just the pwdLastSet attribute but converted in to a DateTime object which is a lot more readable.  But, if you want to make a change directly to an account’s Password Last Set it’s done via the pwdLastSet attribute.  Knowing that it’s stored as a large integer number representing “file time” is important when we start making changes to it.</p>

<h1 id="updating-the-attribute">Updating the Attribute</h1>

<p>Making changes to an Active Directory user account is often done with Set-ADUser and this is no different.  If you look at the help info for <a href="https://learn.microsoft.com/en-us/powershell/module/activedirectory/set-aduser?view=windowsserver2022-ps">Set-ADUser</a> we can see that there are a lot of parameters representing attributes/properties we can change.  The pwdLastSet attribute isn’t on the list however. There are plenty of forum hits and examples that reveal that the parameter we need to use is -Replace.  The -Replace parameter accepts a hashtable as value so the syntax is pretty straight forward:  The property name you want to update, and the value you want to replace it with.</p>

<p>Whether a user account’s password is expired or not, if you replace the pwdLastSet value with a 0 it effectively expires their password immediately.  We’re clearing the slate here.  The next step seems odd but we replace the pwdLastSet value with a -1. Since this is stored as a large integer value we’re telling it to set it to the largest number that can be stored in a large integer value.  This would be some insane date out in the future <em>except</em> that it uses the domain password policy and caps it out at the default max password age. If that’s 90 days for example, then setting it to -1 puts the expiration date as 90 days out in the future from the execution of the command.  The general consensus online is that both of these steps need to be taken: set it to 0, then -1.  I haven’t done a deep dive on why, but if anyone has an explanation feel free to hit me up.</p>

<h1 id="the-script">The Script</h1>

<p>Seems simple enough then right?  The script just needs to set the pwdLastSet attribute for a given user to 0 and then -1.  One of the things I always ask when I’m writing Powershell for someone else’s consumption is “how” they want to be able to use this.  Do they want to manually launch Powershell and execute the script by calling out its path?  Do they want to be able to double-click a shortcut and have the script execute?  Do they just want a function they can run as a CLI tool in Powershell?<br />
In our case the help desk doesn’t spend a lot of time with Powershell and would prefer to just double-click a shortcut.  I on the other hand prefer to run Powershell scripts from an open Powershell session, so I figured I would accommodate both.</p>

<p>At its simplest the script really just needs to do this:</p>

<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="nv">$User</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Read-Host</span><span class="w"> </span><span class="nt">-Prompt</span><span class="w"> </span><span class="s2">"Enter username you wish to reset"</span><span class="w">  
</span><span class="n">Set-ADUser</span><span class="w"> </span><span class="nt">-Identity</span><span class="w"> </span><span class="nv">$User</span><span class="w"> </span><span class="o">-Replace</span><span class="w"> </span><span class="p">@{</span><span class="nx">pwdLastSet</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">}</span><span class="w"> 
</span><span class="n">Set-ADUser</span><span class="w"> </span><span class="nt">-Identity</span><span class="w"> </span><span class="nv">$User</span><span class="w"> </span><span class="o">-Replace</span><span class="w"> </span><span class="p">@{</span><span class="nx">pwdLastSet</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">-</span><span class="mi">1</span><span class="p">}</span><span class="w"> </span></code></pre></figure>

<p>However, I wanted the script to have some sanity checks, provide before and after info regarding the account’s password expiration, allow for alternate credential use and to run in a loop in case there were multiple accounts to target.  I also wanted it to support running as the target of a shortcut, as well as an interactive script for users that would prefer to do it that way.<br />
<a href="https://github.com/grey0ut/ActiveDirectoryPS">Script on Github</a></p>

<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="cm">&lt;#
</span><span class="cs">.Synopsis</span><span class="cm">
Script to reset an Active Directory account's password timer to the current date and time.  
</span><span class="cs">.Description</span><span class="cm">
This script will reset the 'pwdLastSet' attribute in Active Directory to the current date and time.  Useful for when an account has an expired password, but the user is remote and has no way to sign in to change their password.
</span><span class="cs">.Parameter</span><span class="cm"> Username
The Identity of the user you wish to reset in Active Directory. This should be their SamAccountName. 
If provided with a domain prepended the 'Server' variable will be set to that domain, e.g. 'contoso\jsmith'
</span><span class="cs">.Parameter</span><span class="cm"> Credential
A PSCredential object you would like to use instead of the current running user to authenticate the change
</span><span class="cs">.Parameter</span><span class="cm"> Server
The domain name you wish to perform the action against if different than the domain you're currently on. 
</span><span class="cs">.Parameter</span><span class="cm"> Shortcut
A switch parameter to be used in conjunction with a Windows shortcut. When used it keeps the Powershell window open after script execution until the user hits 'enter'
</span><span class="cs">.EXAMPLE</span><span class="cm">
PS&gt; .\Reset-PasswordClock.ps1
Please provide a username: jsmith
User's current info:

User            : jsmith
DisplayName     : Smith, John
PasswordLastSet : 12/12/2022 7:42:01 AM
ExpiryDate      : 3/12/2023 8:42:01 AM
Lockedout       : False

------------------------------------------------
Would you like to reset the 'PasswordLastSet' to the current time? (y/n):y
Resetting password clock.
User's current info:

User            : jsmith
DisplayName     : Smith, John
PasswordLastSet : 3/15/2023 9:32:01 AM
ExpiryDate      : 6/15/2023 10:32:01 AM
Lockedout       : False

------------------------------------------------
#&gt;</span><span class="w">
</span><span class="c">#Requires -Modules 'ActiveDirectory'</span><span class="w">

</span><span class="kr">Param</span><span class="w"> </span><span class="p">(</span><span class="w">
    </span><span class="p">[</span><span class="n">CmdletBinding</span><span class="p">()]</span><span class="w">
    </span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Position</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">)]</span><span class="w">
    </span><span class="p">[</span><span class="n">String</span><span class="p">]</span><span class="nv">$Username</span><span class="p">,</span><span class="w">
    </span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Position</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">2</span><span class="p">)]</span><span class="w">
    </span><span class="p">[</span><span class="n">PSCredential</span><span class="p">]</span><span class="nv">$Credential</span><span class="p">,</span><span class="w">
    </span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Position</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">)]</span><span class="w">
    </span><span class="p">[</span><span class="n">String</span><span class="p">]</span><span class="nv">$Server</span><span class="p">,</span><span class="w">
    </span><span class="p">[</span><span class="n">Switch</span><span class="p">]</span><span class="nv">$Shortcut</span><span class="w">
</span><span class="p">)</span><span class="w">

</span><span class="kr">If</span><span class="w"> </span><span class="p">(</span><span class="nv">$Shortcut</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$CurrentUser</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">ENV</span><span class="p">:</span><span class="nv">USERNAME</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Script running  as </span><span class="nv">$CurrentUser</span><span class="s2">"</span><span class="w">
    </span><span class="kr">Do</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="kr">Try</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="p">[</span><span class="n">ValidateSet</span><span class="p">(</span><span class="s1">'y'</span><span class="p">,</span><span class="s1">'n'</span><span class="p">)]</span><span class="nv">$Answer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Read-Host</span><span class="w"> </span><span class="nt">-Prompt</span><span class="w"> </span><span class="s2">"Would you like to continue? Say 'n' to be prompted for different credentials (y/n)"</span><span class="w">
        </span><span class="nv">$Continue</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
        </span><span class="p">}</span><span class="w"> </span><span class="kr">Catch</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Please answer with 'y' or 'n'"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Red</span><span class="w">
            </span><span class="nv">$Continue</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w"> </span><span class="kr">Until</span><span class="w"> </span><span class="p">(</span><span class="nv">$Continue</span><span class="p">)</span><span class="w">
    </span><span class="kr">If</span><span class="w"> </span><span class="p">(</span><span class="nv">$Answer</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'n'</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$Credential</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Credential</span><span class="w"> </span><span class="nt">-Message</span><span class="w"> </span><span class="s2">"Provide credentials for executing Reset-PasswordClock"</span><span class="w">
        </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Continuing execution as </span><span class="si">$(</span><span class="nv">$Credential</span><span class="o">.</span><span class="nf">Username</span><span class="si">)</span><span class="s2">"</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="nv">$Loop</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"y"</span><span class="w">
</span><span class="kr">Do</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="c"># prompt for a userid to query if not provided at the command line</span><span class="w">
    </span><span class="kr">If</span><span class="w"> </span><span class="p">(</span><span class="o">-not</span><span class="w"> </span><span class="nv">$Username</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$Username</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Read-Host</span><span class="w"> </span><span class="nt">-Prompt</span><span class="w"> </span><span class="s2">"Please provide a username"</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="c"># get our current domain if not provided by the -Server parameter</span><span class="w">
    </span><span class="kr">If</span><span class="w"> </span><span class="p">(</span><span class="o">-not</span><span class="w"> </span><span class="nv">$Server</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="nv">$Username</span><span class="w"> </span><span class="o">-notmatch</span><span class="w"> </span><span class="s1">'\\'</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$Server</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-CimInstance</span><span class="w"> </span><span class="nt">-ClassName</span><span class="w"> </span><span class="nx">win32_computersystem</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-ExpandProperty</span><span class="w"> </span><span class="nx">Domain</span><span class="w">
    </span><span class="p">}</span><span class="w"> </span><span class="kr">ElseIf</span><span class="w"> </span><span class="p">(</span><span class="o">-not</span><span class="w"> </span><span class="nv">$Server</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="nv">$Username</span><span class="w"> </span><span class="o">-match</span><span class="w"> </span><span class="s1">'\\'</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$Server</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Username</span><span class="o">.</span><span class="nf">Split</span><span class="p">(</span><span class="s1">'\'</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="c"># if username was provided with domain prepended, remove it at this point</span><span class="w">
    </span><span class="kr">If</span><span class="w"> </span><span class="p">(</span><span class="nv">$Username</span><span class="w"> </span><span class="o">-match</span><span class="w"> </span><span class="s1">'\\'</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$Username</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Username</span><span class="o">.</span><span class="nf">Split</span><span class="p">(</span><span class="s1">'\'</span><span class="p">)[</span><span class="mi">1</span><span class="p">]</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="c"># define our 'Select-Object' properties to make the command easier to read down below</span><span class="w">
    </span><span class="nv">$SelObjArgs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">Ordered</span><span class="p">]@{</span><span class="w">
        </span><span class="nx">Property</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(@{</span><span class="nx">Name</span><span class="o">=</span><span class="s2">"User"</span><span class="p">;</span><span class="nx">Expression</span><span class="o">=</span><span class="p">{</span><span class="bp">$_</span><span class="o">.</span><span class="nf">SamAccountName</span><span class="p">}},</span><span class="w">
                    </span><span class="s2">"DisplayName"</span><span class="p">,</span><span class="w">
                    </span><span class="s2">"PasswordLastSet"</span><span class="p">,</span><span class="w">
                    </span><span class="p">@{</span><span class="nx">Name</span><span class="o">=</span><span class="s2">"ExpiryDate"</span><span class="p">;</span><span class="nx">Expression</span><span class="o">=</span><span class="p">{[</span><span class="n">datetime</span><span class="p">]::</span><span class="n">fromfiletime</span><span class="p">(</span><span class="bp">$_</span><span class="o">.</span><span class="s2">"msds-userpasswordexpirytimecomputed"</span><span class="p">)}},</span><span class="w">
                    </span><span class="s2">"Lockedout"</span><span class="w">
        </span><span class="p">)</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="c"># our command parameters, defined ahead of time for easier reading down below</span><span class="w">
    </span><span class="nv">$GetADUserArgs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">Ordered</span><span class="p">]@{</span><span class="w">
        </span><span class="nx">Identity</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Username</span><span class="w">
        </span><span class="nx">Server</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Server</span><span class="w">
        </span><span class="nx">Properties</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="s1">'Displayname'</span><span class="p">,</span><span class="s1">'Passwordlastset'</span><span class="p">,</span><span class="s1">'msDS-userpasswordexpirytimecomputed'</span><span class="p">,</span><span class="s1">'lockedout'</span><span class="p">)</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="kr">If</span><span class="w"> </span><span class="p">(</span><span class="nv">$Credential</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$GetADUserArgs</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s1">'Credential'</span><span class="p">,</span><span class="nv">$Credential</span><span class="p">)</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="c"># Check AD to see if the supplied username exists and then provide the current state of the account.</span><span class="w">
    </span><span class="kr">Try</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$ADInfo</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ADUser</span><span class="w"> </span><span class="err">@</span><span class="nx">GetADUserArgs</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="nx">Stop</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w">  </span><span class="err">@</span><span class="nx">SelObjArgs</span><span class="w">
    </span><span class="p">}</span><span class="w"> </span><span class="kr">Catch</span><span class="w"> </span><span class="p">[</span><span class="n">Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException</span><span class="p">]{</span><span class="w">
        </span><span class="n">Write-Warning</span><span class="w"> </span><span class="s2">"</span><span class="nv">$Username</span><span class="s2"> not found in domain: </span><span class="nv">$Server</span><span class="s2">"</span><span class="w">
        </span><span class="kr">Exit</span><span class="w">
    </span><span class="p">}</span><span class="w"> </span><span class="kr">Catch</span><span class="p">{</span><span class="w">
        </span><span class="bp">$Error</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
        </span><span class="kr">Exit</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"User's current info:"</span><span class="w">
    </span><span class="nv">$ADInfo</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="p">(</span><span class="s1">'-'</span><span class="o">*</span><span class="mi">48</span><span class="p">)</span><span class="w">

    </span><span class="kr">Do</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="kr">Try</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="p">[</span><span class="n">ValidateSet</span><span class="p">(</span><span class="s1">'y'</span><span class="p">,</span><span class="s1">'n'</span><span class="p">)]</span><span class="nv">$Answer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Read-Host</span><span class="w"> </span><span class="nt">-Prompt</span><span class="w"> </span><span class="s2">"Would you like to reset the 'PasswordLastSet' to the current time? (y/n)"</span><span class="w">
        </span><span class="nv">$Continue</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
        </span><span class="p">}</span><span class="w"> </span><span class="kr">Catch</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Please answer with 'y' or 'n'"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Red</span><span class="w">
            </span><span class="nv">$Continue</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w"> </span><span class="kr">Until</span><span class="w"> </span><span class="p">(</span><span class="nv">$Continue</span><span class="p">)</span><span class="w">
    </span><span class="n">Remove-Variable</span><span class="w"> </span><span class="nx">Continue</span><span class="w">

    </span><span class="kr">If</span><span class="w"> </span><span class="p">(</span><span class="nv">$Answer</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"n"</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Exiting..."</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Yellow</span><span class="w">
        </span><span class="kr">Exit</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="cm">&lt;# Assigning a 0 to the 'pwdLastSet' attribute immediately expires the password, and is a prerequisite to the next step.
    Followed by assigning a -1. Because of the way 64-bit integers are saved, this is the largest possible value that
    can be saved in a LargeInteger attribute. It corresponds to a date far in the future. But the system will assign a 
    value corresponding to the current datetime the next time the user logs on. The password will then expire according 
    to the maximum password age policy that applies to the user.
    #&gt;</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Resetting password clock."</span><span class="w">
    </span><span class="nv">$SetADUserArgs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">Ordered</span><span class="p">]@{</span><span class="w">
        </span><span class="nx">Identity</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Username</span><span class="w">
        </span><span class="nx">Server</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Server</span><span class="w">
        </span><span class="nx">ErrorAction</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Stop'</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="kr">If</span><span class="w"> </span><span class="p">(</span><span class="nv">$Credential</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$SetADUserArgs</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s1">'Credential'</span><span class="p">,</span><span class="nv">$Credential</span><span class="p">)</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="kr">Try</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Set-ADUser</span><span class="w"> </span><span class="err">@</span><span class="nx">SetADUserArgs</span><span class="w"> </span><span class="o">-Replace</span><span class="w"> </span><span class="p">@{</span><span class="nx">pwdLastSet</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">}</span><span class="w"> 
        </span><span class="n">Set-ADUser</span><span class="w"> </span><span class="err">@</span><span class="nx">SetADUserArgs</span><span class="w"> </span><span class="o">-Replace</span><span class="w"> </span><span class="p">@{</span><span class="nx">pwdLastSet</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">-</span><span class="mi">1</span><span class="p">}</span><span class="w"> 
    </span><span class="p">}</span><span class="w"> </span><span class="kr">Catch</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Write-Warning</span><span class="w"> </span><span class="s2">"Encountered an error. Unable to reset password expiration."</span><span class="w">
        </span><span class="bp">$Error</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="c"># Re-Check AD</span><span class="w">
    </span><span class="kr">Try</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$ADInfo</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ADUser</span><span class="w"> </span><span class="err">@</span><span class="nx">GetADUserArgs</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="nx">Stop</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w">  </span><span class="err">@</span><span class="nx">SelObjArgs</span><span class="w">
    </span><span class="p">}</span><span class="w"> </span><span class="kr">Catch</span><span class="w"> </span><span class="p">[</span><span class="n">Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException</span><span class="p">]{</span><span class="w">
        </span><span class="n">Write-Warning</span><span class="w"> </span><span class="s2">"</span><span class="nv">$Username</span><span class="s2"> not found in domain: </span><span class="nv">$Server</span><span class="s2">"</span><span class="w">
        </span><span class="kr">Exit</span><span class="w">
    </span><span class="p">}</span><span class="w"> </span><span class="kr">Catch</span><span class="p">{</span><span class="w">
        </span><span class="bp">$Error</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="nf">Exception</span><span class="w">
        </span><span class="kr">Exit</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"User's current info:"</span><span class="w">
    </span><span class="nv">$ADInfo</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="p">(</span><span class="s1">'-'</span><span class="o">*</span><span class="mi">48</span><span class="p">)</span><span class="w">

    </span><span class="c"># clear variables in case of loop</span><span class="w">
    </span><span class="n">Remove-Variable</span><span class="w"> </span><span class="nx">ADInfo</span><span class="p">,</span><span class="nx">Username</span><span class="p">,</span><span class="nx">Server</span><span class="w">
    </span><span class="c"># ask if the user would like to run the action again against a different user.</span><span class="w">
    </span><span class="kr">Do</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="kr">Try</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="p">[</span><span class="n">ValidateSet</span><span class="p">(</span><span class="s1">'y'</span><span class="p">,</span><span class="s1">'n'</span><span class="p">)]</span><span class="nv">$Loop</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Read-Host</span><span class="w"> </span><span class="nt">-Prompt</span><span class="w"> </span><span class="s2">"Would you like to work on another user (y/n)?"</span><span class="w">
        </span><span class="nv">$Continue</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
        </span><span class="p">}</span><span class="w"> </span><span class="kr">Catch</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Please answer with 'y' or 'n'"</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Red</span><span class="w">
            </span><span class="nv">$Continue</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w"> </span><span class="kr">Until</span><span class="w"> </span><span class="p">(</span><span class="nv">$Continue</span><span class="p">)</span><span class="w">

</span><span class="p">}</span><span class="w"> </span><span class="kr">While</span><span class="w"> </span><span class="p">(</span><span class="nv">$Loop</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"y"</span><span class="p">)</span><span class="w">

</span><span class="kr">If</span><span class="w"> </span><span class="p">(</span><span class="nv">$Shortcut</span><span class="p">){</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Press enter to close this window..."</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Yellow</span><span class="w">
    </span><span class="n">Read-Host</span><span class="w">
    </span><span class="nx">Exit</span><span class="w">
</span><span class="p">}</span></code></pre></figure>

<p>That’s a lot, I know.  But let’s look at what that might look like in action.  You’ve got a shortcut made for this stored somewhere accessible, and you simply double click it:<br />
<img src="https://courtneybodett.com/assets/images/Reset_Expiration_01.png" alt="Shortcut" /><br />
Then let’s say the account you log in to your computer with doesn’t have delegated permissions to make these changes so you need to provide credentials. This is collected securely via a Read-Host prompt:<br />
<img src="https://courtneybodett.com/assets/images/Reset_Expiration_03.png" alt="Step1" /><br />
Provide the account name that you want to target and it starts by providing the current state of the account.  Seeing the ExpiryDate property represents an expired password I say “y” to the prompt to reset the clock and it pulls the account from AD again to show that now the PasswordLastSet and ExpiryDate properties have updated.<br />
<img src="https://courtneybodett.com/assets/images/Reset_Expiration_04.png" alt="Step2" /><br />
If there are more accounts to be done you can answer “y” at the end and the process starts over.  If we say “n” then it will prompt that pressing “enter” will close the window, and that’s it.  If running the script from the CLI directly it’s very similar except that you can provide a PSCredential object as a parameter or simply specify a username and it will securely prompt for a password.  The loop is the same, just without the prompt at the end to “close this window”.</p>

<h1 id="conclusion">Conclusion</h1>

<p>In the end the script has helped with a task that was previously being performed with poor security practices.  It should be noted that both NIST and ISO have moved away from recommending password expiration as a security control, and as organizations catch up this will likely become less and less of an issue.  Also, Powershell is absolutely <strong>not</strong> the right solution for this problem.  The right solution would be to have a self-service password reset portal available where the users can authenticate with their expired password, and securely update their password and have it update in all required systems.  In absence of that, Powershell turned out to be a pretty good solution.</p>]]></content><author><name>Courtney Bodett</name></author><summary type="html"><![CDATA[With more and more people working remotely there’s been a huge uptick in VPN usage. A lot of organizations have had to completely rethink some of their previous IT policies and procedures. Some things that used to be simple are now slightly more complicated. One thing I wasn’t aware of, being so far removed from front line customer support at work, was that a lot of our user’s passwords were expiring while they were working remote. With an expired password, they couldn’t connect to the VPN, and without connecting to the VPN they couldn’t update their password. Unfortunately self-service password reset is not within our control because that’s the obvious answer. In some cases users were being told to come in to the nearest office so they could sign in to their computer on network, and then update their password. In other cases the help desk was resetting their password and dictating it to them over the phone. But, more often than not the help desk was asking for the user’s current password, and resetting it in AD to that. Obviously this is all really bad (especially that last one), but there wasn’t an available solution to stop this from happening. I read that there was a way with Powershell to essentially reset the password expiration clock on a user account to push the date out. If your password expired yesterday, and the domain policy was a 90 day password, then “resetting” it would change your expiration date to 90 days from now. This would make the user’s currently configured password valid again and prevent any form of password sharing. Then the user could manually initiate a password change once they were up and running again. The pwdLastSet Attribute The pwdLastSet Attribute in Active Directory contains a record of the last time the account’s password was set. Here is the definition from Microsoft: “The date and time that the password for this account was last changed. This value is stored as a large integer that represents the number of 100 nanosecond intervals since January 1, 1601 (UTC). If this value is set to 0 and the User-Account-Control attribute does not contain the UF_DONT_EXPIRE_PASSWD flag, then the user must set the password at the next logon.” There’s also the PasswordLastSet attribute which is just the pwdLastSet attribute but converted in to a DateTime object which is a lot more readable. But, if you want to make a change directly to an account’s Password Last Set it’s done via the pwdLastSet attribute. Knowing that it’s stored as a large integer number representing “file time” is important when we start making changes to it. Updating the Attribute Making changes to an Active Directory user account is often done with Set-ADUser and this is no different. If you look at the help info for Set-ADUser we can see that there are a lot of parameters representing attributes/properties we can change. The pwdLastSet attribute isn’t on the list however. There are plenty of forum hits and examples that reveal that the parameter we need to use is -Replace. The -Replace parameter accepts a hashtable as value so the syntax is pretty straight forward: The property name you want to update, and the value you want to replace it with. Whether a user account’s password is expired or not, if you replace the pwdLastSet value with a 0 it effectively expires their password immediately. We’re clearing the slate here. The next step seems odd but we replace the pwdLastSet value with a -1. Since this is stored as a large integer value we’re telling it to set it to the largest number that can be stored in a large integer value. This would be some insane date out in the future except that it uses the domain password policy and caps it out at the default max password age. If that’s 90 days for example, then setting it to -1 puts the expiration date as 90 days out in the future from the execution of the command. The general consensus online is that both of these steps need to be taken: set it to 0, then -1. I haven’t done a deep dive on why, but if anyone has an explanation feel free to hit me up. The Script Seems simple enough then right? The script just needs to set the pwdLastSet attribute for a given user to 0 and then -1. One of the things I always ask when I’m writing Powershell for someone else’s consumption is “how” they want to be able to use this. Do they want to manually launch Powershell and execute the script by calling out its path? Do they want to be able to double-click a shortcut and have the script execute? Do they just want a function they can run as a CLI tool in Powershell? In our case the help desk doesn’t spend a lot of time with Powershell and would prefer to just double-click a shortcut. I on the other hand prefer to run Powershell scripts from an open Powershell session, so I figured I would accommodate both. At its simplest the script really just needs to do this: $User = Read-Host -Prompt "Enter username you wish to reset" Set-ADUser -Identity $User -Replace @{pwdLastSet = 0} Set-ADUser -Identity $User -Replace @{pwdLastSet = -1} However, I wanted the script to have some sanity checks, provide before and after info regarding the account’s password expiration, allow for alternate credential use and to run in a loop in case there were multiple accounts to target. I also wanted it to support running as the target of a shortcut, as well as an interactive script for users that would prefer to do it that way. Script on Github]]></summary></entry><entry><title type="html">SecretStore Module</title><link href="https://courtneybodett.com/SecretsManagement/" rel="alternate" type="text/html" title="SecretStore Module" /><published>2023-11-04T00:00:00-07:00</published><updated>2023-11-04T00:00:00-07:00</updated><id>https://courtneybodett.com/SecretsManagement</id><content type="html" xml:base="https://courtneybodett.com/SecretsManagement/"><![CDATA[<p>SecretManagement module is a Powershell module intended to make it easier to store and retrieve secrets.</p>
<blockquote>
  <p>The secrets are stored in SecretManagement extension vaults. An extension vault is a PowerShell module that has been registered to SecretManagement, and exports five module functions required by SecretManagement. An extension vault can store secrets locally or remotely. Extension vaults are registered to the current logged in user context, and will be available only to that user (unless also registered to other users).</p>
</blockquote>

<p><a href="https://github.com/PowerShell/SecretManagement">SecretManagement Module on Github</a></p>

<p>This is a really cool project and an awesome tool that Microsoft created.  I see it get referred to a lot in different Powershell communities as a recommended solution for dealing with secrets in automation.  I haven’t had any occasion to use it myself but I had often thought about writing a Powershell based password manager (until SecretManagement was released).</p>

<p>Relevant to my interests then if you want to just store secrets locally on your computer for use in scripts you’ll want to look at the SecretStore module.<br />
<a href="https://github.com/PowerShell/SecretStore">SecretStore Module on Github</a></p>
<blockquote>
  <p>It stores secrets locally on file for the current user account context, and uses .NET crypto APIs to encrypt file contents. Secrets remain encrypted in-memory, and are only decrypted when retrieved and passed to the user. This module works over all supported PowerShell platforms on Windows, Linux, and macOS.</p>
</blockquote>

<p>Since theirs is cross platform and mine isn’t it’s probably different .NET in the backend, but the class is likely the same.  For reference, in .NET it’s referred to by its RFC “Rfc2898DeriveBytes”.  Since this is all publicly available on Github I thought I would search through the relevant CS code and try to understand how they did it differently.  Here is the file I found where I believe PBKDF2 is happening:<br />
<a href="https://github.com/PowerShell/SecretStore/blob/master/src/code/Utils.cs">Utils.cs</a></p>

<p>There’s two sections that drew my attention.  The first:</p>

<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="kr">private</span><span class="w"> </span><span class="kr">static</span><span class="w"> </span><span class="n">byte</span><span class="p">[]</span><span class="w"> </span><span class="n">DeriveKeyFromPassword</span><span class="p">(</span><span class="w">
    </span><span class="n">byte</span><span class="p">[]</span><span class="w"> </span><span class="n">passwordData</span><span class="p">,</span><span class="w">
    </span><span class="n">int</span><span class="w"> </span><span class="nx">keyLength</span><span class="p">)</span><span class="w">
</span><span class="p">{</span><span class="w">
    </span><span class="kr">try</span><span class="w">
    </span><span class="p">{</span><span class="w">
        </span><span class="kr">using</span><span class="w"> </span><span class="p">(</span><span class="kr">var</span><span class="w"> </span><span class="n">derivedBytes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">new</span><span class="w"> </span><span class="nx">Rfc2898DeriveBytes</span><span class="p">(</span><span class="w">
            </span><span class="n">password:</span><span class="w"> </span><span class="nx">passwordData</span><span class="p">,</span><span class="w"> 
            </span><span class="n">salt:</span><span class="w"> </span><span class="nx">salt</span><span class="p">,</span><span class="w"> 
            </span><span class="n">iterations:</span><span class="w"> </span><span class="nx">1000</span><span class="p">))</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="kr">return</span><span class="w"> </span><span class="n">derivedBytes.GetBytes</span><span class="p">(</span><span class="n">keyLength</span><span class="p">);</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="kr">finally</span><span class="w">
    </span><span class="p">{</span><span class="w">
        </span><span class="n">ZeroOutData</span><span class="p">(</span><span class="n">passwordData</span><span class="p">);</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span></code></pre></figure>

<p>And the second:</p>

<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="kr">private</span><span class="w"> </span><span class="kr">static</span><span class="w"> </span><span class="n">AesKey</span><span class="w"> </span><span class="nx">DeriveKeyFromKeyAndPasswordOrUser</span><span class="p">(</span><span class="w">
        </span><span class="n">SecureString</span><span class="w"> </span><span class="nx">passWord</span><span class="p">,</span><span class="w">
        </span><span class="n">AesKey</span><span class="w"> </span><span class="nx">key</span><span class="p">,</span><span class="w">
        </span><span class="n">bool</span><span class="w"> </span><span class="nx">useOrigUserNameCasing</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">false</span><span class="p">)</span><span class="w">
    </span><span class="p">{</span><span class="w">            
        </span><span class="kr">var</span><span class="w"> </span><span class="n">passWordData</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">GetPasswordOrUserData</span><span class="p">(</span><span class="n">passWord</span><span class="p">,</span><span class="w"> </span><span class="nx">useOrigUserNameCasing</span><span class="p">);</span><span class="w">
        </span><span class="kr">try</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="n">byte</span><span class="p">[]</span><span class="w"> </span><span class="n">newKey</span><span class="p">;</span><span class="w">
            </span><span class="kr">using</span><span class="w"> </span><span class="p">(</span><span class="kr">var</span><span class="w"> </span><span class="n">derivedBytes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">new</span><span class="w"> </span><span class="nx">Rfc2898DeriveBytes</span><span class="p">(</span><span class="w">
                </span><span class="n">password:</span><span class="w"> </span><span class="nx">passWordData</span><span class="p">,</span><span class="w"> 
                </span><span class="n">salt:</span><span class="w"> </span><span class="nx">key.Key</span><span class="p">,</span><span class="w"> 
                </span><span class="n">iterations:</span><span class="w"> </span><span class="nx">1000</span><span class="p">))</span><span class="w">
            </span><span class="p">{</span><span class="w">
                </span><span class="n">newKey</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">derivedBytes.GetBytes</span><span class="p">(</span><span class="n">key.Key.Length</span><span class="p">);</span><span class="w">
            </span><span class="p">}</span><span class="w">

            </span><span class="n">byte</span><span class="p">[]</span><span class="w"> </span><span class="n">newIV</span><span class="p">;</span><span class="w">
            </span><span class="kr">using</span><span class="w"> </span><span class="p">(</span><span class="kr">var</span><span class="w"> </span><span class="n">derivedBytes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">new</span><span class="w"> </span><span class="nx">Rfc2898DeriveBytes</span><span class="p">(</span><span class="w">
                </span><span class="n">password:</span><span class="w"> </span><span class="nx">passWordData</span><span class="p">,</span><span class="w">
                </span><span class="n">salt:</span><span class="w"> </span><span class="nx">key.IV</span><span class="p">,</span><span class="w">
                </span><span class="n">iterations:</span><span class="w"> </span><span class="nx">1000</span><span class="p">))</span><span class="w">
            </span><span class="p">{</span><span class="w">
                </span><span class="n">newIV</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">derivedBytes.GetBytes</span><span class="p">(</span><span class="n">key.IV.Length</span><span class="p">);</span><span class="w">
            </span><span class="p">}</span><span class="w">

            </span><span class="kr">return</span><span class="w"> </span><span class="n">new</span><span class="w"> </span><span class="nx">AesKey</span><span class="p">(</span><span class="w">
                </span><span class="n">key:</span><span class="w"> </span><span class="nx">newKey</span><span class="p">,</span><span class="w">
                </span><span class="n">iv:</span><span class="w"> </span><span class="nx">newIV</span><span class="p">);</span><span class="w">
        </span><span class="p">}</span><span class="w">
        </span><span class="kr">finally</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="n">ZeroOutData</span><span class="p">(</span><span class="n">passWordData</span><span class="p">);</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span></code></pre></figure>

<h1 id="backstory">Backstory</h1>

<p>Remember how quickly everyone bailed on LastPass after their breach?  It was revealed that they were using PBKDF2 with SHA256 and only 100,001 iterations (<a href="https://www.theverge.com/2022/12/28/23529547/lastpass-vault-breach-disclosure-encryption-cybersecurity-rebuttal">Article from The Verge</a>).  In some cases, if the account was older, it was even worse than that at only 5000 iterations.  For comparison, OWASP has been recommending 310,000 iterations (SHA256) since at least 2021 and as of today recommends 600,000.</p>

<p>Essentially, LastPass left people’s vaults more vulnerable to brute force attack (after the breach) because it wasn’t computationally costly to iterate through millions of attempted master passwords.</p>

<h1 id="the-problem">The Problem</h1>

<p>Having spent time in Powershell leveraging the Rfc2898DeriveBytes class (PBKDF2) quite a bit with my ProtectStrings module I spent a bit of time reading up on the available hash algorithms, recommended salt lengths and iteration counts and made sure to pick values that I thought would make it not worth while to try to brute force any keys generated using my code.</p>

<p>I was trying to find a deep dive article anywhere online that got in to how the SecretStore module functioned at a cryptographic level but couldn’t find anything.  I’m still not 100% sure at what point these implementations of PBKDF2 are being leveraged but from what I can tell the way they’re being called leverages the default constructors.<br />
Reading the actual RFC it appears that the default constructor (<a href="https://www.rfc-editor.org/rfc/rfc2898">from 2000</a>) leverages SHA1 and 1000 iterations.</p>

<p>Now, it would certainly not be an easy task to brute force this, but this is production code being advertised for essentially the same purpose as LastPass: protect your secrets.  In this module’s case the data would be stored locally on your computer, as compared to someone else’s server, but the fact remains that this code is leveraging 23 year old constructors that <em>even</em> Microsoft says are deprecated.<br />
<img src="https://courtneybodett.com/assets/images/PBKDF2_01.png" alt="PBKDF2Constructors" /><br />
<img src="https://courtneybodett.com/assets/images/PBKDF2_02.png" alt="PBKDF2" /></p>

<p>Seems like poor form for Microsoft to call out all these deprecated constructors as obsolete and insecure and then go ahead and use them in their Powershell module specifically made for storing secrets.</p>]]></content><author><name>Courtney Bodett</name></author><summary type="html"><![CDATA[SecretManagement module is a Powershell module intended to make it easier to store and retrieve secrets. The secrets are stored in SecretManagement extension vaults. An extension vault is a PowerShell module that has been registered to SecretManagement, and exports five module functions required by SecretManagement. An extension vault can store secrets locally or remotely. Extension vaults are registered to the current logged in user context, and will be available only to that user (unless also registered to other users). SecretManagement Module on Github This is a really cool project and an awesome tool that Microsoft created. I see it get referred to a lot in different Powershell communities as a recommended solution for dealing with secrets in automation. I haven’t had any occasion to use it myself but I had often thought about writing a Powershell based password manager (until SecretManagement was released). Relevant to my interests then if you want to just store secrets locally on your computer for use in scripts you’ll want to look at the SecretStore module. SecretStore Module on Github It stores secrets locally on file for the current user account context, and uses .NET crypto APIs to encrypt file contents. Secrets remain encrypted in-memory, and are only decrypted when retrieved and passed to the user. This module works over all supported PowerShell platforms on Windows, Linux, and macOS. Since theirs is cross platform and mine isn’t it’s probably different .NET in the backend, but the class is likely the same. For reference, in .NET it’s referred to by its RFC “Rfc2898DeriveBytes”. Since this is all publicly available on Github I thought I would search through the relevant CS code and try to understand how they did it differently. Here is the file I found where I believe PBKDF2 is happening: Utils.cs There’s two sections that drew my attention. The first: private static byte[] DeriveKeyFromPassword( byte[] passwordData, int keyLength) { try { using (var derivedBytes = new Rfc2898DeriveBytes( password: passwordData, salt: salt, iterations: 1000)) { return derivedBytes.GetBytes(keyLength); } } finally { ZeroOutData(passwordData); } } And the second:]]></summary></entry><entry><title type="html">Status Update</title><link href="https://courtneybodett.com/Status_Update/" rel="alternate" type="text/html" title="Status Update" /><published>2023-08-20T00:00:00-07:00</published><updated>2023-08-20T00:00:00-07:00</updated><id>https://courtneybodett.com/Status_Update</id><content type="html" xml:base="https://courtneybodett.com/Status_Update/"><![CDATA[<p>Hi all.  Just wanted to provide a brief status update.  It’s been a while since my last post and while I have been busy, and making frequent use of Powershell, I haven’t had anything novel that I felt like sharing.</p>

<p>I’ve still been using the Get-GeoLocation function quite a bit as well as another function I wrote called Get-WhoIsIP.  It’s nothing crazy and primarily leverages “http://ipwho.is” API for results.  I spend a lot of time using Powershell as a CLI and want a way to quickly look up IP addresses to determine ownership.  Sometimes lots of IP addresses.</p>

<p>Primarily I would say that I’ve had a lot of occasion to help other people with their Powershell related needs.  Here are some highlight topics I can think of:</p>
<ul>
  <li>modified a Domain Join script to handle adding a computer to groups as part of the process.</li>
  <li>A script as part of a Scheduled Task that emails a CSV of specific types of accounts that need to change their password (based on age)</li>
  <li>A script for someone that needs to rename hundreds of files based on string text found within.  Was previously a manual process, now thanks to Powershell (and Regex) something that used to take hours and hours each week takes a couple of seconds.</li>
  <li>A couple different Active Directory off-boarding scripts to consistently handle removing accounts.</li>
  <li>A script that resets an Active Directory user’s password expiry clock.  Effectively changing the “Password Last Set” time to that of script execution.</li>
  <li>A series of scripts with Scheduled Task setups/executions, data output, and data collection. Heavily relying on DPAPI and AES encryption for data protection.  The “psuedo code” is basically; execute script, save data, acquire data, alert on data.  This also involves build scripts, deployment scripts, removal scripts and a few Scheduled Tasks.  Was a lot of fun to write.</li>
  <li>Helped on a CTF that involved a lot of deobfuscating Powershell and finding flags within the code.</li>
</ul>

<p>That’s about it.  I’m still looking for the idea that’s going to inspire me to write another Powershell module. For now I’ll keep maintaining my team’s internal module, and my publicly available ProtectStrings module.</p>]]></content><author><name>Courtney Bodett</name></author><summary type="html"><![CDATA[Hi all. Just wanted to provide a brief status update. It’s been a while since my last post and while I have been busy, and making frequent use of Powershell, I haven’t had anything novel that I felt like sharing. I’ve still been using the Get-GeoLocation function quite a bit as well as another function I wrote called Get-WhoIsIP. It’s nothing crazy and primarily leverages “http://ipwho.is” API for results. I spend a lot of time using Powershell as a CLI and want a way to quickly look up IP addresses to determine ownership. Sometimes lots of IP addresses. Primarily I would say that I’ve had a lot of occasion to help other people with their Powershell related needs. Here are some highlight topics I can think of: modified a Domain Join script to handle adding a computer to groups as part of the process. A script as part of a Scheduled Task that emails a CSV of specific types of accounts that need to change their password (based on age) A script for someone that needs to rename hundreds of files based on string text found within. Was previously a manual process, now thanks to Powershell (and Regex) something that used to take hours and hours each week takes a couple of seconds. A couple different Active Directory off-boarding scripts to consistently handle removing accounts. A script that resets an Active Directory user’s password expiry clock. Effectively changing the “Password Last Set” time to that of script execution. A series of scripts with Scheduled Task setups/executions, data output, and data collection. Heavily relying on DPAPI and AES encryption for data protection. The “psuedo code” is basically; execute script, save data, acquire data, alert on data. This also involves build scripts, deployment scripts, removal scripts and a few Scheduled Tasks. Was a lot of fun to write. Helped on a CTF that involved a lot of deobfuscating Powershell and finding flags within the code. That’s about it. I’m still looking for the idea that’s going to inspire me to write another Powershell module. For now I’ll keep maintaining my team’s internal module, and my publicly available ProtectStrings module.]]></summary></entry><entry><title type="html">Get-GeoLocation</title><link href="https://courtneybodett.com/Get-GeoLocation/" rel="alternate" type="text/html" title="Get-GeoLocation" /><published>2022-11-13T00:00:00-08:00</published><updated>2022-11-13T00:00:00-08:00</updated><id>https://courtneybodett.com/Get-GeoLocation</id><content type="html" xml:base="https://courtneybodett.com/Get-GeoLocation/"><![CDATA[<h2 id="getting-gps-coordinates-from-a-windows-machine">Getting GPS Coordinates From A Windows Machine</h2>
<p>Since 2020 a lot of organizations have ended up with a more distributed workforce than they previously had.  This means a lot of cloud services, VPNs, and company assets out in the wild.  Some tools will let you build a “geo fence” around your infrastructure and block access to resources if the source is from a country other than your approved list.  Let’s say you only have employees in the United States, you could specify in your cloud services that if anyone attempts to access your email from a country other than the United States, the authentication attempt would be refused.</p>

<p>This is generally accomplished through IP-based geolocation.  We’ve all seen TV and movies where they get a person’s IP address and then magically pinpoint their location down to a couple of feet.  In reality, that’s not true. If you want to see for yourself I used this website quite a bit during testing: <a href="https://www.iplocation.net/">IPlocation.net</a><br />
The site will determine your apparent public IP address (note that a VPN could change this) and then get some publicly available information about the IP from a WHOis lookup.  It will also query several Geo-IP databases and come up with GPS coordinates for your IP.  Using the site above it comes up with a location that’s about 36 miles off.  That’s certainly good enough to determine what country I am in, and maybe even what state I’m in, depending, but that’s about it.  For preventing out-of-country login attempts that’s probably fine, but if I fire up NordVPN and specify Germany as my destination, IPlocation.net will now say I’m in Germany.  That’s how most cloud destinations will view it as well.</p>

<p>For the sake of argument, let’s say you work for an organization that allows remote work within the United States but you want to take a trip out of country and do some remote work.  Maybe a VPN would be enough to convince all of your work resources that you were still in the United States and not throw any alarms.  I wanted to more accurately determine a computer’s location and found that there really aren’t a lot of options out there, except for the Location Services that exist on most Windows computers. One good way to leverage this service is through Powershell and a .NET namespace.</p>

<h2 id="geocoordinatewatcher">GeoCoordinateWatcher</h2>
<p>While searching the internet for how to get GPS coordinates out of a computer I kept running across the same code, or slight variations of it.<br />
From <a href="https://stackoverflow.com/questions/46287792/powershell-getting-gps-coordinates-in-windows-10-using-windows-location-api">StackOverflow</a></p>

<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="w"> 
</span><span class="n">Add-Type</span><span class="w"> </span><span class="nt">-AssemblyName</span><span class="w"> </span><span class="nx">System.Device</span><span class="w"> </span><span class="c">#Required to access System.Device.Location namespace
</span><span class="w">
</span><span class="nv">$GeoWatcher</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-Object</span><span class="w"> </span><span class="nx">System.Device.Location.GeoCoordinateWatcher</span><span class="w"> </span><span class="c">#Create the required object
</span><span class="w">
</span><span class="nv">$GeoWatcher</span><span class="o">.</span><span class="nf">Start</span><span class="p">()</span><span class="w"> </span><span class="c">#Begin resolving current locaton
</span><span class="w">

</span><span class="kr">while</span><span class="w"> </span><span class="p">((</span><span class="nv">$GeoWatcher</span><span class="o">.</span><span class="nf">Status</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="s1">'Ready'</span><span class="p">)</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="p">(</span><span class="nv">$GeoWatcher</span><span class="o">.</span><span class="nf">Permission</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="s1">'Denied'</span><span class="p">))</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Start-Sleep</span><span class="w"> </span><span class="nt">-Milliseconds</span><span class="w"> </span><span class="nx">100</span><span class="w"> </span><span class="c">#Wait for discovery.
</span><span class="w">
</span><span class="p">}</span><span class="w">  

</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$GeoWatcher</span><span class="o">.</span><span class="nf">Permission</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'Denied'</span><span class="p">){</span><span class="w">
    </span><span class="n">Write-Error</span><span class="w"> </span><span class="s1">'Access Denied for Location Information'</span><span class="w">
</span><span class="p">}</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$GeoWatcher</span><span class="o">.</span><span class="nf">Position</span><span class="o">.</span><span class="nf">Location</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select</span><span class="w"> </span><span class="nx">Latitude</span><span class="p">,</span><span class="nx">Longitude</span><span class="w"> </span><span class="c">#Select the relevent results.
</span><span class="w">
</span><span class="p">}</span></code></pre></figure>

<p>From <a href="https://techcommunity.microsoft.com/t5/windows-powershell/accuracy-issues-when-calling-geocoordinatewatcher/m-p/1519830">Microsoft</a></p>

<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="w"> 
</span><span class="c"># Time to see if we can get some location information
</span><span class="w">
</span><span class="c"># Required to access System.Device.Location namespace
</span><span class="w">
</span><span class="n">Add-Type</span><span class="w"> </span><span class="nt">-AssemblyName</span><span class="w"> </span><span class="nx">System.Device</span><span class="w">

</span><span class="c"># Create the required object
</span><span class="w">
</span><span class="nv">$GeoWatcher</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-Object</span><span class="w"> </span><span class="nx">System.Device.Location.GeoCoordinateWatcher</span><span class="w">

</span><span class="c"># Begin resolving current locaton
</span><span class="w">
</span><span class="nv">$GeoWatcher</span><span class="o">.</span><span class="nf">Start</span><span class="p">()</span><span class="w">

</span><span class="kr">while</span><span class="w"> </span><span class="p">((</span><span class="nv">$GeoWatcher</span><span class="o">.</span><span class="nf">Status</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="s1">'Ready'</span><span class="p">)</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="p">(</span><span class="nv">$GeoWatcher</span><span class="o">.</span><span class="nf">Permission</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="s1">'Denied'</span><span class="p">))</span><span class="w"> 
</span><span class="p">{</span><span class="w">
</span><span class="c">#Wait for discovery
</span><span class="w">
</span><span class="n">Start-Sleep</span><span class="w"> </span><span class="nt">-Seconds</span><span class="w"> </span><span class="nx">15</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c">#Select the relevent results.
</span><span class="w">
</span><span class="nv">$LocationArray</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$GeoWatcher</span><span class="o">.</span><span class="nf">Position</span><span class="o">.</span><span class="nf">Location</span></code></pre></figure>

<p>From <a href="https://github.com/I-Am-Jakoby/PowerShell-for-Hackers/blob/main/Functions/Get-GeoLocation.md">Github</a></p>

<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="w"> 
</span><span class="kr">function</span><span class="w"> </span><span class="nf">Get-GeoLocation</span><span class="p">{</span><span class="w">
	</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
	</span><span class="n">Add-Type</span><span class="w"> </span><span class="nt">-AssemblyName</span><span class="w"> </span><span class="nx">System.Device</span><span class="w"> </span><span class="c">#Required to access System.Device.Location namespace
</span><span class="w">
	</span><span class="nv">$GeoWatcher</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-Object</span><span class="w"> </span><span class="nx">System.Device.Location.GeoCoordinateWatcher</span><span class="w"> </span><span class="c">#Create the required object
</span><span class="w">
	</span><span class="nv">$GeoWatcher</span><span class="o">.</span><span class="nf">Start</span><span class="p">()</span><span class="w"> </span><span class="c">#Begin resolving current locaton
</span><span class="w">

	</span><span class="kr">while</span><span class="w"> </span><span class="p">((</span><span class="nv">$GeoWatcher</span><span class="o">.</span><span class="nf">Status</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="s1">'Ready'</span><span class="p">)</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="p">(</span><span class="nv">$GeoWatcher</span><span class="o">.</span><span class="nf">Permission</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="s1">'Denied'</span><span class="p">))</span><span class="w"> </span><span class="p">{</span><span class="w">
		</span><span class="n">Start-Sleep</span><span class="w"> </span><span class="nt">-Milliseconds</span><span class="w"> </span><span class="nx">100</span><span class="w"> </span><span class="c">#Wait for discovery.
</span><span class="w">
	</span><span class="p">}</span><span class="w">  

	</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$GeoWatcher</span><span class="o">.</span><span class="nf">Permission</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'Denied'</span><span class="p">){</span><span class="w">
		</span><span class="n">Write-Error</span><span class="w"> </span><span class="s1">'Access Denied for Location Information'</span><span class="w">
	</span><span class="p">}</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
		</span><span class="nv">$GL</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$GeoWatcher</span><span class="o">.</span><span class="nf">Position</span><span class="o">.</span><span class="nf">Location</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select</span><span class="w"> </span><span class="nx">Latitude</span><span class="p">,</span><span class="nx">Longitude</span><span class="w"> </span><span class="c">#Select the relevent results.
</span><span class="w">
		</span><span class="nv">$GL</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$GL</span><span class="w"> </span><span class="o">-split</span><span class="w"> </span><span class="s2">" "</span><span class="w">
		</span><span class="nv">$Lat</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$GL</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="nf">Substring</span><span class="p">(</span><span class="nx">11</span><span class="p">)</span><span class="w"> </span><span class="o">-replace</span><span class="w"> </span><span class="s2">".$"</span><span class="w">
		</span><span class="nv">$Lon</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$GL</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">.</span><span class="nf">Substring</span><span class="p">(</span><span class="nx">10</span><span class="p">)</span><span class="w"> </span><span class="o">-replace</span><span class="w"> </span><span class="s2">".$"</span><span class="w"> 
		</span><span class="kr">return</span><span class="w"> </span><span class="nv">$Lat</span><span class="p">,</span><span class="w"> </span><span class="nv">$Lon</span><span class="w">


	</span><span class="p">}</span><span class="w">
	</span><span class="p">}</span><span class="w">
    </span><span class="c"># Write Error is just for troubleshooting
</span><span class="w">
    </span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="n">Write-Error</span><span class="w"> </span><span class="s2">"No coordinates found"</span><span class="w"> 
    </span><span class="kr">return</span><span class="w"> </span><span class="s2">"No Coordinates found"</span><span class="w">
    </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="n">SilentlyContinue</span><span class="w">
    </span><span class="p">}</span><span class="w"> 

</span><span class="p">}</span><span class="w">

</span><span class="nv">$Lat</span><span class="p">,</span><span class="w"> </span><span class="nv">$Lon</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-GeoLocation</span></code></pre></figure>

<p>In my preferred editor (Visual Studio Code) I copied over some of these and started experimenting.  It’s clear they’re using a .NET namespace “System.Device” in order to create an instance of a “GeoCoordinateWatcher” object.  I looked this object class up so I could read more about it straight from <a href="https://learn.microsoft.com/en-us/dotnet/api/system.device.location.geocoordinatewatcher?view=netframework-4.8">Microsoft</a>.</p>

<p>I always like stepping through code line by line when I’m writing it and exploring what properties and methods the objects I’m dealing with have.  If we execute the first couple lines we’ve seen in all of these examples we’ll have an object we can play with:</p>

<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="w"> 
</span><span class="n">Add-Type</span><span class="w"> </span><span class="nt">-AssemblyName</span><span class="w"> </span><span class="nx">System.Device</span><span class="w"> 
</span><span class="nv">$GeoWatcher</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-Object</span><span class="w"> </span><span class="nx">System.Device.Location.GeoCoordinateWatcher</span></code></pre></figure>

<p>Then simply call the new object and just see what it says:<br />
<img src="https://courtneybodett.com/assets/images/Get_GeoLocation_01.png" alt="Geo1" /><br />
From this we can see that my “Permission” property is “Granted”, “Status” is “NoData” and “Position” looks like it contains some additional objects.  I can infer from the code examples I pointed out that there must be some occasions where “Permission” is actually “Denied” but nobody seems to talk about that so for now I’ll just be thankful I’m not in that boat and move on.  What’s in the “Position” property?<br />
<img src="https://courtneybodett.com/assets/images/Get_GeoLocation_02.png" alt="Geo2" /><br />
The next step seems like calling the “Start” method on the object.<br />
<img src="https://courtneybodett.com/assets/images/Get_GeoLocation_03.png" alt="Geo3" /><br />
I only waited a second or two before calling the object again and as you can see the “Status” was “Ready” pretty quickly.  If I look at the “Position” property again we see that it actually has value now:<br />
<img src="https://courtneybodett.com/assets/images/Get_GeoLocation_04.png" alt="Geo4" /><br />
Wow, awesome. GPS coordinates and a timestamp.  The GPS coordinates are returned in decimal degrees, which can be copy and pasted right in to Google maps to show you the location.</p>

<h2 id="accuracy">Accuracy</h2>
<p>Where is Location Services getting this information from?  Well, it’s not 100% clear from just this object class alone. The “System.Device” namespace <a href="https://learn.microsoft.com/en-us/dotnet/api/system.device.location?view=netframework-4.8">page</a> has this to say about it:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Location information may come from multiple providers, such as GPS, Wi-Fi triangulation, and cell phone tower triangulation.  
</code></pre></div></div>
<p>I’ve read similar remarks on forums regarding this service, but that’s about as deep as it goes. Through my own testing it seems that if there are no radios (WiFi, GPS, Cellular) on the computer, it will do some type of geolocation look up based on the apparent public IP address.  However, if even a USB WiFi dongle is plugged in the accuracy of the returned GPS coordinates can get as high as within a few yards.  I haven’t gotten to test on a computer with a cellular card in it but I assume it would be similarly accurate.</p>

<h2 id="string-manipulation-the-results">String Manipulation: The Results</h2>
<p>One thing I noticed in most of the examples of the code was that people were specifically calling the longitude and latitude of the “Position” property.  Remember above that when calling the “Position” property it returned a location and a timestamp, and the location was shown as decimal degrees.  Call the “Location” property of the “Position” property and you get a bigger picture:<br />
<img src="https://courtneybodett.com/assets/images/Get_GeoLocation_05.png" alt="Geo5" /><br />
Now we can see that when viewing “Position” property there’s some object formatting taking place to show us the latitude and longitude as comma separated numbers.  In actuality the “Location” property itself has 8 properties and we’re really only interested in the “Latitude” and “Longitude” ones.  There’s examples out there of different ways to manipulate these to get what you want out of them, but I’m always a big fan of piping an object to Get-Member to see what’s available:<br />
<img src="https://courtneybodett.com/assets/images/Get_GeoLocation_06.png" alt="Geo6" /> 
I see a “ToString” method, I wonder what that looks like:<br />
<img src="https://courtneybodett.com/assets/images/Get_GeoLocation_07.png" alt="Geo7" /> <br />
Great, I’m done.  They did all the work for me and all those other examples out there of using Select-Object, or splitting, or whatever can be ignored.</p>

<h2 id="permissions">Permissions</h2>
<p>One thing you see in every example is a reference to the “Permission” property possibly being listed as “Denied”.  I was able to find a couple of computers where this was the case and wanted to understand what was controlling that and how I could possibly overcome it since no one seemed to talk about it in the posts surrounding the above code examples.<br />
The short story is that it depends on whether or not Location Services is being allowed, at both the computer configuration and user configuration level.  I make that distinction because there’s a registry key in both the LocalMachine and CurrentUser hive that applies to this. The path is here:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\location\Value  
</code></pre></div></div>
<p><img src="https://courtneybodett.com/assets/images/Get_GeoLocation_08.png" alt="Geo8" /><br />
At the computer configuration level if this is set to “Deny” then Location Services won’t work and you’ll need admin rights to change that registry key.  If the computer configuration is set to “Allow” but the current user configuration is set to “Deny” then the registry key can be changed without administrative privilege.<br />
To show you what I mean, I’ve set my current user location registry key to “Deny” and recreated my GeoCoordinateWatcher object:<br />
<img src="https://courtneybodett.com/assets/images/Get_GeoLocation_09.png" alt="Geo9" /> 
As you can see the “Permission” property shows “Denied”. Calling the “Start” method on the object does not throw an error, and the “Status” property never changes from “NoData”.<br />
<img src="https://courtneybodett.com/assets/images/Get_GeoLocation_10.png" alt="Geo10" /><br />
The while loop in all of the code examples is reliant on “Permission” being equal to something other than “Denied”, so in my current state the script would flow right through the while loop and move on, possibly writing some kind of error to host.</p>

<p>What if instead we checked to see what the registry key’s value was before trying to start the process?  Then if we have the appropriate permissions, change the value, do the work, and change it back when we’re done.<br />
Let’s look at what I would do instead and then talk about it.</p>

<h2 id="code">Code</h2>

<figure class="highlight"><pre><code class="language-powershell" data-lang="powershell"><span class="w"> 
</span><span class="nv">$IsAdmin</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">Security.Principal.WindowsIdentity</span><span class="p">]::</span><span class="n">GetCurrent</span><span class="p">()</span><span class="o">.</span><span class="nf">Groups</span><span class="w"> </span><span class="o">-contains</span><span class="w"> </span><span class="s1">'S-1-5-32-544'</span><span class="w">
</span><span class="nv">$RegPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\location\"</span><span class="w">
</span><span class="nv">$CompValue</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ItemProperty</span><span class="w"> </span><span class="p">(</span><span class="s2">"HKLM:\"</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nv">$RegPath</span><span class="p">)</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="s2">"Value"</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-ExpandProperty</span><span class="w"> </span><span class="nx">Value</span><span class="w">

</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$CompValue</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="s2">"Allow"</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="nv">$IsAdmin</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Verbose</span><span class="w"> </span><span class="s2">"Admin rights present. Editing registry to allow location services"</span><span class="w">
    </span><span class="n">Set-ItemProperty</span><span class="w"> </span><span class="p">(</span><span class="s2">"HKLM:\"</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nv">$RegPath</span><span class="p">)</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="s2">"Value"</span><span class="w"> </span><span class="nt">-Value</span><span class="w"> </span><span class="s2">"Allow"</span><span class="w">
    </span><span class="nv">$ChangedHKLM</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
    </span><span class="nv">$Continue</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
</span><span class="p">}</span><span class="w"> </span><span class="kr">elseif</span><span class="w"> </span><span class="p">(</span><span class="nv">$CompValue</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"Allow"</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Verbose</span><span class="w"> </span><span class="s2">"Local machine allows for location services"</span><span class="w">
    </span><span class="nv">$Continue</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
</span><span class="p">}</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Verbose</span><span class="w"> </span><span class="s2">"No admin rights and location services denied at machine level"</span><span class="w">
    </span><span class="nv">$Location</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Permission"</span><span class="w">
    </span><span class="nv">$Continue</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Continue</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$UserValue</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ItemProperty</span><span class="w"> </span><span class="p">(</span><span class="s2">"HKCU:\"</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nv">$RegPath</span><span class="p">)</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="s2">"Value"</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="n">SilentlyContinue</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-ExpandProperty</span><span class="w"> </span><span class="nx">Value</span><span class="w">
    </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$UserValue</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"Deny"</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Write-Verbose</span><span class="w"> </span><span class="s2">"User config: location services denied"</span><span class="w">
        </span><span class="n">Write-Verbose</span><span class="w"> </span><span class="s2">"Updating registry for current user to allow location services"</span><span class="w">
        </span><span class="n">Set-ItemProperty</span><span class="w"> </span><span class="p">(</span><span class="s2">"HKCU:\"</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nv">$RegPath</span><span class="p">)</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="s2">"Value"</span><span class="w"> </span><span class="nt">-Value</span><span class="w"> </span><span class="s2">"Allow"</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="n">Add-Type</span><span class="w"> </span><span class="nt">-AssemblyName</span><span class="w"> </span><span class="nx">System.Device</span><span class="w">
    </span><span class="nv">$GeoWatcher</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-Object</span><span class="w"> </span><span class="nx">System.Device.Location.GeoCoordinateWatcher</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="w">
    </span><span class="n">Write-Verbose</span><span class="w"> </span><span class="s2">"Starting GeoCoordinateWatcher"</span><span class="w">
    </span><span class="nv">$GeoWatcher</span><span class="o">.</span><span class="nf">Start</span><span class="p">()</span><span class="w">
    </span><span class="nv">$C</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="w">
    </span><span class="kr">while</span><span class="w"> </span><span class="p">((</span><span class="nv">$GeoWatcher</span><span class="o">.</span><span class="nf">Status</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="s1">'Ready'</span><span class="p">)</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="p">(</span><span class="nv">$Geowatcher</span><span class="o">.</span><span class="nf">Permission</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="s1">'Denied'</span><span class="p">)</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="p">(</span><span class="nv">$C</span><span class="w"> </span><span class="o">-le</span><span class="w"> </span><span class="mi">15</span><span class="p">))</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Write-Verbose</span><span class="w"> </span><span class="s2">"[</span><span class="nv">$C</span><span class="s2">] Waiting 2 seconds for results"</span><span class="w">
        </span><span class="n">Start-Sleep</span><span class="w"> </span><span class="nt">-Seconds</span><span class="w"> </span><span class="nx">2</span><span class="w">
        </span><span class="nv">$C</span><span class="o">++</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="c"># need to wait a little longer to allow for more accurate data. 
</span><span class="w">
    </span><span class="n">Start-Sleep</span><span class="w"> </span><span class="nt">-Seconds</span><span class="w"> </span><span class="nx">2</span><span class="w">
    </span><span class="nv">$Location</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nv">$GeoWatcher</span><span class="o">.</span><span class="nf">Position</span><span class="o">.</span><span class="nf">Location</span><span class="p">)</span><span class="o">.</span><span class="nf">ToString</span><span class="p">()</span><span class="w">
    </span><span class="nv">$GeoWatcher</span><span class="o">.</span><span class="nf">Dispose</span><span class="p">()</span><span class="w">
    </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$UserValue</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"Deny"</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Write-Verbose</span><span class="w"> </span><span class="s2">"Updating registry for current user to revert changes"</span><span class="w">
        </span><span class="n">Set-ItemProperty</span><span class="w"> </span><span class="p">(</span><span class="s2">"HKCU:\"</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nv">$RegPath</span><span class="p">)</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="s2">"Value"</span><span class="w"> </span><span class="nt">-Value</span><span class="w"> </span><span class="s2">"Deny"</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$ChangedHKLM</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Verbose</span><span class="w"> </span><span class="s2">"Updating registry for local machine to revert changes"</span><span class="w">
    </span><span class="n">Set-ItemProperty</span><span class="w"> </span><span class="p">(</span><span class="s2">"HKLM:\"</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nv">$RegPath</span><span class="p">)</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="s2">"Value"</span><span class="w"> </span><span class="nt">-Value</span><span class="w"> </span><span class="s2">"Deny"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
    </span><span class="nx">Computer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">Env</span><span class="p">:</span><span class="nv">COMPUTERNAME</span><span class="w">
    </span><span class="nx">Location</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Location</span><span class="w">
    </span><span class="nx">NetAdapter</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">(</span><span class="nx">Get</span><span class="err">-</span><span class="nx">NetAdapter</span><span class="w"> </span><span class="err">-</span><span class="nx">Physical</span><span class="w"> </span><span class="err">|</span><span class="w"> </span><span class="nx">Where</span><span class="err">-</span><span class="nx">Object</span><span class="w"> </span><span class="p">{</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Status</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"Up"</span><span class="p">}</span><span class="w"> </span><span class="err">|</span><span class="w"> </span><span class="nx">Select</span><span class="err">-</span><span class="nx">Object</span><span class="w"> </span><span class="err">-</span><span class="nx">ExpandProperty</span><span class="w"> </span><span class="nx">Name</span><span class="err">)</span><span class="w"> </span><span class="err">-</span><span class="nx">Join</span><span class="w"> </span><span class="s1">','</span><span class="w">
</span><span class="p">}</span></code></pre></figure>

<p>Let’s talk through this.<br />
<img src="https://courtneybodett.com/assets/images/Get_GeoLocation_Code_01.png" alt="GeoCode1" /><br />
The first line is a method for determining if the current running user is in the local Administrators group.  Then we save the bulk of a registry path for use later, and then check the HKLM value in the registry to see if Location Services are allowed.<br />
When I tested this on several Domain joined computers it worked great (and was new to me) but as I write this on my personal computer I see some interesting behavior.  My local account (a Microsoft account) <strong>is</strong> in the local Administrators group, but that first line returns false.  Checking manually in the GUI it shows that I <strong>am</strong> in fact in that group, but the “Groups” property from that code doesn’t show that I’m in that group.  So I flipped the logic around and instead got the members of that group to see if it contains the SID of the current user, and that returns true.<br />
<img src="https://courtneybodett.com/assets/images/Get_GeoLocation_Code_02.png" alt="GeoCode2" /><br />
Ok, next section.<br />
<img src="https://courtneybodett.com/assets/images/Get_GeoLocation_Code_03.png" alt="GeoCode3" /><br />
A little if/elseif/else action here.  If Location Services is currently not being allowed at the computer level, <em>and</em> we’re running as admin, then change the registry and define a couple of variables. Else, if the value is already  set to “Allow” then just define a variable.  Else, finally, if we’re not running as admin then define that same variable as “False”.  If that were the case, then this next section that checks that variable first, would be skipped.<br />
<img src="https://courtneybodett.com/assets/images/Get_GeoLocation_Code_04.png" alt="GeoCode4" /><br />
This is where we do the actual work.  We’ve checked if Location Services is allowed at the computer level, and if that worked then “Continue” will be true. We start off by essentially doing the same registry check but for the Current User hive.  If Location Services isn’t being allowed then we’ll change it to allow.  Then we add our .NET namespace and create out GeoCoordinateWatcher object. Note the “(1)” at the end of that. This denotes that it should be created with “High Accuracy” mode.  Since we’re only going to be leveraging it for a few seconds, I see no downside to this.</p>

<p>Then we start the process and I also start a counter by defining “$C” as zero.  No one else had this in their code, but I wasn’t sure what the maximum potential amount of time this process might take was, and I didn’t want to accidentally create an infinite loop so my while loop has 3 conditions.  The final condition, the counter, must be less than or equal to 15.  With the Start-Sleep statement within the loop set to 2 seconds this means the maximum amount of time this loop could go on for is approximately 30 seconds.</p>

<p>I then found through some testing that if you just immediately go from “Ready” to checking for the location that it may not actually be ready.  Unclear what’s happening in the background, but if you just wait 2 more seconds it seems to allow for enough time.  Then I take the positional data and use its own “ToString” method to save the GPS coordinates to a variable.  Dispose of the GeoWatcher object and if the user’s registry value was changed by this script, change it back.  The next section does the same thing for the computer registry hive.<br />
<img src="https://courtneybodett.com/assets/images/Get_GeoLocation_Code_05.png" alt="GeoCode5" /><br />
Then finally the last section.<br />
<img src="https://courtneybodett.com/assets/images/Get_GeoLocation_Code_06.png" alt="GeoCode6" /><br />
This just creates, and returns, a PSCustomObject with three properties: The current computer name, the GPS coordinates, and the name of the connected network adapters.  Let’s execute all of this and see what that looks like.<br />
<img src="https://courtneybodett.com/assets/images/Get_GeoLocation_Code_07.png" alt="GeoCode7" /><br />
Great! Now we have the name of the computer, some pretty accurate looking GPS coordinates, and the network adapter(s) that were present at the time.  I included this because the presence of WiFi seems to be a pretty big factor in how accurate the GPS coordinates are and I thought it might be nice for reference.</p>

<p>The reason it was written the way it was is because I figured more often than not I’m going to be running this against a remote computer and might want to pass this code as a script block.  With that in mind, this is all collected together as a function called Get-GeoLocation, which has a parameter called “ComputerName” for specifying a remote computer you wish to run it against.  This has only been tested in one Active Directory environment so far, but the code is available on Github in case you want to play around with it on your own.<br />
<a href="https://github.com/grey0ut/Powershell-General/blob/main/Get-GeoLocation.ps1">GitHub Get-GeoLocation</a></p>

<h2 id="closing-thoughts">Closing Thoughts</h2>
<p>When I started down this rabbit hole of trying to reliably determine a computer’s location I really did <em>not</em> want to use Powershell to do it.  I know I have a tendency to use Powershell for everything and I wanted to use tools that we already had available to us.  Unfortunately everything seems to rely on Geo-IP databases which return fairly inaccurate results.  This was also a fun exercise in taking “found in the wild” code a step further and hydrating it with some more error handling, and features.<br />
Remember to check out Microsoft’s documentation when you can, pipe to Get-Member, and just explore in general.  It’s interesting what you’ll find.</p>]]></content><author><name>Courtney Bodett</name></author><summary type="html"><![CDATA[Getting GPS Coordinates From A Windows Machine Since 2020 a lot of organizations have ended up with a more distributed workforce than they previously had. This means a lot of cloud services, VPNs, and company assets out in the wild. Some tools will let you build a “geo fence” around your infrastructure and block access to resources if the source is from a country other than your approved list. Let’s say you only have employees in the United States, you could specify in your cloud services that if anyone attempts to access your email from a country other than the United States, the authentication attempt would be refused. This is generally accomplished through IP-based geolocation. We’ve all seen TV and movies where they get a person’s IP address and then magically pinpoint their location down to a couple of feet. In reality, that’s not true. If you want to see for yourself I used this website quite a bit during testing: IPlocation.net The site will determine your apparent public IP address (note that a VPN could change this) and then get some publicly available information about the IP from a WHOis lookup. It will also query several Geo-IP databases and come up with GPS coordinates for your IP. Using the site above it comes up with a location that’s about 36 miles off. That’s certainly good enough to determine what country I am in, and maybe even what state I’m in, depending, but that’s about it. For preventing out-of-country login attempts that’s probably fine, but if I fire up NordVPN and specify Germany as my destination, IPlocation.net will now say I’m in Germany. That’s how most cloud destinations will view it as well. For the sake of argument, let’s say you work for an organization that allows remote work within the United States but you want to take a trip out of country and do some remote work. Maybe a VPN would be enough to convince all of your work resources that you were still in the United States and not throw any alarms. I wanted to more accurately determine a computer’s location and found that there really aren’t a lot of options out there, except for the Location Services that exist on most Windows computers. One good way to leverage this service is through Powershell and a .NET namespace. GeoCoordinateWatcher While searching the internet for how to get GPS coordinates out of a computer I kept running across the same code, or slight variations of it. From StackOverflow Add-Type -AssemblyName System.Device #Required to access System.Device.Location namespace $GeoWatcher = New-Object System.Device.Location.GeoCoordinateWatcher #Create the required object $GeoWatcher.Start() #Begin resolving current locaton while (($GeoWatcher.Status -ne 'Ready') -and ($GeoWatcher.Permission -ne 'Denied')) { Start-Sleep -Milliseconds 100 #Wait for discovery. } if ($GeoWatcher.Permission -eq 'Denied'){ Write-Error 'Access Denied for Location Information' } else { $GeoWatcher.Position.Location | Select Latitude,Longitude #Select the relevent results. } From Microsoft # Time to see if we can get some location information # Required to access System.Device.Location namespace Add-Type -AssemblyName System.Device # Create the required object $GeoWatcher = New-Object System.Device.Location.GeoCoordinateWatcher # Begin resolving current locaton $GeoWatcher.Start() while (($GeoWatcher.Status -ne 'Ready') -and ($GeoWatcher.Permission -ne 'Denied')) { #Wait for discovery Start-Sleep -Seconds 15 } #Select the relevent results. $LocationArray = $GeoWatcher.Position.Location From Github function Get-GeoLocation{ try { Add-Type -AssemblyName System.Device #Required to access System.Device.Location namespace $GeoWatcher = New-Object System.Device.Location.GeoCoordinateWatcher #Create the required object $GeoWatcher.Start() #Begin resolving current locaton while (($GeoWatcher.Status -ne 'Ready') -and ($GeoWatcher.Permission -ne 'Denied')) { Start-Sleep -Milliseconds 100 #Wait for discovery. } if ($GeoWatcher.Permission -eq 'Denied'){ Write-Error 'Access Denied for Location Information' } else { $GL = $GeoWatcher.Position.Location | Select Latitude,Longitude #Select the relevent results. $GL = $GL -split " " $Lat = $GL[0].Substring(11) -replace ".$" $Lon = $GL[1].Substring(10) -replace ".$" return $Lat, $Lon } } # Write Error is just for troubleshooting catch {Write-Error "No coordinates found" return "No Coordinates found" -ErrorAction SilentlyContinue } } $Lat, $Lon = Get-GeoLocation In my preferred editor (Visual Studio Code) I copied over some of these and started experimenting. It’s clear they’re using a .NET namespace “System.Device” in order to create an instance of a “GeoCoordinateWatcher” object. I looked this object class up so I could read more about it straight from Microsoft. I always like stepping through code line by line when I’m writing it and exploring what properties and methods the objects I’m dealing with have. If we execute the first couple lines we’ve seen in all of these examples we’ll have an object we can play with: Add-Type -AssemblyName System.Device $GeoWatcher = New-Object System.Device.Location.GeoCoordinateWatcher Then simply call the new object and just see what it says: From this we can see that my “Permission” property is “Granted”, “Status” is “NoData” and “Position” looks like it contains some additional objects. I can infer from the code examples I pointed out that there must be some occasions where “Permission” is actually “Denied” but nobody seems to talk about that so for now I’ll just be thankful I’m not in that boat and move on. What’s in the “Position” property? The next step seems like calling the “Start” method on the object. I only waited a second or two before calling the object again and as you can see the “Status” was “Ready” pretty quickly. If I look at the “Position” property again we see that it actually has value now: Wow, awesome. GPS coordinates and a timestamp. The GPS coordinates are returned in decimal degrees, which can be copy and pasted right in to Google maps to show you the location. Accuracy Where is Location Services getting this information from? Well, it’s not 100% clear from just this object class alone. The “System.Device” namespace page has this to say about it: Location information may come from multiple providers, such as GPS, Wi-Fi triangulation, and cell phone tower triangulation. I’ve read similar remarks on forums regarding this service, but that’s about as deep as it goes. Through my own testing it seems that if there are no radios (WiFi, GPS, Cellular) on the computer, it will do some type of geolocation look up based on the apparent public IP address. However, if even a USB WiFi dongle is plugged in the accuracy of the returned GPS coordinates can get as high as within a few yards. I haven’t gotten to test on a computer with a cellular card in it but I assume it would be similarly accurate. String Manipulation: The Results One thing I noticed in most of the examples of the code was that people were specifically calling the longitude and latitude of the “Position” property. Remember above that when calling the “Position” property it returned a location and a timestamp, and the location was shown as decimal degrees. Call the “Location” property of the “Position” property and you get a bigger picture: Now we can see that when viewing “Position” property there’s some object formatting taking place to show us the latitude and longitude as comma separated numbers. In actuality the “Location” property itself has 8 properties and we’re really only interested in the “Latitude” and “Longitude” ones. There’s examples out there of different ways to manipulate these to get what you want out of them, but I’m always a big fan of piping an object to Get-Member to see what’s available: I see a “ToString” method, I wonder what that looks like: Great, I’m done. They did all the work for me and all those other examples out there of using Select-Object, or splitting, or whatever can be ignored. Permissions One thing you see in every example is a reference to the “Permission” property possibly being listed as “Denied”. I was able to find a couple of computers where this was the case and wanted to understand what was controlling that and how I could possibly overcome it since no one seemed to talk about it in the posts surrounding the above code examples. The short story is that it depends on whether or not Location Services is being allowed, at both the computer configuration and user configuration level. I make that distinction because there’s a registry key in both the LocalMachine and CurrentUser hive that applies to this. The path is here: Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\location\Value At the computer configuration level if this is set to “Deny” then Location Services won’t work and you’ll need admin rights to change that registry key. If the computer configuration is set to “Allow” but the current user configuration is set to “Deny” then the registry key can be changed without administrative privilege. To show you what I mean, I’ve set my current user location registry key to “Deny” and recreated my GeoCoordinateWatcher object: As you can see the “Permission” property shows “Denied”. Calling the “Start” method on the object does not throw an error, and the “Status” property never changes from “NoData”. The while loop in all of the code examples is reliant on “Permission” being equal to something other than “Denied”, so in my current state the script would flow right through the while loop and move on, possibly writing some kind of error to host. What if instead we checked to see what the registry key’s value was before trying to start the process? Then if we have the appropriate permissions, change the value, do the work, and change it back when we’re done. Let’s look at what I would do instead and then talk about it. Code $IsAdmin = [Security.Principal.WindowsIdentity]::GetCurrent().Groups -contains 'S-1-5-32-544' $RegPath = "Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\location\" $CompValue = Get-ItemProperty ("HKLM:\" + $RegPath) -Name "Value" | Select-Object -ExpandProperty Value if ($CompValue -ne "Allow" -and $IsAdmin) { Write-Verbose "Admin rights present. Editing registry to allow location services" Set-ItemProperty ("HKLM:\" + $RegPath) -Name "Value" -Value "Allow" $ChangedHKLM = $true $Continue = $true } elseif ($CompValue -eq "Allow") { Write-Verbose "Local machine allows for location services" $Continue = $true } else { Write-Verbose "No admin rights and location services denied at machine level" $Location = "Permission" $Continue = $false } if ($Continue) { $UserValue = Get-ItemProperty ("HKCU:\" + $RegPath) -Name "Value" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Value if ($UserValue -eq "Deny") { Write-Verbose "User config: location services denied" Write-Verbose "Updating registry for current user to allow location services" Set-ItemProperty ("HKCU:\" + $RegPath) -Name "Value" -Value "Allow" } Add-Type -AssemblyName System.Device $GeoWatcher = New-Object System.Device.Location.GeoCoordinateWatcher(1) Write-Verbose "Starting GeoCoordinateWatcher" $GeoWatcher.Start() $C = 0 while (($GeoWatcher.Status -ne 'Ready') -and ($Geowatcher.Permission -ne 'Denied') -and ($C -le 15)) { Write-Verbose "[$C] Waiting 2 seconds for results" Start-Sleep -Seconds 2 $C++ } # need to wait a little longer to allow for more accurate data. Start-Sleep -Seconds 2 $Location = ($GeoWatcher.Position.Location).ToString() $GeoWatcher.Dispose() if ($UserValue -eq "Deny") { Write-Verbose "Updating registry for current user to revert changes" Set-ItemProperty ("HKCU:\" + $RegPath) -Name "Value" -Value "Deny" } } if ($ChangedHKLM) { Write-Verbose "Updating registry for local machine to revert changes" Set-ItemProperty ("HKLM:\" + $RegPath) -Name "Value" -Value "Deny" } [PSCustomObject]@{ Computer = $Env:COMPUTERNAME Location = $Location NetAdapter = (Get-NetAdapter -Physical | Where-Object {$_.Status -eq "Up"} | Select-Object -ExpandProperty Name) -Join ',' } Let’s talk through this. The first line is a method for determining if the current running user is in the local Administrators group. Then we save the bulk of a registry path for use later, and then check the HKLM value in the registry to see if Location Services are allowed. When I tested this on several Domain joined computers it worked great (and was new to me) but as I write this on my personal computer I see some interesting behavior. My local account (a Microsoft account) is in the local Administrators group, but that first line returns false. Checking manually in the GUI it shows that I am in fact in that group, but the “Groups” property from that code doesn’t show that I’m in that group. So I flipped the logic around and instead got the members of that group to see if it contains the SID of the current user, and that returns true. Ok, next section. A little if/elseif/else action here. If Location Services is currently not being allowed at the computer level, and we’re running as admin, then change the registry and define a couple of variables. Else, if the value is already set to “Allow” then just define a variable. Else, finally, if we’re not running as admin then define that same variable as “False”. If that were the case, then this next section that checks that variable first, would be skipped. This is where we do the actual work. We’ve checked if Location Services is allowed at the computer level, and if that worked then “Continue” will be true. We start off by essentially doing the same registry check but for the Current User hive. If Location Services isn’t being allowed then we’ll change it to allow. Then we add our .NET namespace and create out GeoCoordinateWatcher object. Note the “(1)” at the end of that. This denotes that it should be created with “High Accuracy” mode. Since we’re only going to be leveraging it for a few seconds, I see no downside to this. Then we start the process and I also start a counter by defining “$C” as zero. No one else had this in their code, but I wasn’t sure what the maximum potential amount of time this process might take was, and I didn’t want to accidentally create an infinite loop so my while loop has 3 conditions. The final condition, the counter, must be less than or equal to 15. With the Start-Sleep statement within the loop set to 2 seconds this means the maximum amount of time this loop could go on for is approximately 30 seconds. I then found through some testing that if you just immediately go from “Ready” to checking for the location that it may not actually be ready. Unclear what’s happening in the background, but if you just wait 2 more seconds it seems to allow for enough time. Then I take the positional data and use its own “ToString” method to save the GPS coordinates to a variable. Dispose of the GeoWatcher object and if the user’s registry value was changed by this script, change it back. The next section does the same thing for the computer registry hive. Then finally the last section. This just creates, and returns, a PSCustomObject with three properties: The current computer name, the GPS coordinates, and the name of the connected network adapters. Let’s execute all of this and see what that looks like. Great! Now we have the name of the computer, some pretty accurate looking GPS coordinates, and the network adapter(s) that were present at the time. I included this because the presence of WiFi seems to be a pretty big factor in how accurate the GPS coordinates are and I thought it might be nice for reference. The reason it was written the way it was is because I figured more often than not I’m going to be running this against a remote computer and might want to pass this code as a script block. With that in mind, this is all collected together as a function called Get-GeoLocation, which has a parameter called “ComputerName” for specifying a remote computer you wish to run it against. This has only been tested in one Active Directory environment so far, but the code is available on Github in case you want to play around with it on your own. GitHub Get-GeoLocation Closing Thoughts When I started down this rabbit hole of trying to reliably determine a computer’s location I really did not want to use Powershell to do it. I know I have a tendency to use Powershell for everything and I wanted to use tools that we already had available to us. Unfortunately everything seems to rely on Geo-IP databases which return fairly inaccurate results. This was also a fun exercise in taking “found in the wild” code a step further and hydrating it with some more error handling, and features. Remember to check out Microsoft’s documentation when you can, pipe to Get-Member, and just explore in general. It’s interesting what you’ll find.]]></summary></entry></feed>