<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.2.2">Jekyll</generator><link href="http://greybeard.ai/feed.xml" rel="self" type="application/atom+xml" /><link href="http://greybeard.ai/" rel="alternate" type="text/html" /><updated>2025-07-04T18:58:38+00:00</updated><id>http://greybeard.ai/feed.xml</id><title type="html">greybeard.ai</title><subtitle>The Install Wizard.</subtitle><author><name>Danial Bulloch</name></author><entry><title type="html">3D Printing: Alice in Wonderland Shadowbox</title><link href="http://greybeard.ai/2025/03/29/AliceInWonderland" rel="alternate" type="text/html" title="3D Printing: Alice in Wonderland Shadowbox" /><published>2025-03-29T16:00:00+00:00</published><updated>2025-03-29T16:00:00+00:00</updated><id>http://greybeard.ai/2025/03/29/AliceInWonderland</id><content type="html" xml:base="http://greybeard.ai/2025/03/29/AliceInWonderland"><![CDATA[<p>Alright, so perhaps I was optimistic on the site move and the speed at which I would release new posts, but here we are.</p>

<p>I’ve been really enjoying HueForge lately, having made several shadow boxes of family photos as gifts, I decided I wanted to stretch my legs a little, and really test my HueForge and design skills.</p>

<p>I wont bury the lead, here’s the finished product, stick around if you want to know the hows.</p>

<p><img src="/assets/images/alice/20250310_Alice.webp" alt="Alice in Wonderland" /></p>

<p>With all the fun stuff out of the way, let’s get down to the real details. So first of all, I used a mix of tools to get the job done. You can probably guess a few.</p>

<ul>
  <li><a href="https://shop.thehueforge.com/">HueForge</a></li>
  <li><a href="https://inkscape.org/">Inkscape</a></li>
  <li><a href="https://www.blender.org/">Blender</a></li>
  <li><a href="https://www.gimp.org/">GIMP</a></li>
  <li><a href="https://www.prusa3d.com/page/prusaslicer_424/">PrusaSlicer</a></li>
  <li><a href="https://www.freecad.org/">FreeCAD</a></li>
</ul>

<p>I’m going to be glosing over a lot of the finer details here, because nobody wants a book. The jist of it was I wanted to make a shadowbox, so I needed to isolate at least 2 distinct layers. A simple background and foreground was enough for this project. So first stop, GIMP.</p>

<h1 id="the-background">The Background</h1>

<p>I used it to remove Alice and do a ton of touchup work on the original Disney Classics #8 cover. I had to really flatten things out, denoise, redraw a few lines to subtle for the later process.</p>

<p>I moved it from GIMP over to HueForge and started playing with settings and colors. This took a lot of fiddling.</p>

<p><img src="/assets/images/alice/HueForge Alice.png" alt="HueForge Screenshot" /></p>

<p>I had to pass it back and forth from GIMP to HueForge to get the colors where I wanted them and make sure it was clean enough to successfully print. Specifically, the compression artifacts from the scans I could find online were quite brutal on the process. For photos, I find compression to be fine, but for drawn art like this, it made a huge difference.</p>

<p><img src="/assets/images/alice/Background Finished.webp" alt="Background Printed Result" /></p>

<p>Not bad. It had quite a bit of stringing, but a large part of that was my very old white filament. I hit it with a heatgun and that cleaned up most of them.</p>

<p>If you are a little unsure about how HueForge works, here’s a closeup. Basically it only prints a single color per layer, changing between them.</p>

<p><img src="/assets/images/alice/Background Closeup.webp" alt="Background Printed Closeup" /></p>

<p>My first few attempts failed because I disabled the wipe tower, thinking it wasn’t needed. Boy was I wrong. At least on my Prusa MK3 with MMU2S it really needed it to properly change.</p>

<h1 id="the-foreground">The Foreground</h1>

<p>With the background out of the way, it came time to forge a new path. HueForge is fun, but I’m already pretty good at that. Time to try something new. I ran through a lot of ideas, including hand painting Alice (A skill I don’t have), laser etching her outline on the acrylic and flood filling the details, but I finally settled on keeping her 3D printed, but doing a very different (although technically similar) process.</p>

<p>I’ve played around with transparent first layers before, it makes a great technique to create a clean first layer, while having complex detail that might not want to stick to the bed. This seemed like a great way to get what I wanted. Unfortunately, the source image was both highly compressed, and had too many colors, so there was but one option, redraw her in vector.</p>

<p>In comes Inkscape. I imported the jpg of her into inkscape and used it to trace. I couldn’t use the original design 100%, and had to make some choices for the 3D printing medium. Specifically, small details had to be eliminated, lines had to be significantly thickened, and colors had to be simplified. Once the design and art work was done, I duplicated my layer in Inkscape, then used it to merge all the various elements together, then split everything back out by color. I exported only the “Selected” elements so that all my work layers weren’t included. I repeated the process for a fully black silhouette version. With 2 SVGs in hand, I went into PrusaSlice and imported both as parts. I set my layer height to 0.2mm, and so the SVGs I imported I, likewise, set to 0.2mm on import.</p>

<p>This may sound like we are almost there, but no, I had more work to do still. I took the color segmented SVG and exported it to STL. I then opened that STL in Blender to do a little post processing work, cleaning up some conversion issues. Then back to STL and imported it back into PrusaSlicer. This took a ton of time to refine, but eventually I got what I wanted.</p>

<p>Finally, now that everything was in the slicer I built the print part by part. Layer 1 was the silhouette, printed in transparent. If you have a keen eye, you might have noticed that the final version of Alice had 5 primary colors, and the Prusa MK3 MMU2 only supports 5 colors, so how did I get transparent worked in? Just a simple manual color swap on the spool while other colors were being used.</p>

<p>Continuing the layering process, layers 2 and 3 were color layers, but I alternated the colors in a few areas like the skirt and the skin to give different tones. The skirt is layer 2 white, layer 3 blue. And the skin is layer 2 gold, layer 3 red. It just gives me a little more dynamic range with my limited palette.</p>

<p><img src="/assets/images/alice/Alice Start Print.webp" alt="Alice at the early part of Layer 2" /></p>

<p>Hopefully this photo helps to highlight the process. You can see the first layer is transparent, that’s our front most layer and it provides a solid bed for the more detailed layers that come next.</p>

<p><img src="/assets/images/alice/Alice Completed Print Facedown.webp" alt="Alice Print Finished" /></p>

<p>Oh, that’s not very impressive at all. Wait, perhaps if we remove it from the bed…</p>

<p><img src="/assets/images/alice/Alice Completed Print Flipped.webp" alt="Alice Print Finished and Flipped" /></p>

<p>That’s much better. She wasn’t perfect, but pretty sharp none the less. My biggest mistake was going with 0.2mm layer heights. HueForge likes 0.08mm, which really lets the colors mix much better. But at this point I was seeing the finish line and wasn’t willing to do the work that was needed to lower that down.</p>

<h1 id="frame-and-acrylic">Frame and Acrylic</h1>

<p>The frame was simply generated using my <a href="https://www.printables.com/model/1120635-frameshadowbox-generator">Frame/Shadowbox generator you can find on Printables</a>. This was made for some previous projects, but I’ve found it quite useful time and time again. It was designed in FreeCAD and has a nice spreadsheet embedded in it so you can easily generate frames of any size.</p>

<p>The acrylic cover was simply cut on my laser cutter, nothing particularly special about it, just a nice rectangular piece that could easily be mistaken for glass.</p>

<h1 id="final-assembly--conclusion">Final Assembly &amp; Conclusion</h1>

<p>All that was left was to put it together. I printed a small spacer to hold Alice well above the background, then glued her and the spacer to the background. In hindsight I could have probably printed the space on Alice, and possibly put some holes to screw from behind the background, but glue worked just fine. Then everything slotted into the frame. I also printed a space to give Alice room between the background the the Acrylic. The frame has a nice snap fit, and has a built in comb/rake for hanging on a nail.</p>

<p>The gift went over well. I didn’t keep track of time on this project, but I’d say in excess of 20 solid hours of work went into it. It was worth it, but boy was I ready to be done by the end of it, it was probably 3 weekends spent on it.</p>

<h1 id="comments-or-questions">Comments or Questions?</h1>

<p>I have no idea how to do this right, but I’ve posted this at <a href="https://lemmy.world/post/27559714">Lemmy.World’s 3D Printing Community</a>. A great place to leave a comment or question.</p>]]></content><author><name>Danial Bulloch</name></author><category term="3dprinting" /><summary type="html"><![CDATA[Alright, so perhaps I was optimistic on the site move and the speed at which I would release new posts, but here we are.]]></summary></entry><entry><title type="html">The Big Move</title><link href="http://greybeard.ai/2022/09/03/thebigmove" rel="alternate" type="text/html" title="The Big Move" /><published>2022-09-03T16:00:00+00:00</published><updated>2022-09-03T16:00:00+00:00</updated><id>http://greybeard.ai/2022/09/03/thebigmove</id><content type="html" xml:base="http://greybeard.ai/2022/09/03/thebigmove"><![CDATA[<p>When I started this new website in 2018(Over 4 years ago, my god), I used a minimal Google Cloud Compute VM to host it. Running Nginx and nothing else, it did a really basic serve of the webpage. I eventually sent it through CloudFlare’s proxy for a little bit of caching and DDOS protection, but my pipeline stayed the same pretty much this entire time.</p>

<p>Today marks a big move, I’m now storing my website in git, and using CloudFlare pages to render and host them. This means no more hosting for me at all. Completely takes the risk of web server exploits off my plate, and simplifies the build system so all I have to do is commit and sync and within a few minutes, my website will be public. I’m excited to see the new simplified flow, and hope it will encurage me to publish more often.</p>]]></content><author><name>Danial Bulloch</name></author><category term="jibberjabber" /><summary type="html"><![CDATA[When I started this new website in 2018(Over 4 years ago, my god), I used a minimal Google Cloud Compute VM to host it. Running Nginx and nothing else, it did a really basic serve of the webpage. I eventually sent it through CloudFlare’s proxy for a little bit of caching and DDOS protection, but my pipeline stayed the same pretty much this entire time.]]></summary></entry><entry><title type="html">Listing Groups, Owners, and Members from Microsoft 365 with Powershell</title><link href="http://greybeard.ai/2021/06/02/PowershellListingM365Groups" rel="alternate" type="text/html" title="Listing Groups, Owners, and Members from Microsoft 365 with Powershell" /><published>2021-06-02T00:33:00+00:00</published><updated>2021-06-02T00:33:00+00:00</updated><id>http://greybeard.ai/2021/06/02/PowershellListingM365Groups</id><content type="html" xml:base="http://greybeard.ai/2021/06/02/PowershellListingM365Groups"><![CDATA[<p><img src="/assets/images/20210601-GroupPowershell.png" alt="Group Powershell" /></p>

<h1 id="why-do-they-need-to-make-this-so-difficult">Why Do They Need To Make This So Difficult?</h1>

<p>A client requested a list of groups in Microsoft 365, members, and the owners/managers of those groups. My first impulse was to use <code class="language-plaintext highlighter-rouge">Get-MsolGroup</code> and <code class="language-plaintext highlighter-rouge">Get-MsolGroupMember</code>, but I quickly realized that it didn’t return the owner. So I had to go to a completely different module and compile two different list of groups. Microsoft just can’t make this easy.</p>

<h1 id="code-time">Code Time</h1>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Import-Module</span><span class="w"> </span><span class="nx">ExchangeOnlineManagement</span><span class="w">
</span><span class="n">Connect-ExchangeOnline</span><span class="w">
</span><span class="nv">$DistroGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-DistributionGroup</span><span class="w">
</span><span class="nv">$UnifiedGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-UnifiedGroup</span><span class="w">
</span><span class="nv">$DistroGroupMembers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$DistroGroups</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ForEach-Object</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">Name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="err">.</span><span class="nx">DisplayName</span><span class="w">
        </span><span class="nx">Type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"DistroGroup"</span><span class="w">
        </span><span class="nx">ManagedBy</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="err">.</span><span class="nx">ManagedBy</span><span class="w">
        </span><span class="nx">Members</span><span class="o">=</span><span class="err">(</span><span class="nx">Get</span><span class="err">-</span><span class="nx">DistributionGroupMember</span><span class="w"> </span><span class="err">-</span><span class="nx">Identity</span><span class="w"> </span><span class="bp">$_</span><span class="err">.</span><span class="nx">SamAccountName</span><span class="err">).</span><span class="nx">Name</span><span class="w"> </span><span class="err">-</span><span class="nx">join</span><span class="w"> </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">$UnifiedGroupMembers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$UnifiedGroups</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ForEach-Object</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">Name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="err">.</span><span class="nx">DisplayName</span><span class="w">
        </span><span class="nx">Type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Microsoft 365 Group"</span><span class="w">
        </span><span class="nx">ManagedBy</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="err">.</span><span class="nx">ManagedBy</span><span class="w">
        </span><span class="nx">Members</span><span class="o">=</span><span class="err">(</span><span class="bp">$_</span><span class="w"> </span><span class="err">|</span><span class="w"> </span><span class="nx">Get</span><span class="err">-</span><span class="nx">UnifiedGroupLinks</span><span class="w"> </span><span class="err">-</span><span class="nx">linktype</span><span class="w"> </span><span class="nx">Members</span><span class="err">).</span><span class="nx">Name</span><span class="w"> </span><span class="err">-</span><span class="nx">join</span><span class="w"> </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">$DistroGroupMembers</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nv">$UnifiedGroupMembers</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Export-CSV</span><span class="w"> </span><span class="nt">-path</span><span class="w"> </span><span class="s2">"</span><span class="si">$(</span><span class="p">[</span><span class="n">Environment</span><span class="p">]::</span><span class="n">GetFolderPath</span><span class="p">(</span><span class="s1">'MyDocuments'</span><span class="si">)</span><span class="s2">)\Group Report </span><span class="si">$(</span><span class="n">get-date</span><span class="w"> </span><span class="nt">-Format</span><span class="w"> </span><span class="s2">"yyyyMMdd"</span><span class="p">)</span><span class="o">.</span><span class="nf">csv</span><span class="s2">" -NoTypeInformation
</span></code></pre></div></div>

<h1 id="bonus-extra-credit">Bonus Extra Credit</h1>

<p>Here’s something new. I’ve decided to throw in some extra credit. If you are running this at home, try throwing in <a href="/2021/03/24/PowershellOutGridView">Out-GridView</a> into middle of the output so you can manually filter what gets saved int the report.</p>]]></content><author><name>Danial Bulloch</name></author><category term="powershell" /><summary type="html"><![CDATA[Why Do They Need To Make This So Difficult? A client requested a list of groups in Microsoft 365, members, and the owners/managers of those groups. My first impulse was to use Get-MsolGroup and Get-MsolGroupMember, but I quickly realized that it didn’t return the owner. So I had to go to a completely different module and compile two different list of groups. Microsoft just can’t make this easy. Code Time Import-Module ExchangeOnlineManagement Connect-ExchangeOnline $DistroGroups = Get-DistributionGroup $UnifiedGroups = Get-UnifiedGroup $DistroGroupMembers = $DistroGroups | ForEach-Object{ [PSCustomObject]@{ Name = $_.DisplayName Type = "DistroGroup" ManagedBy = $_.ManagedBy Members=(Get-DistributionGroupMember -Identity $_.SamAccountName).Name -join ", " } } $UnifiedGroupMembers = $UnifiedGroups | ForEach-Object{ [PSCustomObject]@{ Name = $_.DisplayName Type = "Microsoft 365 Group" ManagedBy = $_.ManagedBy Members=($_ | Get-UnifiedGroupLinks -linktype Members).Name -join ", " } } $DistroGroupMembers + $UnifiedGroupMembers | Export-CSV -path "$([Environment]::GetFolderPath('MyDocuments'))\Group Report $(get-date -Format "yyyyMMdd").csv" -NoTypeInformation Bonus Extra Credit Here’s something new. I’ve decided to throw in some extra credit. If you are running this at home, try throwing in Out-GridView into middle of the output so you can manually filter what gets saved int the report.]]></summary></entry><entry><title type="html">Maintaining an Active Directory OU Structure</title><link href="http://greybeard.ai/2021/04/20/PowerShellMaintainOUStructure" rel="alternate" type="text/html" title="Maintaining an Active Directory OU Structure" /><published>2021-04-20T00:55:00+00:00</published><updated>2021-04-20T00:55:00+00:00</updated><id>http://greybeard.ai/2021/04/20/PowerShellMaintainOUStructure</id><content type="html" xml:base="http://greybeard.ai/2021/04/20/PowerShellMaintainOUStructure"><![CDATA[<p><img src="/assets/images/20210419-OUStructure.png" alt="OU Structure" /></p>

<h1 id="automate-more-of-the-things">Automate More of The Things</h1>

<p>A few weeks ago I wrote about how to <a href="/2021/04/04/PowershellMaintainFolderStructure">maintain a folder structure with PowerShell</a>. That was a lot more involved than this, but when you are managing dozens to hundreds of domains, keeping things tidy is important. A peer over in the MSPGeek Slack was asking for input on just such a thing a few days ago, and I shared with him an ancient and terrible but functional script for creating our minimum required OUs, as well as security groups. Well, I’ve went and cleaned it up a little. This script will create all of the requested OUs, checking to see if they exist first before trying. This means you can regularly run this on an environment to ensure compliance.</p>

<p>Much like my previous script folder maintenance I made a function that does the core of creating an OU. After that, I just call that for every new OU I want created. Reducing code and making revisions easier. I didn’t do the same for making the security group, but if you had a small list of groups you always used, you could certainly do the same for it.</p>

<p>The only real issues you might find are:</p>
<ol>
  <li>Azure AD Sync can be configured to sync based on OU, so moving things around may cause accidental deletion in Azure AD</li>
  <li>There are a lot of other things that work off OU. Be careful when moving things around if you have an Active Directory integrations like LDAP user authentication on firewalls.</li>
  <li>GPOs are often tied to OU. Better make sure the client doesn’t have GPOs linked to OUs, and if they do, mirror the OU structure and linked GPOs inside your new structure.</li>
</ol>

<p>It may be wise to audit your client’s use of protocols like LDAP before rearranging everything. Once everything is in the proper OUs, it will make it much easier to find what you are looking for, and</p>

<h1 id="code-time">Code Time</h1>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">Function</span><span class="w"> </span><span class="nf">New-OU</span><span class="w"> </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">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</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">$Name</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">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</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">$Path</span><span class="w">
    </span><span class="p">)</span><span class="w">
    </span><span class="nv">$OU</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ADOrganizationalUnit</span><span class="w"> </span><span class="nt">-Filter</span><span class="w"> </span><span class="s2">"name -eq '</span><span class="nv">$Name</span><span class="s2">'"</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">!</span><span class="nv">$OU</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">$Path</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">New-ADOrganizationalUnit</span><span class="w"> </span><span class="nt">-name</span><span class="w"> </span><span class="nv">$Name</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$Path</span><span class="p">;</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">New-ADOrganizationalUnit</span><span class="w"> </span><span class="nt">-name</span><span class="w"> </span><span class="nv">$Name</span><span class="p">;</span><span class="w">
        </span><span class="p">}</span><span class="w">
        </span><span class="nv">$OU</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ADOrganizationalUnit</span><span class="w"> </span><span class="nt">-Filter</span><span class="w"> </span><span class="s2">"name -eq '</span><span class="nv">$Name</span><span class="s2">'"</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="nv">$OU</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c">#Make the primary OU that all the others will go under</span><span class="w">
</span><span class="nv">$OUPrimary</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-OU</span><span class="w"> </span><span class="s2">"Company"</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$OUPrimary</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="c">#Make all the Children OUs</span><span class="w">
    </span><span class="nv">$OUEmployees</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-OU</span><span class="w"> </span><span class="s2">"Employees"</span><span class="w"> </span><span class="nv">$OUPrimary</span><span class="o">.</span><span class="nf">DistinguishedName</span><span class="w">
    </span><span class="nv">$OUDisabledEmployees</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-OU</span><span class="w"> </span><span class="s2">"Disabled Employees"</span><span class="w"> </span><span class="nv">$OUPrimary</span><span class="o">.</span><span class="nf">DistinguishedName</span><span class="w">
    </span><span class="nv">$OUServiceAccounts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-OU</span><span class="w"> </span><span class="s2">"Service Accounts"</span><span class="w"> </span><span class="nv">$OUPrimary</span><span class="o">.</span><span class="nf">DistinguishedName</span><span class="w">
    </span><span class="nv">$OUSecurityGroups</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-OU</span><span class="w"> </span><span class="s2">"Security Groups"</span><span class="w"> </span><span class="nv">$OUPrimary</span><span class="o">.</span><span class="nf">DistinguishedName</span><span class="w">
    </span><span class="c">#Make Security Groups inside of the Security Groups OU</span><span class="w">
    </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$OUSecurityGroups</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="nv">$EmployeeSG</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ADGroup</span><span class="w"> </span><span class="nt">-Identity</span><span class="w"> </span><span class="s2">"Employees"</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">"Creating Employees Security Group"</span><span class="w">
            </span><span class="n">New-ADGroup</span><span class="w"> </span><span class="nt">-name</span><span class="w"> </span><span class="s2">"Employees"</span><span class="w"> </span><span class="nt">-GroupCategory</span><span class="w"> </span><span class="nx">Security</span><span class="w"> </span><span class="nt">-GroupScope</span><span class="w"> </span><span class="nx">Global</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$OUSecurityGroups</span><span class="o">.</span><span class="nf">DistinguishedName</span><span class="p">;</span><span class="w">
            </span><span class="nv">$EmployeeSG</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ADGroup</span><span class="w"> </span><span class="nt">-Identity</span><span class="w"> </span><span class="s2">"Employees"</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="p">}</span><span class="w">
</span></code></pre></div></div>]]></content><author><name>Danial Bulloch</name></author><category term="powershell" /><summary type="html"><![CDATA[Automate More of The Things A few weeks ago I wrote about how to maintain a folder structure with PowerShell. That was a lot more involved than this, but when you are managing dozens to hundreds of domains, keeping things tidy is important. A peer over in the MSPGeek Slack was asking for input on just such a thing a few days ago, and I shared with him an ancient and terrible but functional script for creating our minimum required OUs, as well as security groups. Well, I’ve went and cleaned it up a little. This script will create all of the requested OUs, checking to see if they exist first before trying. This means you can regularly run this on an environment to ensure compliance. Much like my previous script folder maintenance I made a function that does the core of creating an OU. After that, I just call that for every new OU I want created. Reducing code and making revisions easier. I didn’t do the same for making the security group, but if you had a small list of groups you always used, you could certainly do the same for it. The only real issues you might find are: Azure AD Sync can be configured to sync based on OU, so moving things around may cause accidental deletion in Azure AD There are a lot of other things that work off OU. Be careful when moving things around if you have an Active Directory integrations like LDAP user authentication on firewalls. GPOs are often tied to OU. Better make sure the client doesn’t have GPOs linked to OUs, and if they do, mirror the OU structure and linked GPOs inside your new structure. It may be wise to audit your client’s use of protocols like LDAP before rearranging everything. Once everything is in the proper OUs, it will make it much easier to find what you are looking for, and Code Time Function New-OU { param ( [Parameter(Mandatory = $true)] [string]$Name, [Parameter(Mandatory = $false)] [string]$Path ) $OU = Get-ADOrganizationalUnit -Filter "name -eq '$Name'" ; if (!$OU) { if ($Path) { New-ADOrganizationalUnit -name $Name -Path $Path; } else { New-ADOrganizationalUnit -name $Name; } $OU = Get-ADOrganizationalUnit -Filter "name -eq '$Name'"; } return $OU } #Make the primary OU that all the others will go under $OUPrimary = New-OU "Company" if ($OUPrimary) { #Make all the Children OUs $OUEmployees = New-OU "Employees" $OUPrimary.DistinguishedName $OUDisabledEmployees = New-OU "Disabled Employees" $OUPrimary.DistinguishedName $OUServiceAccounts = New-OU "Service Accounts" $OUPrimary.DistinguishedName $OUSecurityGroups = New-OU "Security Groups" $OUPrimary.DistinguishedName #Make Security Groups inside of the Security Groups OU if ($OUSecurityGroups) { Try {$EmployeeSG = Get-ADGroup -Identity "Employees"} Catch { write-host "Creating Employees Security Group" New-ADGroup -name "Employees" -GroupCategory Security -GroupScope Global -Path $OUSecurityGroups.DistinguishedName; $EmployeeSG = Get-ADGroup -Identity "Employees"; } } }]]></summary></entry><entry><title type="html">Maintaining a Folder Structure</title><link href="http://greybeard.ai/2021/04/04/PowershellMaintainFolderStructure" rel="alternate" type="text/html" title="Maintaining a Folder Structure" /><published>2021-04-04T15:33:00+00:00</published><updated>2021-04-04T15:33:00+00:00</updated><id>http://greybeard.ai/2021/04/04/PowershellMaintainFolderStructure</id><content type="html" xml:base="http://greybeard.ai/2021/04/04/PowershellMaintainFolderStructure"><![CDATA[<p><img src="/assets/images/20210404-FolderStructure.png" alt="Folder Structure" /></p>

<h1 id="automate-all-the-things">Automate All The Things</h1>

<p>At the office we were discussing a new process that needed a new folder in our client documents folder. To add the folder to every single client’s directory, I created a very quick little PowerShell command that did it for me. Something like this:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-Item</span><span class="w"> </span><span class="s2">"D:\Clients\*\5. Misc\"</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="o">%</span><span class="p">{</span><span class="n">New-Item</span><span class="w"> </span><span class="s2">"</span><span class="si">$(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">FullName</span><span class="si">)</span><span class="s2">\1. New Folder"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Totally worked. Took about 5 minutes to write it off the top of my head, and I had created hundreds of folders.</p>

<p>Let’s do a quick breakdown of what I did there. First, I used Get-Item with a wildcard to give me an object for every <code class="language-plaintext highlighter-rouge">5. Misc</code> folder for every client. I then piped that into a <code class="language-plaintext highlighter-rouge">ForEach-Object</code> using the <code class="language-plaintext highlighter-rouge">%</code> abbreviation. The <code class="language-plaintext highlighter-rouge">ForEach-Object</code> command will loop through every directory, running the command for each. In this case I simply used a New-Item command to create the folder under each <code class="language-plaintext highlighter-rouge">5. Misc</code> folder.</p>

<p>But it reminded me that creating all those client folders was a manual process, which is kinda dumb. In addition, changes were all being done manually, things like archiving old clients. Lastly, nothing was enforcing using the proper structure.</p>

<h1 id="the-simple-solution">The Simple Solution</h1>

<p>So I sat down on a lunch break and wrote out the following script. It isn’t full featured, but for less than an hour’s work it does the job quite nicely. Recommended improvements would be parameterization of inputs, and possibly CVS inputs. My testing did not show any issues, but if I had to guess I’d say long file names would be a problem, especially with the archiving sections.</p>

<p>Features:</p>
<ol>
  <li>Create Client Folders</li>
  <li>Create Client Sub-folders</li>
  <li>Archive Old Clients with Timestamp</li>
  <li>Move Unmanaged Client Sub-folders and Documents into an Orphan Folder</li>
</ol>

<p>For future updates, check out the <a href="https://github.com/dbeta/Set-DirectoryStructure">Set-DirectoryStructure</a> page on GitHub</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$Clients</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"ABC"</span><span class="p">,</span><span class="w"> </span><span class="s2">"NMO"</span><span class="p">,</span><span class="w"> </span><span class="s2">"XYZ"</span><span class="w">
</span><span class="nv">$Folders</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"1. Vendor Info"</span><span class="p">,</span><span class="w"> </span><span class="s2">"2. Network Infrastructure"</span><span class="p">,</span><span class="w"> </span><span class="s2">"2. Network Infrastructure\1. Firewall Configs"</span><span class="p">,</span><span class="w"> </span><span class="s2">"3. Reports"</span><span class="w">
</span><span class="nv">$ClientArchive</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Z. Archived"</span><span class="w">
</span><span class="nv">$OrphanFolder</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Z. Orphans"</span><span class="w">

</span><span class="nv">$TargetFolder</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"D:\Clients"</span><span class="w">

</span><span class="c">#Check if path already exists, if it doesn't, create it</span><span class="w">
</span><span class="kr">function</span><span class="w"> </span><span class="nf">New-DirectoryIfNeeded</span><span class="w"> </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">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
        </span><span class="nv">$Path</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">!</span><span class="p">(</span><span class="n">Test-Path</span><span class="w"> </span><span class="nv">$Path</span><span class="p">))</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">New-Item</span><span class="w"> </span><span class="nv">$Path</span><span class="w"> </span><span class="nt">-ItemType</span><span class="w"> </span><span class="nx">Directory</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="kr">return</span><span class="w"> </span><span class="p">(</span><span class="n">Test-Path</span><span class="w"> </span><span class="nv">$Path</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="n">Test-Path</span><span class="w"> </span><span class="nv">$TargetFolder</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">$Client</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$Clients</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="c">#Make the Client Folder</span><span class="w">
        </span><span class="nv">$ClientFolder</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-DirectoryIfNeeded</span><span class="w"> </span><span class="s2">"</span><span class="nv">$TargetFolder</span><span class="s2">\</span><span class="nv">$Client</span><span class="s2">"</span><span class="w">
        </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$ClientFolder</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nv">$Folders</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">$Folder</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-DirectoryIfNeeded</span><span class="w"> </span><span class="s2">"</span><span class="nv">$TargetFolder</span><span class="s2">\</span><span class="nv">$Client</span><span class="s2">\</span><span class="bp">$_</span><span class="s2">"</span><span class="w">
            </span><span class="p">}</span><span class="w">
            </span><span class="c">#Create Orphan Folder</span><span class="w">
            </span><span class="nv">$OrphanFolderObj</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-DirectoryIfNeeded</span><span class="w"> </span><span class="s2">"</span><span class="nv">$TargetFolder</span><span class="s2">\</span><span class="nv">$Client</span><span class="s2">\</span><span class="nv">$OrphanFolder</span><span class="s2">"</span><span class="w">
            </span><span class="c">#Move Orphaned Objects to the Orphan folder</span><span class="w">
            </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$OrphanFolderObj</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nv">$ExistingFolders</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ChildItem</span><span class="w"> </span><span class="s2">"</span><span class="nv">$TargetFolder</span><span class="s2">\</span><span class="nv">$Client</span><span class="s2">"</span><span class="w">
                </span><span class="nv">$ExistingFolders</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="kr">if</span><span class="w"> </span><span class="p">(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="w"> </span><span class="nt">-notin</span><span class="w"> </span><span class="nv">$Folders</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="nv">$OrphanFolder</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
                        </span><span class="nv">$NewName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"</span><span class="si">$(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="si">)</span><span class="s2"> - Archived </span><span class="si">$(</span><span class="n">Get-Date</span><span class="w"> </span><span class="nt">-Format</span><span class="w"> </span><span class="s2">"yyyy-MM-dd_hhmmss"</span><span class="p">)</span><span class="s2">"
                        </span><span class="nv">$NewObject</span><span class="s2"> = Rename-Item -Path </span><span class="bp">$_</span><span class="s2">.FullName -NewName </span><span class="nv">$NewName</span><span class="s2"> -PassThru
                        Move-Item -Path </span><span class="nv">$NewObject</span><span class="s2">.FullName -Destination "</span><span class="nv">$TargetFolder</span><span class="n">\</span><span class="nv">$Client</span><span class="nx">\</span><span class="nv">$OrphanFolder</span><span class="nx">\</span><span class="s2">"
                    }
                }
            }

        }
    } 
    #Create Client Archive
    </span><span class="nv">$ClientArchiveFolder</span><span class="s2"> = New-DirectoryIfNeeded "</span><span class="nv">$TargetFolder</span><span class="nx">\</span><span class="nv">$ClientArchive</span><span class="s2">"
    #Move any items from the root folder into archived if they aren't current clients
    if (</span><span class="nv">$ClientArchiveFolder</span><span class="s2">) {
        </span><span class="nv">$ExistingClientFolders</span><span class="s2"> = Get-ChildItem </span><span class="nv">$TargetFolder</span><span class="s2">
        </span><span class="nv">$ExistingClientFolders</span><span class="s2"> | ForEach-Object {
            if (</span><span class="bp">$_</span><span class="s2">.Name -notin </span><span class="nv">$Clients</span><span class="s2"> -and </span><span class="bp">$_</span><span class="s2">.Name -ne </span><span class="nv">$ClientArchive</span><span class="s2">) {
                </span><span class="nv">$NewName</span><span class="s2"> = "</span><span class="err">$</span><span class="p">(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="si">)</span><span class="s2"> - Archived </span><span class="si">$(</span><span class="n">Get-Date</span><span class="w"> </span><span class="nt">-Format</span><span class="w"> </span><span class="s2">"yyyy-MM-dd_hhmmss"</span><span class="p">)</span><span class="s2">"
                </span><span class="nv">$NewObject</span><span class="s2"> = Rename-Item -Path </span><span class="bp">$_</span><span class="s2">.FullName -NewName </span><span class="nv">$NewName</span><span class="s2"> -PassThru
                Move-Item -Path </span><span class="nv">$NewObject</span><span class="s2">.FullName -Destination "</span><span class="nv">$TargetFolder</span><span class="n">\</span><span class="nv">$ClientArchive</span><span class="s2">"
            }
        }
    }
}


</span></code></pre></div></div>]]></content><author><name>Danial Bulloch</name></author><category term="powershell" /><summary type="html"><![CDATA[Automate All The Things At the office we were discussing a new process that needed a new folder in our client documents folder. To add the folder to every single client’s directory, I created a very quick little PowerShell command that did it for me. Something like this: Get-Item "D:\Clients\*\5. Misc\" | %{New-Item "$($_.FullName)\1. New Folder"} Totally worked. Took about 5 minutes to write it off the top of my head, and I had created hundreds of folders. Let’s do a quick breakdown of what I did there. First, I used Get-Item with a wildcard to give me an object for every 5. Misc folder for every client. I then piped that into a ForEach-Object using the % abbreviation. The ForEach-Object command will loop through every directory, running the command for each. In this case I simply used a New-Item command to create the folder under each 5. Misc folder. But it reminded me that creating all those client folders was a manual process, which is kinda dumb. In addition, changes were all being done manually, things like archiving old clients. Lastly, nothing was enforcing using the proper structure. The Simple Solution So I sat down on a lunch break and wrote out the following script. It isn’t full featured, but for less than an hour’s work it does the job quite nicely. Recommended improvements would be parameterization of inputs, and possibly CVS inputs. My testing did not show any issues, but if I had to guess I’d say long file names would be a problem, especially with the archiving sections. Features: Create Client Folders Create Client Sub-folders Archive Old Clients with Timestamp Move Unmanaged Client Sub-folders and Documents into an Orphan Folder For future updates, check out the Set-DirectoryStructure page on GitHub $Clients = "ABC", "NMO", "XYZ" $Folders = "1. Vendor Info", "2. Network Infrastructure", "2. Network Infrastructure\1. Firewall Configs", "3. Reports" $ClientArchive = "Z. Archived" $OrphanFolder = "Z. Orphans" $TargetFolder = "D:\Clients" #Check if path already exists, if it doesn't, create it function New-DirectoryIfNeeded { param ( [Parameter(Mandatory = $true)] $Path ) if (!(Test-Path $Path)) { New-Item $Path -ItemType Directory } return (Test-Path $Path) } if (Test-Path $TargetFolder) { ForEach ($Client in $Clients) { #Make the Client Folder $ClientFolder = New-DirectoryIfNeeded "$TargetFolder\$Client" if ($ClientFolder) { $Folders | ForEach-Object { $Folder = New-DirectoryIfNeeded "$TargetFolder\$Client\$_" } #Create Orphan Folder $OrphanFolderObj = New-DirectoryIfNeeded "$TargetFolder\$Client\$OrphanFolder" #Move Orphaned Objects to the Orphan folder if ($OrphanFolderObj) { $ExistingFolders = Get-ChildItem "$TargetFolder\$Client" $ExistingFolders | ForEach-Object { if ($_.Name -notin $Folders -and $_.Name -ne $OrphanFolder) { $NewName = "$($_.Name) - Archived $(Get-Date -Format "yyyy-MM-dd_hhmmss")" $NewObject = Rename-Item -Path $_.FullName -NewName $NewName -PassThru Move-Item -Path $NewObject.FullName -Destination "$TargetFolder\$Client\$OrphanFolder\" } } } } } #Create Client Archive $ClientArchiveFolder = New-DirectoryIfNeeded "$TargetFolder\$ClientArchive" #Move any items from the root folder into archived if they aren't current clients if ($ClientArchiveFolder) { $ExistingClientFolders = Get-ChildItem $TargetFolder $ExistingClientFolders | ForEach-Object { if ($_.Name -notin $Clients -and $_.Name -ne $ClientArchive) { $NewName = "$($_.Name) - Archived $(Get-Date -Format "yyyy-MM-dd_hhmmss")" $NewObject = Rename-Item -Path $_.FullName -NewName $NewName -PassThru Move-Item -Path $NewObject.FullName -Destination "$TargetFolder\$ClientArchive" } } } }]]></summary></entry><entry><title type="html">Quickie: Out-Gridview for Input?</title><link href="http://greybeard.ai/2021/03/24/PowershellOutGridView" rel="alternate" type="text/html" title="Quickie: Out-Gridview for Input?" /><published>2021-03-24T23:40:00+00:00</published><updated>2021-03-24T23:40:00+00:00</updated><id>http://greybeard.ai/2021/03/24/PowershellOutGridView</id><content type="html" xml:base="http://greybeard.ai/2021/03/24/PowershellOutGridView"><![CDATA[<p><img src="/assets/images/20210324-OutGridView.png" alt="Powershell Example Output" /></p>

<h1 id="always-something-new-to-learn">Always Something New to Learn</h1>

<p>Most of you probably already know about <code class="language-plaintext highlighter-rouge">Out-Gridview</code>, a great command to output data into a human readable grid. Not great for automation, but sometimes you just want to gather information, or pull a detailed sanity check before fully automating something.</p>

<p>Let’s do a quick example.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-Process</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Out-Gridview</span><span class="w">
</span></code></pre></div></div>

<p>Pretty nifty, no? Way better than <code class="language-plaintext highlighter-rouge">Format-Table</code> in a lot of use cases. What if it could do more? Well, you are in luck. Check out the <code class="language-plaintext highlighter-rouge">-OutputMode</code> parameter. It’s the real magic!</p>

<h1 id="the-main-event">The Main Event</h1>

<p>Now that we know about the <code class="language-plaintext highlighter-rouge">-OutputMode</code> parameter, let’s put it in practice.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-Process</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Out-GridView</span><span class="w"> </span><span class="nt">-OutputMode</span><span class="w"> </span><span class="nx">Single</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Stop-Process</span><span class="w">
</span></code></pre></div></div>

<p>That’s it for the quickie today. Hope you had fun.</p>]]></content><author><name>Danial Bulloch</name></author><category term="powershell" /><summary type="html"><![CDATA[Always Something New to Learn Most of you probably already know about Out-Gridview, a great command to output data into a human readable grid. Not great for automation, but sometimes you just want to gather information, or pull a detailed sanity check before fully automating something. Let’s do a quick example. Get-Process | Out-Gridview Pretty nifty, no? Way better than Format-Table in a lot of use cases. What if it could do more? Well, you are in luck. Check out the -OutputMode parameter. It’s the real magic! The Main Event Now that we know about the -OutputMode parameter, let’s put it in practice. Get-Process | Out-GridView -OutputMode Single | Stop-Process That’s it for the quickie today. Hope you had fun.]]></summary></entry><entry><title type="html">Quickie: Monitor for File Modifications</title><link href="http://greybeard.ai/2021/03/23/PowershellMonitorModifiedDate" rel="alternate" type="text/html" title="Quickie: Monitor for File Modifications" /><published>2021-03-23T15:40:00+00:00</published><updated>2021-03-23T15:40:00+00:00</updated><id>http://greybeard.ai/2021/03/23/PowershellMonitorModifiedDate</id><content type="html" xml:base="http://greybeard.ai/2021/03/23/PowershellMonitorModifiedDate"><![CDATA[<p><img src="/assets/images/20210323-PowerShellModifiedDate.png" alt="Powershell Example Output" /></p>

<h1 id="the-quick-gist">The Quick Gist</h1>

<p>A user in the <a href="https://mspgeek.org">MSPGeek</a> Slack asked a question about monitoring a file for not being changed. He wanted to setup an alert if the file hadn’t been modified in x number of days, so I wrote this quick one liner to help him, then over the next hour I came back and tweaked it several times. Here is the final result.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">get-item</span><span class="w"> </span><span class="nx">test1.txt</span><span class="p">,</span><span class="nx">test2.txt</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="o">%</span><span class="p">{</span><span class="kr">if</span><span class="p">(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">LastWriteTime</span><span class="w"> </span><span class="o">-lt</span><span class="w"> </span><span class="p">(</span><span class="n">Get-Date</span><span class="p">)</span><span class="o">.</span><span class="nf">AddDays</span><span class="p">(</span><span class="nt">-5</span><span class="p">))</span><span class="w"> </span><span class="p">{</span><span class="n">Write-Output</span><span class="w"> </span><span class="p">(</span><span class="s1">'Failure: {0} - {1}'</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="p">,</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">LastWriteTime</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-Output</span><span class="w"> </span><span class="p">(</span><span class="s1">'Success: {0} - {1}'</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="p">,</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">LastWriteTime</span><span class="p">)}}</span><span class="w">
</span></code></pre></div></div>

<p>A quick and dirty command to do the job. I could end the blog post here, but…</p>

<h1 id="-whats-that">%? What’s that?</h1>

<p><code class="language-plaintext highlighter-rouge">%</code> is an alias for <code class="language-plaintext highlighter-rouge">ForEach-Object</code>. When you are making a quick and dirty one liner, it’s nice to keep things shorter, but might be a little hard to reader to read. That’s why when you are writing a full script, you should probably explode that out completely.</p>

<h1 id="write-output-vs-write-host">Write-Output vs Write-Host</h1>

<p>If you’ve been PowerShelling on the side for some time, you might have picked up the habit of using Write-Host. Very, very rarely is it the right option. Write host writes to the console no mater what. Write-Output, however, will write into the stream. In my example, that doesn’t matter much, but what if you wanted to pump the results into a variable? We could do <code class="language-plaintext highlighter-rouge">$results = get-item ...</code>. Now it is in a variable we could use later down on the line. Although if we were doing that, I’d probably just push the objects, or create new objects, but for the scope we were working at, it was fine.</p>

<h1 id="whats-with-the-weird-strings">What’s with the weird strings?</h1>

<p>In this use case, double quotes were likely to cause problems. They would need to be escaped. Not to mention that inside those strings inside of For-EachObject would need to be wrapped in <code class="language-plaintext highlighter-rouge">$()</code> because the string would get confused with <code class="language-plaintext highlighter-rouge">$_.</code> and not realize you wanted the <code class="language-plaintext highlighter-rouge">Name</code> and <code class="language-plaintext highlighter-rouge">LastWriteTime</code>. So instead I used a single quote, then used the <code class="language-plaintext highlighter-rouge">-f</code> parameter on the string to fill in variables where the <code class="language-plaintext highlighter-rouge">{0}</code> and <code class="language-plaintext highlighter-rouge">{1}</code> are. Pretty handy when you are embedding the powershell scripts in your RMM or somewhere else that might have conflicting quotes.</p>

<h1 id="what-if-we-wanted-it-to-be-more-dynamic">What if we wanted it to be more dynamic?</h1>

<p>So it’s setup pretty well to be expanded or embedded in another script. Let’s blow it out past a one liner.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$files</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'test1.txt'</span><span class="p">,</span><span class="s1">'test2.txt'</span><span class="w">
</span><span class="nv">$MaximumAgeDays</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">5</span><span class="w">
</span><span class="n">get-item</span><span class="w"> </span><span class="nv">$files</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="kr">if</span><span class="p">(</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">LastWriteTime</span><span class="w"> </span><span class="o">-lt</span><span class="w"> </span><span class="p">(</span><span class="n">Get-Date</span><span class="p">)</span><span class="o">.</span><span class="nf">AddDays</span><span class="p">(</span><span class="o">-</span><span class="nv">$MaximumAgeDays</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="n">Write-Output</span><span class="w"> </span><span class="p">(</span><span class="s1">'Failure: {0} - {1}'</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="p">,</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">LastWriteTime</span><span class="p">)</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-Output</span><span class="w"> </span><span class="p">(</span><span class="s1">'Success: {0} - {1}'</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="p">,</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">LastWriteTime</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></code></pre></div></div>

<p>Hey look at that, with a few small edits, we can now easily change the files and age. Great if you are getting the list of files from another location like registry keys. Could be embedded well in an existing script, and is easy to understand.</p>]]></content><author><name>Danial Bulloch</name></author><category term="powershell" /><summary type="html"><![CDATA[The Quick Gist A user in the MSPGeek Slack asked a question about monitoring a file for not being changed. He wanted to setup an alert if the file hadn’t been modified in x number of days, so I wrote this quick one liner to help him, then over the next hour I came back and tweaked it several times. Here is the final result. get-item test1.txt,test2.txt | %{if($_.LastWriteTime -lt (Get-Date).AddDays(-5)) {Write-Output ('Failure: {0} - {1}' -f $_.Name, $_.LastWriteTime)} else { Write-Output ('Success: {0} - {1}' -f $_.Name, $_.LastWriteTime)}} A quick and dirty command to do the job. I could end the blog post here, but… %? What’s that? % is an alias for ForEach-Object. When you are making a quick and dirty one liner, it’s nice to keep things shorter, but might be a little hard to reader to read. That’s why when you are writing a full script, you should probably explode that out completely. Write-Output vs Write-Host If you’ve been PowerShelling on the side for some time, you might have picked up the habit of using Write-Host. Very, very rarely is it the right option. Write host writes to the console no mater what. Write-Output, however, will write into the stream. In my example, that doesn’t matter much, but what if you wanted to pump the results into a variable? We could do $results = get-item .... Now it is in a variable we could use later down on the line. Although if we were doing that, I’d probably just push the objects, or create new objects, but for the scope we were working at, it was fine. What’s with the weird strings? In this use case, double quotes were likely to cause problems. They would need to be escaped. Not to mention that inside those strings inside of For-EachObject would need to be wrapped in $() because the string would get confused with $_. and not realize you wanted the Name and LastWriteTime. So instead I used a single quote, then used the -f parameter on the string to fill in variables where the {0} and {1} are. Pretty handy when you are embedding the powershell scripts in your RMM or somewhere else that might have conflicting quotes. What if we wanted it to be more dynamic? So it’s setup pretty well to be expanded or embedded in another script. Let’s blow it out past a one liner. $files = 'test1.txt','test2.txt' $MaximumAgeDays = 5 get-item $files | ForEach-Object { if( $_.LastWriteTime -lt (Get-Date).AddDays(-$MaximumAgeDays) ) { Write-Output ('Failure: {0} - {1}' -f $_.Name, $_.LastWriteTime) } else { Write-Output ('Success: {0} - {1}' -f $_.Name, $_.LastWriteTime) } } Hey look at that, with a few small edits, we can now easily change the files and age. Great if you are getting the list of files from another location like registry keys. Could be embedded well in an existing script, and is easy to understand.]]></summary></entry><entry><title type="html">Maintenance Alerts</title><link href="http://greybeard.ai/2021/03/08/PowershellMaintenanceAlerts" rel="alternate" type="text/html" title="Maintenance Alerts" /><published>2021-03-08T16:35:00+00:00</published><updated>2021-03-08T16:35:00+00:00</updated><id>http://greybeard.ai/2021/03/08/PowershellMaintenanceAlerts</id><content type="html" xml:base="http://greybeard.ai/2021/03/08/PowershellMaintenanceAlerts"><![CDATA[<p><img src="/assets/images/20210308_PowershellNotification.png" alt="Notification Preview" /></p>

<h1 id="why">Why?</h1>

<p>For a long time I’ve been using the alert box feature built into my RMM to alert end users that updates may be installed that night. It is technically functional, but the the style is quite aged. I also am interested in not relying on my RMM where it isn’t needed. So I sat down and wrote up this powershell script to send an alert to the signed in user. I’m using my RMM to detect when a user is signed in, and to only run the script when they are, otherwise it keeps trying through the day until the user is logged on and it can send them the alert.</p>

<h1 id="tools-used">Tools Used</h1>

<p>I used a few tools for this script. I highly recommend checking out Kelvin Tegelaar’s <a href="https://github.com/KelvinTegelaar/RunAsUser">RunAsUser</a> powershell module. It’s what makes this pretty easy. Kelvin was nice enough to add the <code class="language-plaintext highlighter-rouge">CacheToDisk</code> flag when I tried to feed this script to it and the length broke the function. I also used SMSAgent’s <a href="https://smsagent.blog/2017/08/24/a-customisable-wpf-messagebox-for-powershell/">Customizable WPF MessageBox</a> as the base for my message box. I ripped out all of the unneeded code though. I highly recommend spending time with his code if you want to better understand WPF based windows. Theoretically you should be able to lookup details on making WPF windows, and use that in powershell just like any other programming language.</p>

<h1 id="caveat-emptor">Caveat Emptor</h1>

<p>I wrote this and locked the version of Kelvin’s module at 2.0. This is for 2 reasons: 1) To make sure future changes to his module do not cause compatibility issues. B) To make sure if Kelvin’s accounts were ever compromised, changes couldn’t be introduced that were security risks. Since this will be running as system, across entire client bases, it’s a very juicy target. I trust Kelvin, but the world is a wild place for security, best to plan for the worst. With that in mind, I highly recommend forking his entire module, and running your own, instead taking his directly. I recommend that for any module you use in an automated fashion. Supply chain attacks are getting powerful, and if you aren’t careful, a silly dependency might result in your entire infrastructure being compromised.</p>

<h1 id="get-to-the-monkey">Get to the Monkey!</h1>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Install-PackageProvider</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nx">NuGet</span><span class="w">
</span><span class="c"># We demand an exact version of RunAsUser because theoretically future versions could be uploaded that were malicious.</span><span class="w">
</span><span class="c"># By setting a static version number, we always know we get a version we trust. You may want to go one step further and fork the repo, inspect, and use your fork.</span><span class="w">
</span><span class="n">Install-Module</span><span class="w"> </span><span class="nx">RunAsuser</span><span class="w"> </span><span class="nt">-RequiredVersion</span><span class="w"> </span><span class="nx">2.0</span><span class="w">
</span><span class="n">Import-Module</span><span class="w"> </span><span class="nx">RunASuser</span><span class="w"> </span><span class="nt">-RequiredVersion</span><span class="w"> </span><span class="nx">2.0</span><span class="w">
</span><span class="n">Invoke-AsCurrentUser</span><span class="w"> </span><span class="nt">-CacheToDisk</span><span class="w"> </span><span class="nt">-scriptblock</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$Title</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Maintenance Notification"</span><span class="w">
    </span><span class="nv">$Message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"System maintenance is being performed tonight. Please save your work &amp; leave your system on. A reboot may occur. If 3 of these are missed, updates will occur the next time the system comes online."</span><span class="w">
    </span><span class="nv">$ImageSource</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"https://domain.com/notificationlogo.png"</span><span class="w">
    </span><span class="nv">$ButtonMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"OK"</span><span class="w">

    </span><span class="c"># Required to draw the window</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">PresentationFramework</span><span class="w">
    
    </span><span class="c"># Setup the logo to show next to the text.</span><span class="w">
    </span><span class="nv">$Image</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.Windows.Controls.Image</span><span class="w">
    </span><span class="nv">$Image</span><span class="o">.</span><span class="nf">Source</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ImageSource</span><span class="w">
    </span><span class="nv">$Image</span><span class="o">.</span><span class="nf">Margin</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">10</span><span class="w">
    </span><span class="c"># I recommend matching the source image resolution, and the height/width defined here. Windows doesn't do a great job of scaling the image.</span><span class="w">
    </span><span class="nv">$Image</span><span class="o">.</span><span class="nf">Height</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">128</span><span class="w">
    </span><span class="nv">$Image</span><span class="o">.</span><span class="nf">Width</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">128</span><span class="w">

    </span><span class="c"># Setup the main message text.</span><span class="w">
    </span><span class="nv">$TextBlock</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.Windows.Controls.TextBlock</span><span class="w">
    </span><span class="nv">$TextBlock</span><span class="o">.</span><span class="nf">Text</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Message</span><span class="w">
    </span><span class="nv">$TextBlock</span><span class="o">.</span><span class="nf">Padding</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">10</span><span class="w">
    </span><span class="nv">$TextBlock</span><span class="o">.</span><span class="nf">FontSize</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">20</span><span class="w">
    </span><span class="nv">$TextBlock</span><span class="o">.</span><span class="nf">VerticalAlignment</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Center"</span><span class="w">
    </span><span class="nv">$TextBlock</span><span class="o">.</span><span class="nf">TextWrapping</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Wrap"</span><span class="w">
    </span><span class="nv">$TextBlock</span><span class="o">.</span><span class="nf">MaxWidth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"600"</span><span class="w">
    </span><span class="nv">$TextBlock</span><span class="o">.</span><span class="nf">Foreground</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"White"</span><span class="w">

    </span><span class="c"># Combine the two into a "Horizontal" panel. Depending on your logo or styling concerns, you can switch this to "Vertical".</span><span class="w">
    </span><span class="nv">$Content</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.Windows.Controls.StackPanel</span><span class="w">
    </span><span class="nv">$Content</span><span class="o">.</span><span class="nf">Orientation</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Horizontal"</span><span class="w">
    </span><span class="nv">$Content</span><span class="o">.</span><span class="nf">AddChild</span><span class="p">(</span><span class="nv">$Image</span><span class="p">)</span><span class="w">
    </span><span class="nv">$Content</span><span class="o">.</span><span class="nf">AddChild</span><span class="p">(</span><span class="nv">$TextBlock</span><span class="p">)</span><span class="w">

    </span><span class="c"># XML defining the main window.</span><span class="w">
    </span><span class="p">[</span><span class="n">XML</span><span class="p">]</span><span class="nv">$Xaml</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="sh">@"
    &lt;Window 
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            x:Name="Window" Title="" SizeToContent="WidthAndHeight" WindowStartupLocation="CenterScreen" WindowStyle="None" ResizeMode="NoResize" AllowsTransparency="True" Background="Transparent" Opacity="1"&gt;
        &lt;Window.Resources&gt;
            &lt;Style TargetType="{x:Type Button}"&gt;
                &lt;Setter Property="Template"&gt;
                    &lt;Setter.Value&gt;
                        &lt;ControlTemplate TargetType="Button"&gt;
                            &lt;Border&gt;
                                &lt;Grid Background="{TemplateBinding Background}"&gt;
                                    &lt;ContentPresenter /&gt;
                                &lt;/Grid&gt;
                            &lt;/Border&gt;
                        &lt;/ControlTemplate&gt;
                    &lt;/Setter.Value&gt;
                &lt;/Setter&gt;
            &lt;/Style&gt;
        &lt;/Window.Resources&gt;
        &lt;Border x:Name="MainBorder" Margin="10" CornerRadius="0" BorderThickness="2" BorderBrush="Gray" Padding="0" &gt;
            &lt;Border.Effect&gt;
                &lt;DropShadowEffect x:Name="DSE" Color="Black" Direction="270" BlurRadius="20" ShadowDepth="3" Opacity="0.6" /&gt;
            &lt;/Border.Effect&gt;
            &lt;Border.Triggers&gt;
                &lt;EventTrigger RoutedEvent="Window.Loaded"&gt;
                    &lt;BeginStoryboard&gt;
                        &lt;Storyboard&gt;
                            &lt;DoubleAnimation Storyboard.TargetName="DSE" Storyboard.TargetProperty="ShadowDepth" From="0" To="3" Duration="0:0:1" AutoReverse="False" /&gt;
                            &lt;DoubleAnimation Storyboard.TargetName="DSE" Storyboard.TargetProperty="BlurRadius" From="0" To="20" Duration="0:0:1" AutoReverse="False" /&gt;
                        &lt;/Storyboard&gt;
                    &lt;/BeginStoryboard&gt;
                &lt;/EventTrigger&gt;
            &lt;/Border.Triggers&gt;
            &lt;Grid &gt;
                &lt;Border Name="Mask" CornerRadius="0" Background="Black" /&gt;
                &lt;Grid x:Name="Grid" Background="Black"&gt;
                    &lt;Grid.OpacityMask&gt;
                        &lt;VisualBrush Visual="{Binding ElementName=Mask}"/&gt;
                    &lt;/Grid.OpacityMask&gt;
                    &lt;StackPanel Name="StackPanel" &gt;                   
                        &lt;TextBox Name="TitleBar" IsReadOnly="True" IsHitTestVisible="False" Text="</span><span class="nv">$Title</span><span class="sh">" Padding="10" FontFamily="Segoe UI" FontSize="14" Foreground="White" FontWeight="Normal" Background="Black" HorizontalAlignment="Stretch" VerticalAlignment="Center" Width="Auto" HorizontalContentAlignment="Center" BorderThickness="0"/&gt;
                        &lt;DockPanel Name="ContentHost" Margin="0,10,0,10"  &gt;
                        &lt;/DockPanel&gt;
                        &lt;DockPanel Name="ButtonHost" LastChildFill="False" HorizontalAlignment="Center" &gt;
                        &lt;/DockPanel&gt;
                    &lt;/StackPanel&gt;
                &lt;/Grid&gt;
            &lt;/Grid&gt;
        &lt;/Border&gt;
    &lt;/Window&gt;
"@</span><span class="w">
    
    </span><span class="c"># XML Defining the confirm button.</span><span class="w">
    </span><span class="p">[</span><span class="n">XML</span><span class="p">]</span><span class="nv">$ButtonXaml</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="sh">@"
    &lt;Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Width="Auto" Height="30" FontFamily="Segui" FontSize="16" Background="Transparent" Foreground="White" BorderThickness="1" Margin="10" Padding="20,0,20,0" HorizontalAlignment="Right" Cursor="Hand"/&gt;
"@</span><span class="w">
    
    </span><span class="p">[</span><span class="n">XML</span><span class="p">]</span><span class="nv">$ButtonTextXaml</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="sh">@"
    &lt;TextBlock xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" FontFamily="Segoe UI" FontSize="16" Background="Gray" Foreground="Black" Padding="20,5,20,5" HorizontalAlignment="Center" VerticalAlignment="Center"/&gt;
"@</span><span class="w">
    </span><span class="c"># Create the window.</span><span class="w">
    </span><span class="nv">$Window</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">Windows.Markup.XamlReader</span><span class="p">]::</span><span class="n">Load</span><span class="p">((</span><span class="n">New-Object</span><span class="w"> </span><span class="nt">-TypeName</span><span class="w"> </span><span class="nx">System.Xml.XmlNodeReader</span><span class="w"> </span><span class="nt">-ArgumentList</span><span class="w"> </span><span class="nv">$xaml</span><span class="p">))</span><span class="w">

    </span><span class="c"># Create the button</span><span class="w">
    </span><span class="nv">$Button</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">Windows.Markup.XamlReader</span><span class="p">]::</span><span class="n">Load</span><span class="p">((</span><span class="n">New-Object</span><span class="w"> </span><span class="nt">-TypeName</span><span class="w"> </span><span class="nx">System.Xml.XmlNodeReader</span><span class="w"> </span><span class="nt">-ArgumentList</span><span class="w"> </span><span class="nv">$ButtonXaml</span><span class="p">))</span><span class="w">
    </span><span class="nv">$ButtonText</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">Windows.Markup.XamlReader</span><span class="p">]::</span><span class="n">Load</span><span class="p">((</span><span class="n">New-Object</span><span class="w"> </span><span class="nt">-TypeName</span><span class="w"> </span><span class="nx">System.Xml.XmlNodeReader</span><span class="w"> </span><span class="nt">-ArgumentList</span><span class="w"> </span><span class="nv">$ButtonTextXaml</span><span class="p">))</span><span class="w">
    </span><span class="nv">$ButtonText</span><span class="o">.</span><span class="nf">Text</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ButtonMessage</span><span class="w">
    </span><span class="nv">$Button</span><span class="o">.</span><span class="nf">Content</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ButtonText</span><span class="w">
    </span><span class="nv">$Button</span><span class="o">.</span><span class="nf">Add_MouseEnter</span><span class="p">(</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$This</span><span class="o">.</span><span class="nf">Content</span><span class="o">.</span><span class="nf">FontSize</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"20"</span><span class="w"> </span><span class="p">})</span><span class="w">
    </span><span class="nv">$Button</span><span class="o">.</span><span class="nf">Add_MouseLeave</span><span class="p">(</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$This</span><span class="o">.</span><span class="nf">Content</span><span class="o">.</span><span class="nf">FontSize</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"20"</span><span class="w"> </span><span class="p">})</span><span class="w">
    </span><span class="nv">$Button</span><span class="o">.</span><span class="nf">Add_Click</span><span class="p">(</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$Window</span><span class="o">.</span><span class="nf">Close</span><span class="p">()</span><span class="w"> </span><span class="p">})</span><span class="w">
    </span><span class="nv">$Window</span><span class="o">.</span><span class="nf">FindName</span><span class="p">(</span><span class="s1">'ButtonHost'</span><span class="p">)</span><span class="o">.</span><span class="nf">AddChild</span><span class="p">(</span><span class="nv">$Button</span><span class="p">)</span><span class="w">
    </span><span class="nv">$Window</span><span class="o">.</span><span class="nf">FindName</span><span class="p">(</span><span class="s1">'ContentHost'</span><span class="p">)</span><span class="o">.</span><span class="nf">AddChild</span><span class="p">(</span><span class="nv">$Content</span><span class="p">)</span><span class="w">        
    </span><span class="nv">$Window</span><span class="o">.</span><span class="nf">Add_Loaded</span><span class="p">(</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$This</span><span class="o">.</span><span class="nf">Activate</span><span class="p">()</span><span class="w"> </span><span class="p">})</span><span class="w">

    </span><span class="c"># Set the window to be always on top.</span><span class="w">
    </span><span class="nv">$Window</span><span class="o">.</span><span class="nf">Topmost</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
    </span><span class="bp">$null</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$window</span><span class="o">.</span><span class="nf">Dispatcher</span><span class="o">.</span><span class="nf">InvokeAsync</span><span class="p">{</span><span class="w"> </span><span class="nv">$window</span><span class="o">.</span><span class="nf">ShowDialog</span><span class="p">()</span><span class="w"> </span><span class="p">}</span><span class="o">.</span><span class="nf">Wait</span><span class="p">()</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>]]></content><author><name>Danial Bulloch</name></author><category term="powershell" /><summary type="html"><![CDATA[Why? For a long time I’ve been using the alert box feature built into my RMM to alert end users that updates may be installed that night. It is technically functional, but the the style is quite aged. I also am interested in not relying on my RMM where it isn’t needed. So I sat down and wrote up this powershell script to send an alert to the signed in user. I’m using my RMM to detect when a user is signed in, and to only run the script when they are, otherwise it keeps trying through the day until the user is logged on and it can send them the alert. Tools Used I used a few tools for this script. I highly recommend checking out Kelvin Tegelaar’s RunAsUser powershell module. It’s what makes this pretty easy. Kelvin was nice enough to add the CacheToDisk flag when I tried to feed this script to it and the length broke the function. I also used SMSAgent’s Customizable WPF MessageBox as the base for my message box. I ripped out all of the unneeded code though. I highly recommend spending time with his code if you want to better understand WPF based windows. Theoretically you should be able to lookup details on making WPF windows, and use that in powershell just like any other programming language. Caveat Emptor I wrote this and locked the version of Kelvin’s module at 2.0. This is for 2 reasons: 1) To make sure future changes to his module do not cause compatibility issues. B) To make sure if Kelvin’s accounts were ever compromised, changes couldn’t be introduced that were security risks. Since this will be running as system, across entire client bases, it’s a very juicy target. I trust Kelvin, but the world is a wild place for security, best to plan for the worst. With that in mind, I highly recommend forking his entire module, and running your own, instead taking his directly. I recommend that for any module you use in an automated fashion. Supply chain attacks are getting powerful, and if you aren’t careful, a silly dependency might result in your entire infrastructure being compromised. Get to the Monkey! Install-PackageProvider -Name NuGet # We demand an exact version of RunAsUser because theoretically future versions could be uploaded that were malicious. # By setting a static version number, we always know we get a version we trust. You may want to go one step further and fork the repo, inspect, and use your fork. Install-Module RunAsuser -RequiredVersion 2.0 Import-Module RunASuser -RequiredVersion 2.0 Invoke-AsCurrentUser -CacheToDisk -scriptblock { $Title = "Maintenance Notification" $Message = "System maintenance is being performed tonight. Please save your work &amp; leave your system on. A reboot may occur. If 3 of these are missed, updates will occur the next time the system comes online." $ImageSource = "https://domain.com/notificationlogo.png" $ButtonMessage = "OK" # Required to draw the window Add-Type -AssemblyName PresentationFramework # Setup the logo to show next to the text. $Image = New-Object System.Windows.Controls.Image $Image.Source = $ImageSource $Image.Margin = 10 # I recommend matching the source image resolution, and the height/width defined here. Windows doesn't do a great job of scaling the image. $Image.Height = 128 $Image.Width = 128 # Setup the main message text. $TextBlock = New-Object System.Windows.Controls.TextBlock $TextBlock.Text = $Message $TextBlock.Padding = 10 $TextBlock.FontSize = 20 $TextBlock.VerticalAlignment = "Center" $TextBlock.TextWrapping = "Wrap" $TextBlock.MaxWidth = "600" $TextBlock.Foreground = "White" # Combine the two into a "Horizontal" panel. Depending on your logo or styling concerns, you can switch this to "Vertical". $Content = New-Object System.Windows.Controls.StackPanel $Content.Orientation = "Horizontal" $Content.AddChild($Image) $Content.AddChild($TextBlock) # XML defining the main window. [XML]$Xaml = @" &lt;Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Name="Window" Title="" SizeToContent="WidthAndHeight" WindowStartupLocation="CenterScreen" WindowStyle="None" ResizeMode="NoResize" AllowsTransparency="True" Background="Transparent" Opacity="1"&gt; &lt;Window.Resources&gt; &lt;Style TargetType="{x:Type Button}"&gt; &lt;Setter Property="Template"&gt; &lt;Setter.Value&gt; &lt;ControlTemplate TargetType="Button"&gt; &lt;Border&gt; &lt;Grid Background="{TemplateBinding Background}"&gt; &lt;ContentPresenter /&gt; &lt;/Grid&gt; &lt;/Border&gt; &lt;/ControlTemplate&gt; &lt;/Setter.Value&gt; &lt;/Setter&gt; &lt;/Style&gt; &lt;/Window.Resources&gt; &lt;Border x:Name="MainBorder" Margin="10" CornerRadius="0" BorderThickness="2" BorderBrush="Gray" Padding="0" &gt; &lt;Border.Effect&gt; &lt;DropShadowEffect x:Name="DSE" Color="Black" Direction="270" BlurRadius="20" ShadowDepth="3" Opacity="0.6" /&gt; &lt;/Border.Effect&gt; &lt;Border.Triggers&gt; &lt;EventTrigger RoutedEvent="Window.Loaded"&gt; &lt;BeginStoryboard&gt; &lt;Storyboard&gt; &lt;DoubleAnimation Storyboard.TargetName="DSE" Storyboard.TargetProperty="ShadowDepth" From="0" To="3" Duration="0:0:1" AutoReverse="False" /&gt; &lt;DoubleAnimation Storyboard.TargetName="DSE" Storyboard.TargetProperty="BlurRadius" From="0" To="20" Duration="0:0:1" AutoReverse="False" /&gt; &lt;/Storyboard&gt; &lt;/BeginStoryboard&gt; &lt;/EventTrigger&gt; &lt;/Border.Triggers&gt; &lt;Grid &gt; &lt;Border Name="Mask" CornerRadius="0" Background="Black" /&gt; &lt;Grid x:Name="Grid" Background="Black"&gt; &lt;Grid.OpacityMask&gt; &lt;VisualBrush Visual="{Binding ElementName=Mask}"/&gt; &lt;/Grid.OpacityMask&gt; &lt;StackPanel Name="StackPanel" &gt; &lt;TextBox Name="TitleBar" IsReadOnly="True" IsHitTestVisible="False" Text="$Title" Padding="10" FontFamily="Segoe UI" FontSize="14" Foreground="White" FontWeight="Normal" Background="Black" HorizontalAlignment="Stretch" VerticalAlignment="Center" Width="Auto" HorizontalContentAlignment="Center" BorderThickness="0"/&gt; &lt;DockPanel Name="ContentHost" Margin="0,10,0,10" &gt; &lt;/DockPanel&gt; &lt;DockPanel Name="ButtonHost" LastChildFill="False" HorizontalAlignment="Center" &gt; &lt;/DockPanel&gt; &lt;/StackPanel&gt; &lt;/Grid&gt; &lt;/Grid&gt; &lt;/Border&gt; &lt;/Window&gt; "@ # XML Defining the confirm button. [XML]$ButtonXaml = @" &lt;Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Width="Auto" Height="30" FontFamily="Segui" FontSize="16" Background="Transparent" Foreground="White" BorderThickness="1" Margin="10" Padding="20,0,20,0" HorizontalAlignment="Right" Cursor="Hand"/&gt; "@ [XML]$ButtonTextXaml = @" &lt;TextBlock xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" FontFamily="Segoe UI" FontSize="16" Background="Gray" Foreground="Black" Padding="20,5,20,5" HorizontalAlignment="Center" VerticalAlignment="Center"/&gt; "@ # Create the window. $Window = [Windows.Markup.XamlReader]::Load((New-Object -TypeName System.Xml.XmlNodeReader -ArgumentList $xaml)) # Create the button $Button = [Windows.Markup.XamlReader]::Load((New-Object -TypeName System.Xml.XmlNodeReader -ArgumentList $ButtonXaml)) $ButtonText = [Windows.Markup.XamlReader]::Load((New-Object -TypeName System.Xml.XmlNodeReader -ArgumentList $ButtonTextXaml)) $ButtonText.Text = $ButtonMessage $Button.Content = $ButtonText $Button.Add_MouseEnter( { $This.Content.FontSize = "20" }) $Button.Add_MouseLeave( { $This.Content.FontSize = "20" }) $Button.Add_Click( { $Window.Close() }) $Window.FindName('ButtonHost').AddChild($Button) $Window.FindName('ContentHost').AddChild($Content) $Window.Add_Loaded( { $This.Activate() }) # Set the window to be always on top. $Window.Topmost = $true $null = $window.Dispatcher.InvokeAsync{ $window.ShowDialog() }.Wait() }]]></summary></entry><entry><title type="html">Reporting on Remote File Permissions</title><link href="http://greybeard.ai/2021/02/14/PoweshellRemoteSharePermissionsReport" rel="alternate" type="text/html" title="Reporting on Remote File Permissions" /><published>2021-02-14T14:25:00+00:00</published><updated>2021-02-14T14:25:00+00:00</updated><id>http://greybeard.ai/2021/02/14/PoweshellRemoteSharePermissionsReport</id><content type="html" xml:base="http://greybeard.ai/2021/02/14/PoweshellRemoteSharePermissionsReport"><![CDATA[<h1 id="a-quick-remix">A Quick Remix</h1>

<p>Shortly after I posted <a href="https://dbeta.com/2021/02/05/PoweshellSharePermissionsReport">Powershell: Reporting on File Permissions</a>, I was asked about how it could be done for remote target. Although in my case it easy to run it on the server I want to scan, in many organizations getting permissions to access servers directly, or getting the server team to implement scheduled scanning might not be possible. Thankfully, it only takes rather minor change to make the exact same script hit a target server.</p>

<p>The big gotcha would be if you have permissions or not to view a share, or files in a share. If you don’t have permissions, it will put an error on the console, but the reports won’t mention them, it will just skip right past. Maybe error reporting will be a future feature.</p>

<h1 id="enumerating-shares-on-a-remote-server">Enumerating Shares on a Remote Server</h1>

<p>Looking online, I only found one simple, built in, reliable way to enumerate the shares on a remote server without needing WMI access to the target server. <code class="language-plaintext highlighter-rouge">net view servername</code>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Shared resources at localhost



Share name  Type  Used as  Comment

-------------------------------------------------------------------------------
Test Share  Disk
Users       Disk
The command completed successfully.
</code></pre></div></div>

<p>It’s simple, but the output really stinks for automation. So let’s get that into a powershell useful output.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ServerName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"localhost"</span><span class="w">
</span><span class="n">net</span><span class="w"> </span><span class="nx">view</span><span class="w"> </span><span class="nv">$ServerName</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="kr">if</span><span class="p">(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">indexof</span><span class="p">(</span><span class="s2">" Disk "</span><span class="p">)</span><span class="w"> </span><span class="o">-gt</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$ShareName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="bp">$_</span><span class="w"> </span><span class="o">-split</span><span class="w"> </span><span class="s2">"\s{2,}"</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="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Let’s break that down. First we call <code class="language-plaintext highlighter-rouge">net view</code> as normal, but we pipe that directly in to a <code class="language-plaintext highlighter-rouge">ForEach-Object</code>, which loops through each line of the output as separate strings.</p>

<p>Once in a loop, we use <code class="language-plaintext highlighter-rouge">indexof()</code> to count how many times <code class="language-plaintext highlighter-rouge">" Disk "</code> shows up in the line. Note the spaces on both sides. That filters out all of the none share lines.</p>

<p>Next, we use <code class="language-plaintext highlighter-rouge">-split</code> to break the string into pieces. We are making an assumption, that’s not 100% certain, that none of your share names include any double spaces. <code class="language-plaintext highlighter-rouge">\s{2,}</code> tells split to look for 2 concurrent spaces. Because the output of <code class="language-plaintext highlighter-rouge">net view</code> always seems to put at least 2 spaces between Share name and Type. Once we have it all split up, we ask for the first result, which will always be share name. <code class="language-plaintext highlighter-rouge">[0]</code> might not be obviously the first part of the split up strings, but in programming, lists normally start at 0 instead of 1 the way humans normally think of it. So when you have a list on computers, you generally ask for 0 to get the first.</p>

<p>Alright, we have a nice output of every share on the machine now, that’s a great start. But how do we re-factor that back into my original script.</p>

<p>Well, if you look down low in my script, you will see it expects the $Share to have objects with 2 properties, Name and Path. So what we want to do is create an object out of the output of <code class="language-plaintext highlighter-rouge">net view</code> that replicates what we were using from Get-SmbShare. Thankfully we already have all the info we need, the share name and the server name are all we need to create an object that the rest of my script can use.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
    </span><span class="s1">'Name'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ShareName</span><span class="w">
    </span><span class="s1">'Path'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"\\</span><span class="nv">$ServerName</span><span class="s2">\</span><span class="nv">$ShareName</span><span class="s2">"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>See, custom objects aren’t that hard. But how to we build that into the script? We put it in the <code class="language-plaintext highlighter-rouge">ForEach-Object</code> section.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ServerName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"localhost"</span><span class="w">
</span><span class="n">net</span><span class="w"> </span><span class="nx">view</span><span class="w"> </span><span class="s2">"</span><span class="nv">$ServerName</span><span class="s2">"</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="kr">if</span><span class="p">(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">indexof</span><span class="p">(</span><span class="s2">" Disk "</span><span class="p">)</span><span class="w"> </span><span class="o">-gt</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$ShareName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="bp">$_</span><span class="w"> </span><span class="o">-split</span><span class="w"> </span><span class="s2">"\s{2,}"</span><span class="p">)[</span><span class="mi">0</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="s1">'Name'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ShareName</span><span class="w">
            </span><span class="s1">'Path'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"\\</span><span class="nv">$ServerName</span><span class="s2">\</span><span class="nv">$ShareName</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="p">}</span><span class="w">
</span></code></pre></div></div>
<p>So when that loops through, it is going to return just the custom object we built there. So right now, if we replace Get-SmbShare function, we’d pretty much have it working. But let’s modify the Where-Object part to account for it.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ServerName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"localhost"</span><span class="w">
</span><span class="nv">$Shares</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">net</span><span class="w"> </span><span class="nx">view</span><span class="w"> </span><span class="s2">"</span><span class="nv">$ServerName</span><span class="s2">"</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="kr">if</span><span class="p">(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">indexof</span><span class="p">(</span><span class="s2">" Disk "</span><span class="p">)</span><span class="w"> </span><span class="o">-gt</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$ShareName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="bp">$_</span><span class="w"> </span><span class="o">-split</span><span class="w"> </span><span class="s2">"\s{2,}"</span><span class="p">)[</span><span class="mi">0</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="s1">'Name'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ShareName</span><span class="w">
            </span><span class="s1">'Path'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"\\</span><span class="nv">$ServerName</span><span class="s2">\</span><span class="nv">$ShareName</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="p">}</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="s2">"SYSVOL"</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="s2">"NETLOGON"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>And there we have it, that technically works. We don’t need the <code class="language-plaintext highlighter-rouge">*$</code> part, because <code class="language-plaintext highlighter-rouge">net view</code> doesn’t return hidden shares. And we don’t need <code class="language-plaintext highlighter-rouge">FileSystemDirectory</code>, because we are already filtering to disk shares only, and <code class="language-plaintext highlighter-rouge">ShareType</code> isn’t a valid variable on the share.</p>

<h1 id="making-it-easier-to-run-against-more-targets">Making it Easier to Run Against More Targets</h1>

<p>The script works as is, but imagine you want to be able to set this up on a schedule, perhaps saving the logs to specific directories, targeting multiple servers. Perhaps even scanning from workstations or user accounts to see what they see. Well, you could have 20 different copies of the script, one for each server and save target, or you could include parameters on the script, which would allow you, when you run the PS1, to specify a target server and path to save the reports to. Thankfully, all we need to do is replace all the variables at the beginning with parameters we can pass data into.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">param</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">Mandatory</span><span class="o">=</span><span class="bp">$true</span><span class="p">)]</span><span class="w">
    </span><span class="p">[</span><span class="n">ValidatePattern</span><span class="p">(</span><span class="s1">'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$'</span><span class="p">)]</span><span class="w">
    </span><span class="p">[</span><span class="n">String</span><span class="p">]</span><span class="w"> </span><span class="nv">$ServerName</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">Mandatory</span><span class="o">=</span><span class="bp">$false</span><span class="p">)][</span><span class="n">String</span><span class="p">]</span><span class="w"> </span><span class="nv">$OutputPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"."</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">Mandatory</span><span class="o">=</span><span class="bp">$false</span><span class="p">)][</span><span class="n">String</span><span class="p">]</span><span class="w"> </span><span class="nv">$OutputFileName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"</span><span class="nv">$ServerName</span><span class="s2">-Shares"</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">Mandatory</span><span class="o">=</span><span class="bp">$false</span><span class="p">)][</span><span class="n">Int</span><span class="p">]</span><span class="w"> </span><span class="nv">$RecurseDepth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">2</span><span class="w">
</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p>That’s going to force whoever is running it to supply a server name, but nothing else. But optionally the other things can be tweaked. I have added a simple validation pattern to the server name to prevent accidental errors that may result in very big problems (running unexpected commands on a command line). It’s not a 100% safeguard, I’m guessing, but it should prevent accidents.</p>

<p>Let’s put it all together now.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">param</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">Mandatory</span><span class="o">=</span><span class="bp">$true</span><span class="p">)]</span><span class="w">
    </span><span class="p">[</span><span class="n">ValidatePattern</span><span class="p">(</span><span class="s1">'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$'</span><span class="p">)]</span><span class="w">
    </span><span class="p">[</span><span class="n">String</span><span class="p">]</span><span class="w"> </span><span class="nv">$ServerName</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">Mandatory</span><span class="o">=</span><span class="bp">$false</span><span class="p">)][</span><span class="n">String</span><span class="p">]</span><span class="w"> </span><span class="nv">$OutputPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"."</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">Mandatory</span><span class="o">=</span><span class="bp">$false</span><span class="p">)][</span><span class="n">String</span><span class="p">]</span><span class="w"> </span><span class="nv">$OutputFileName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"</span><span class="nv">$ServerName</span><span class="s2">-Shares"</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">Mandatory</span><span class="o">=</span><span class="bp">$false</span><span class="p">)][</span><span class="n">Int</span><span class="p">]</span><span class="w"> </span><span class="nv">$RecurseDepth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">2</span><span class="w">
</span><span class="p">)</span><span class="w">

</span><span class="c">#Automatically gets the list of shares. Excludes system shares and non-file shares like printers.</span><span class="w">
</span><span class="nv">$Shares</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">net</span><span class="w"> </span><span class="nx">view</span><span class="w"> </span><span class="s2">"</span><span class="nv">$ServerName</span><span class="s2">"</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="kr">if</span><span class="p">(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">indexof</span><span class="p">(</span><span class="s2">" Disk "</span><span class="p">)</span><span class="w"> </span><span class="o">-gt</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$ShareName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="bp">$_</span><span class="w"> </span><span class="o">-split</span><span class="w"> </span><span class="s2">"\s{2,}"</span><span class="p">)[</span><span class="mi">0</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="s1">'Name'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ShareName</span><span class="w">
            </span><span class="s1">'Path'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"\\</span><span class="nv">$ServerName</span><span class="s2">\</span><span class="nv">$ShareName</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="p">}</span><span class="w">
</span><span class="c">#Gets NTDomain to help with name filters</span><span class="w">
</span><span class="nv">$Domain</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">Get-WmiObject</span><span class="w"> </span><span class="nx">Win32_NTDomain</span><span class="p">)</span><span class="o">.</span><span class="nf">DomainName</span><span class="w">

</span><span class="c">#List of names to filter out. Great for client facing reports, might not be best for change monitoring.</span><span class="w">
</span><span class="nv">$NameFilter</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"</span><span class="nv">$Domain</span><span class="s2">\MSPAdmin"</span><span class="p">,</span><span class="w"> </span><span class="s1">'BUILTIN\Administrators'</span><span class="p">,</span><span class="w"> </span><span class="s1">'NT AUTHORITY\SYSTEM'</span><span class="p">,</span><span class="w"> </span><span class="s2">"</span><span class="nv">$Domain</span><span class="s2">\Domain Admins"</span><span class="w">


</span><span class="c">#Converts the Propagation and Inheritance flags into human readable output</span><span class="w">
</span><span class="kr">Function</span><span class="w"> </span><span class="nf">Get-HumanReadablePropagation</span><span class="p">(</span><span class="nv">$PropagationFlags</span><span class="p">,</span><span class="w"> </span><span class="nv">$InheritanceFlags</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">switch</span><span class="w"> </span><span class="p">(</span><span class="nv">$PropagationFlags</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="s2">"None"</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="kr">switch</span><span class="w"> </span><span class="p">(</span><span class="nv">$InheritanceFlags</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="s2">"None"</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="kr">return</span><span class="w"> </span><span class="s2">"This Folder Only"</span><span class="w"> </span><span class="p">}</span><span class="w">
                </span><span class="s2">"Container"</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="kr">return</span><span class="w"> </span><span class="s2">"This Folder and Sub-folders"</span><span class="w"> </span><span class="p">}</span><span class="w">
                </span><span class="s2">"Object"</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="kr">return</span><span class="w"> </span><span class="s2">"This Folder and Files"</span><span class="w"> </span><span class="p">}</span><span class="w">
                </span><span class="n">Default</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="kr">return</span><span class="w"> </span><span class="s2">"This Folder, Sub-folders and Files"</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><span class="w">
        </span><span class="n">Default</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="kr">switch</span><span class="w"> </span><span class="p">(</span><span class="nv">$InheritanceFlags</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="s2">"Container"</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="kr">return</span><span class="s2">"Sub-Folders"</span><span class="w"> </span><span class="p">}</span><span class="w">
                </span><span class="s2">"Object"</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="kr">return</span><span class="w"> </span><span class="s2">"Files"</span><span class="w"> </span><span class="p">}</span><span class="w">
                </span><span class="n">Default</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="kr">return</span><span class="w"> </span><span class="s2">"Sub-folders and Files"</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><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="nv">$Output</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Share</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$Shares</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">  
    </span><span class="nv">$RootPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Share</span><span class="o">.</span><span class="nf">Path</span><span class="w">
    </span><span class="nv">$FolderPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ChildItem</span><span class="w"> </span><span class="nt">-Directory</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$RootPath</span><span class="w"> </span><span class="nt">-Recurse</span><span class="w"> </span><span class="nt">-Depth</span><span class="w"> </span><span class="nv">$RecurseDepth</span><span class="w"> </span><span class="nt">-Force</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">PSPath</span><span class="w"> </span><span class="o">-notlike</span><span class="w"> </span><span class="s2">"*DfsrPrivate*"</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="nv">$MainFolderACL</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Acl</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$RootPath</span><span class="w">
    </span><span class="kr">ForEach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Access</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$MainFolderACL</span><span class="o">.</span><span class="nf">Access</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$Propagation</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-HumanReadablePropagation</span><span class="p">(</span><span class="nv">$Access</span><span class="o">.</span><span class="nf">PropagationFlags</span><span class="p">,</span><span class="w"> </span><span class="nv">$Access</span><span class="o">.</span><span class="nf">InheritanceFlags</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="s1">'Share'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Share</span><span class="err">.</span><span class="nx">Name</span><span class="w">
            </span><span class="s1">'Folder Name'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$RootPath</span><span class="w">
            </span><span class="s1">'Group/User'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Access</span><span class="err">.</span><span class="nx">IdentityReference</span><span class="w">
            </span><span class="s1">'Permissions'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Access</span><span class="err">.</span><span class="nx">FileSystemRights</span><span class="w">
            </span><span class="s1">'Inherited'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Access</span><span class="err">.</span><span class="nx">IsInherited</span><span class="w">
            </span><span class="s1">'Propagation'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Propagation</span><span class="w">
        </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">$Folder</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$FolderPath</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$Acl</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Acl</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$Folder</span><span class="o">.</span><span class="nf">FullName</span><span class="w">
        </span><span class="nx">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Acl</span><span class="o">.</span><span class="nf">Access</span><span class="o">.</span><span class="nf">IsInherited</span><span class="w"> </span><span class="o">-contains</span><span class="w"> </span><span class="bp">$false</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">$Access</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$Acl</span><span class="o">.</span><span class="nf">Access</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nv">$Propagation</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-HumanReadablePropagation</span><span class="p">(</span><span class="nv">$Access</span><span class="o">.</span><span class="nf">PropagationFlags</span><span class="p">,</span><span class="w"> </span><span class="nv">$Access</span><span class="o">.</span><span class="nf">InheritanceFlags</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="s1">'Share'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Share</span><span class="err">.</span><span class="nx">Name</span><span class="w">
                    </span><span class="s1">'Folder Name'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Folder</span><span class="err">.</span><span class="nx">FullName</span><span class="w">
                    </span><span class="s1">'Group/User'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Access</span><span class="err">.</span><span class="nx">IdentityReference</span><span class="p">;</span><span class="w">
                    </span><span class="s1">'Permissions'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Access</span><span class="err">.</span><span class="nx">FileSystemRights</span><span class="w">
                    </span><span class="s1">'Inherited'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Access</span><span class="err">.</span><span class="nx">IsInherited</span><span class="w">
                    </span><span class="s1">'Propagation'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Propagation</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><span class="w">
    </span><span class="p">}</span><span class="w">   
</span><span class="p">}</span><span class="w">

</span><span class="c">#Output Phase</span><span class="w">

</span><span class="nv">$Output</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Group-Object</span><span class="w"> </span><span class="s2">"Share"</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">$ShareOutput</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Group</span><span class="w">
    </span><span class="nv">$FilteredOutput</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ShareOutput</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="s2">"Group/User"</span><span class="w"> </span><span class="nt">-notin</span><span class="w"> </span><span class="nv">$NameFilter</span><span class="w"> </span><span class="p">}</span><span class="w"> 
    </span><span class="nv">$FormattedOutput</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$FilteredOutput</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Format-Table</span><span class="w"> </span><span class="s2">"Group/User"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Permissions"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Inherited"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Propagation"</span><span class="w"> </span><span class="nt">-GroupBy</span><span class="w"> </span><span class="s2">"Folder Name"</span><span class="w">
    </span><span class="nv">$FormattedOutput</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Out-File</span><span class="w"> </span><span class="s2">"</span><span class="nv">$OutputPath</span><span class="s2">\</span><span class="nv">$OutputFileName</span><span class="s2">-</span><span class="si">$(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="si">)</span><span class="s2">.txt"</span><span class="w"> </span><span class="nt">-Width</span><span class="w"> </span><span class="nx">120</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"================</span><span class="se">`n</span><span class="si">$(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="si">)</span><span class="se">`n</span><span class="s2">================"</span><span class="w">
    </span><span class="nv">$FormattedOutput</span><span class="w">
</span><span class="p">}</span><span class="w">


</span><span class="nv">$FilteredOutput</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Output</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="s2">"Group/User"</span><span class="w"> </span><span class="nt">-notin</span><span class="w"> </span><span class="nv">$NameFilter</span><span class="w"> </span><span class="p">}</span><span class="w"> 
</span><span class="nv">$FilteredOutput</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Export-Csv</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="s2">"</span><span class="nv">$OutputPath</span><span class="s2">\</span><span class="nv">$OutputFileName</span><span class="s2">.csv"</span><span class="w">
</span></code></pre></div></div>]]></content><author><name>Danial Bulloch</name></author><category term="powershell" /><summary type="html"><![CDATA[A Quick Remix]]></summary></entry><entry><title type="html">Reporting on File Permissions</title><link href="http://greybeard.ai/2021/02/05/PoweshellSharePermissionsReport" rel="alternate" type="text/html" title="Reporting on File Permissions" /><published>2021-02-05T17:00:00+00:00</published><updated>2021-02-05T17:00:00+00:00</updated><id>http://greybeard.ai/2021/02/05/PoweshellSharePermissionsReport</id><content type="html" xml:base="http://greybeard.ai/2021/02/05/PoweshellSharePermissionsReport"><![CDATA[<h1 id="new-year-new-segment">New Year, New Segment</h1>

<p>Hello everyone, I’ve decided to decouple my stuff from RMM specific scripts where possible. This starts a new blog series for me, just pure PowerShell. We have several clients that need regular reports on who has access to what files, but making those reports is either a lot of manual work, or at the very least, tedious amounts of documentation that will likely fall out of sync when a tech doesn’t bother updating it when they make a change.</p>

<p>So I sat down with the task of creating the reports in an automated way. As generic as possible so it could hit multiple clients with minimal or no modifications. What I’ve made is a script that will loop through all network shares, and grab the file permissions of them, and write each to a file. It, by default, only goes 2 folders deep, to prevent scanning millions of folders all of which have the same permissions as their parent. It only reports on folders that have permissions that do not match their parent, so the reports are no longer than they need to be. It prints them to text files that are pretty easy for humans to read. A crafty person might even write them to more user friendly reports.</p>

<p>Ideally you would parameterize the output path and recursedepth for easy changes without having to change the code, but for an easy quick run, I have left them not.</p>

<p>Small warning, -depth parameter of Get-ChildObject is only valid for PowerShell 5.1 and up. So that may create issues. You can exclude that parameter, but doing so will significantly increase scan times. You can get the same limits by removing -Depth and -Recurse, but changing the path to be something like “-Path $RootPath\*, $RootPath\*\*”</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#Edit these to meet your liking.</span><span class="w">
</span><span class="nv">$OutputPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"."</span><span class="w">
</span><span class="nv">$OutputFileName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Shares"</span><span class="w">
</span><span class="nv">$RecurseDepth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">2</span><span class="w">

</span><span class="c">#Automatically gets the list of shares. Excludes system shares and non-file shares like printers.</span><span class="w">
</span><span class="nv">$Shares</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-SmbShare</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="w"> </span><span class="o">-NotLike</span><span class="w"> </span><span class="s2">"*$"</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="s2">"SYSVOL"</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="s2">"NETLOGON"</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">ShareType</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"FileSystemDirectory"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="c">#Gets NTDomain to help with name filters</span><span class="w">
</span><span class="nv">$Domain</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">Get-WmiObject</span><span class="w"> </span><span class="nx">Win32_NTDomain</span><span class="p">)</span><span class="o">.</span><span class="nf">DomainName</span><span class="w">

</span><span class="c">#List of names to filter out. Great for client facing reports, might not be best for change monitoring.</span><span class="w">
</span><span class="nv">$NameFilter</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"</span><span class="nv">$Domain</span><span class="s2">\MSPAdmin"</span><span class="p">,</span><span class="w"> </span><span class="s1">'BUILTIN\Administrators'</span><span class="p">,</span><span class="w"> </span><span class="s1">'NT AUTHORITY\SYSTEM'</span><span class="p">,</span><span class="w"> </span><span class="s2">"</span><span class="nv">$Domain</span><span class="s2">\Domain Admins"</span><span class="w">


</span><span class="c">#Converts the Propagation and Inheritance flags into human readable output</span><span class="w">
</span><span class="kr">Function</span><span class="w"> </span><span class="nf">Get-HumanReadablePropagation</span><span class="p">(</span><span class="nv">$PropagationFlags</span><span class="p">,</span><span class="w"> </span><span class="nv">$InheritanceFlags</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">switch</span><span class="w"> </span><span class="p">(</span><span class="nv">$PropagationFlags</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="s2">"None"</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="kr">switch</span><span class="w"> </span><span class="p">(</span><span class="nv">$InheritanceFlags</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="s2">"None"</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="kr">return</span><span class="w"> </span><span class="s2">"This Folder Only"</span><span class="w"> </span><span class="p">}</span><span class="w">
                </span><span class="s2">"Container"</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="kr">return</span><span class="w"> </span><span class="s2">"This Folder and Sub-folders"</span><span class="w"> </span><span class="p">}</span><span class="w">
                </span><span class="s2">"Object"</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="kr">return</span><span class="w"> </span><span class="s2">"This Folder and Files"</span><span class="w"> </span><span class="p">}</span><span class="w">
                </span><span class="n">Default</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="kr">return</span><span class="w"> </span><span class="s2">"This Folder, Sub-folders and Files"</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><span class="w">
        </span><span class="n">Default</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="kr">switch</span><span class="w"> </span><span class="p">(</span><span class="nv">$InheritanceFlags</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="s2">"Container"</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="kr">return</span><span class="s2">"Sub-Folders"</span><span class="w"> </span><span class="p">}</span><span class="w">
                </span><span class="s2">"Object"</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="kr">return</span><span class="w"> </span><span class="s2">"Files"</span><span class="w"> </span><span class="p">}</span><span class="w">
                </span><span class="n">Default</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="kr">return</span><span class="w"> </span><span class="s2">"Sub-folders and Files"</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><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="nv">$Output</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Share</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$Shares</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">  
    </span><span class="nv">$RootPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Share</span><span class="o">.</span><span class="nf">Path</span><span class="w">
    </span><span class="nv">$FolderPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ChildItem</span><span class="w"> </span><span class="nt">-Directory</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$RootPath</span><span class="w"> </span><span class="nt">-Recurse</span><span class="w"> </span><span class="nt">-Depth</span><span class="w"> </span><span class="nv">$RecurseDepth</span><span class="w"> </span><span class="nt">-Force</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">PSPath</span><span class="w"> </span><span class="o">-notlike</span><span class="w"> </span><span class="s2">"*DfsrPrivate*"</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="nv">$MainFolderACL</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Acl</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$RootPath</span><span class="w">
    </span><span class="kr">ForEach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Access</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$MainFolderACL</span><span class="o">.</span><span class="nf">Access</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$Propagation</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-HumanReadablePropagation</span><span class="p">(</span><span class="nv">$Access</span><span class="o">.</span><span class="nf">PropagationFlags</span><span class="p">,</span><span class="w"> </span><span class="nv">$Access</span><span class="o">.</span><span class="nf">InheritanceFlags</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="s1">'Share'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Share</span><span class="err">.</span><span class="nx">Name</span><span class="w">
            </span><span class="s1">'Folder Name'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$RootPath</span><span class="w">
            </span><span class="s1">'Group/User'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Access</span><span class="err">.</span><span class="nx">IdentityReference</span><span class="w">
            </span><span class="s1">'Permissions'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Access</span><span class="err">.</span><span class="nx">FileSystemRights</span><span class="w">
            </span><span class="s1">'Inherited'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Access</span><span class="err">.</span><span class="nx">IsInherited</span><span class="w">
            </span><span class="s1">'Propagation'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Propagation</span><span class="w">
        </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">$Folder</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$FolderPath</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">$Acl</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Acl</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$Folder</span><span class="o">.</span><span class="nf">FullName</span><span class="w">
        </span><span class="nx">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Acl</span><span class="o">.</span><span class="nf">Access</span><span class="o">.</span><span class="nf">IsInherited</span><span class="w"> </span><span class="o">-contains</span><span class="w"> </span><span class="bp">$false</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">$Access</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$Acl</span><span class="o">.</span><span class="nf">Access</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nv">$Propagation</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-HumanReadablePropagation</span><span class="p">(</span><span class="nv">$Access</span><span class="o">.</span><span class="nf">PropagationFlags</span><span class="p">,</span><span class="w"> </span><span class="nv">$Access</span><span class="o">.</span><span class="nf">InheritanceFlags</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="s1">'Share'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Share</span><span class="err">.</span><span class="nx">Name</span><span class="w">
                    </span><span class="s1">'Folder Name'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Folder</span><span class="err">.</span><span class="nx">FullName</span><span class="w">
                    </span><span class="s1">'Group/User'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Access</span><span class="err">.</span><span class="nx">IdentityReference</span><span class="p">;</span><span class="w">
                    </span><span class="s1">'Permissions'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Access</span><span class="err">.</span><span class="nx">FileSystemRights</span><span class="w">
                    </span><span class="s1">'Inherited'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Access</span><span class="err">.</span><span class="nx">IsInherited</span><span class="w">
                    </span><span class="s1">'Propagation'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Propagation</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><span class="w">
    </span><span class="p">}</span><span class="w">   
</span><span class="p">}</span><span class="w">

</span><span class="c">#Output Phase</span><span class="w">

</span><span class="nv">$Output</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Group-Object</span><span class="w"> </span><span class="s2">"Share"</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">$ShareOutput</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Group</span><span class="w">
    </span><span class="nv">$FilteredOutput</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ShareOutput</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="s2">"Group/User"</span><span class="w"> </span><span class="nt">-notin</span><span class="w"> </span><span class="nv">$NameFilter</span><span class="w"> </span><span class="p">}</span><span class="w"> 
    </span><span class="nv">$FormattedOutput</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$FilteredOutput</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Format-Table</span><span class="w"> </span><span class="s2">"Group/User"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Permissions"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Inherited"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Propagation"</span><span class="w"> </span><span class="nt">-GroupBy</span><span class="w"> </span><span class="s2">"Folder Name"</span><span class="w">
    </span><span class="nv">$FormattedOutput</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Out-File</span><span class="w"> </span><span class="s2">"</span><span class="nv">$OutputPath</span><span class="s2">\</span><span class="nv">$OutputFileName</span><span class="s2">-</span><span class="si">$(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="si">)</span><span class="s2">.txt"</span><span class="w"> </span><span class="nt">-Width</span><span class="w"> </span><span class="nx">120</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"================</span><span class="se">`n</span><span class="si">$(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="si">)</span><span class="se">`n</span><span class="s2">================"</span><span class="w">
    </span><span class="nv">$FormattedOutput</span><span class="w">
</span><span class="p">}</span><span class="w">


</span><span class="nv">$FilteredOutput</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Output</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="s2">"Group/User"</span><span class="w"> </span><span class="nt">-notin</span><span class="w"> </span><span class="nv">$NameFilter</span><span class="w"> </span><span class="p">}</span><span class="w"> 
</span><span class="nv">$FilteredOutput</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Export-Csv</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="s2">"</span><span class="nv">$OutputPath</span><span class="s2">\</span><span class="nv">$OutputFileName</span><span class="s2">.csv"</span><span class="w">
</span></code></pre></div></div>]]></content><author><name>Danial Bulloch</name></author><category term="powershell" /><summary type="html"><![CDATA[New Year, New Segment]]></summary></entry></feed>