Even More PowerShell Fun

The Story

It’s another day, and we all know what that means… yes, another blog post, and even more PowerShell! Can you feel all the power!?!?!

This time it came down to the storage size of my Exchange servers C:\ which turns out to be due to Logs. Logs are great, and best practice is to only clear them if you have a backup copy. Often is the case that logs can be truncated after a backup via VSS by many backup solutions however in my case I could and probably should get that validated with Veeam (as I can’t seem to get that working ‘out of the box’) at the moment. So instead I wanted to know what was “usually” done server side even if someone was not implementing a backup solution.

Source: https://social.technet.microsoft.com/wiki/contents/articles/31117.exchange-201320162019-logging-clear-out-the-log-files.aspx

Neat, but the script is just alright, good for them doing what they want and that’s running it as a scheduled task. Not my goal, but a great source and starting point… let’s have some fun and give this script some roids, much like my last one… I’ll give this a home on GitHub.

Things learned…

  1. Working with the Registry
  2. Determining if Elevated (This is great and I may have a solution to the conundrum in my previous PowerShell post)
  3. Getting a Number, and validating it
  4. Validating Objects by Type
  5. Getting Folder Sizes

Check out my script for all the fun coding bits. I’m a bit tired now as it’s getting late so not much blogging, all more coding. ๐Ÿ™‚

ErrorAction Stop Not Stopping Script

Quick Educational note (Source)

$ETLLogKey2 = 'HKLM:\SOFTWARE\Microsoft\Search Foundation for Exchange\Diagnostics'
try{Get-ItemProperty -Path $ETLLogKey2 -ErrorAction Stop}
catch{Write-Host "No Key"}
Write-Host "This should not hit"

Produces:

Well poop… The catch block was triggered but the script did not stop…

Oddly, changing to Throw, which is ugly does make the script stop…

$ETLLogKey2 = 'HKLM:\SOFTWARE\Microsoft\Search Foundation for Exchange\Diagnostics'
try{Get-ItemProperty -Path $ETLLogKey2 -ErrorAction Stop}
catch{throw "No Key"}
Write-Host "This should not hit"

Nice it worked this time, but it’s ugly…

Write-Error is just as ugly, but doesn’t stop the script?

$ETLLogKey2 = 'HKLM:\SOFTWARE\Microsoft\Search Foundation for Exchange\Diagnostics'
try{Get-ItemProperty -Path $ETLLogKey2 -ErrorAction Stop}
catch{Write-Error "No Key"}
Write-Host "This should not hit"

Produces:

Yet if I follow Sages answer in the source, and do a script variable for the stop action it then works???!?!

$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
$ETLLogKey2 = 'HKLM:\SOFTWARE\Microsoft\Search Foundation for Exchange\Diagnostics'
try{Get-ItemProperty -Path $ETLLogKey2}
catch{Write-Error "No Key"}
Write-Host "This should not hit"

Those are really weird results, but all still ugly…ย  so it seems even though -ErrorAction Stop causes a non-terminating error to be treated as a terminating error, but depending on what you do in the catch block determines if there’s a break/exit event being done. In my case to have things look nice and actually stop the script I have to do the follow.

$ETLLogKey2 = 'HKLM:\SOFTWARE\Microsoft\Search Foundation for Exchange\Diagnostics'
try{Get-ItemProperty -Path $ETLLogKey2 -ErrorAction Stop}
catch{Write-host "No Key";break}
Write-Host "This should not hit"

Which finally produced the output I wanted. (I could have also used exit in place of break)

Finally!

Validating URLs:

Source:ย 

Good but broken due to the match on the scheme extension:

function isURI($address) {
	($address -as [System.URI]).AbsoluteURI -ne $null
}

function isURIWeb($address) {
	$uri = $address -as [System.URI]
	$uri.AbsoluteURI -ne $null -and $uri.Scheme -match '[http|https]'
}


isURI('http://www.powershell.com')
isURI('test')
isURI($null)
isURI('zzz://zumsel.zum')

"-" * 50

isURIWeb('http://www.powershell.com')
isURIWeb('test')
isURIWeb($null)
isURIWeb('zzz://zumsel.zum')
isURIWeb('hp:') #Return True

 

Better results with:

function isURI($address) {
($address -as [System.URI]).AbsoluteURI -ne $null
}

function isURIWeb($address) {
$uri = $address -as [System.URI]
$uri.AbsoluteURI -ne $null -and $uri.Scheme -match "http|https"
}


isURI('http://www.powershell.com')
isURI('test')
isURI($null)
isURI('zzz://zumsel.zum')

"-" * 50

isURIWeb('http://www.powershell.com')
isURIWeb('test')
isURIWeb($null)
isURIWeb('zzz://zumsel.zum')
isURIWeb('hp:') #Return True

Powershell fun

PowerShell

Finding specific files

The Folder path in which you need to search
$Path = ‘C:\folder\path’

The Regular Expression you like to match
$RegEx = ‘[^-\w\.]’

The CSV File name
$OutCSV=’C:\temp\fileResults.csv’

Get-ChildItem -Path $Path -Recurse | Where-Object -FilterScript { $_.Name -match $RegEx } | Export-Csv -Path $OutCSV -NoTypeInformation

 

And YA! files modified in the last 24 hrs…

Get-ChildItem -Path $Path -Recurse | Where-Object -FilterScript { ((get-date) - $_.LastWriteTime).totalhours -le 24 } | Export-Csv -Path $OutCSV -NoTypeInformation

Finding Objects Properties

I used to use

$Object | FT *

to try and find properties to use, but easier is

$Object | get-Member

So much easier to see all an objects properties, and methods… and whatever ๐Ÿ˜€

There are obviously endless things you can do with powershell, with a syntax farrrrrrrrr more intuitive then BATCH ever was. That is really thanks to a lot of object operations relying on non other then regular expressions AKA RegEx.

RegEx is powerfull but also hard to grasp at times. Which funny enough has caused this blogger an unknown grief :P, though someone suggests not to use $ in between double quotes but for single quotes.. but it’s not “just that easy” as understanding the difference between using single quotes and double quotes is a big thing in PowerShell and understanding this as PowerShell as much as the RegEx underneith has it’s own special synatx on certain things and certain characters, so not only does PowerShell interpreter have it’s own parser with escape characters… So does RegEx so you have to know the info in the above link, as well as understand RexEx’s syntax, special characters, and escape characters specially when using operators like -like -match -contains or even -AllMatches…

Finding Files With Specific Characters

Outside of setting variables this really comes down to one line in the alternative bloggers script… line 8…

$item.Name |ย Select-Stringย -AllMatchesย $UnsupportedChars

Nice, so simple, but as mentioned this breaks if you attempt just specific special characters, such as $ or ^ as these are special characters to RegEx and need to otherwise be escaped to be taken literally.

Now the script in and of itself is simple enough to accomplish a simple task.. to explain in better detail what I mean… here’s my list of files, NTFS accepts more strange characters then I first though, so users can really be creative…

clearly much more complex, so if I run his script what happens….

Now I commented out the line that actually renames the files, so you know, I don’t have to rename them to reset my test. That’s good, but even though he added the money sign in his blog to the “$UnsupportedChars” var, he didn’t actually add it in the final script, and what happens if you do?

ok so in this case, the -AllMatches worked without issues I suppose due to changing to single quotes. but what if we need to change it’s character, let’s copy one of his ifs that changes it and see how it responds…

The results…

hahahha that’s a lot of money, clearly the RegEx got confused when we specified $ in the -Replace operations… so as the source RegEx escape characters says, escape it with \…. lets do that…

Awesome it worked… but I don’t like edited a script every time I need a slight change in my search criteria…. let’s take his script… and well completely redesign it as if it were on Steroids!!!ย  Functions! – to reuse reusable code with returns! Write-Hosts and Read-Hosts for user interaction and input, and heck why not options on results! Export, List, rename sounds good! This along with some dependency checking! (Does the path exist? Do you have rights to modify?)… oh yeah and confirmation of action! ๐Ÿ˜›

This beast will need it’s own GitHub Repo… so… here it is!

OK… now that I’m actually writing this blog actively while I’m polishing my script.. I noticed my export section had a Try, Catch for the creation of the CSV file. Simple but this, in itself, is not actually checking if I have permission… it’s simply checking to see if the I/O operation is actually able to succeed… it may fail due to other reasons (store space for example). So how do I check to see if I actually have permission before continuing? Well check your group membership (along with your own SID) and check the files/folders Access Control Lists (ACLs), obviously.

So how do I get a workaround based machines local user group membership?

Googling I can find easy how to do this for a domain joined machine, which is fantastic if I wanted to validate the path on a network share where the ACE’s and ACL’s are all managed by domain SIDs. But this test and script is all on my “workgroup” machine… and finally after enough searching the holy grail!

Wasn’t done directly by Ed Wilson but close enough, it gave the the code I needed to check the members of the local admin group, but what I really need is simply to validate read/write permissions.. So I found this… and sadly it doesn’t understand the fully distinguished user name when using whoami as the user variable… and it can’t sense inherited permissions… only once I granted my akadmin direct read and execute permissions would the code work…

Like ughhhhhhhhhhh………

I’m gonna skip this in my script for now… rather work on getting export to work properly, and actually getting the rename code in when saying yes. :O.. will update this post when that’s done.

… and yet in my drunken’ stuper I decide to keep trying…

I think the only way is only to iterate through the local groups and verify users.. nope leaving this again, unless a better way comes about.

OK.. so I felt I was on to something…

figured there’s got to be an easy way to do the same cmd as whoami /groups in PowerShell right… wtf is going on here?!?! I thought PowerShell was the easier scripting language… man this is crazy. and this

Not even brah… THIS! This is getting close!

This is gon’ be fun! (Great Source)

$folder = "c:\temp"
$CP = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
$UGM = $CP.Identities.Groups | ?{$_.AccountDomainSid -ne $null}
$UGM = $UGM + $CP.Identities.User
$TPP = (Get-Acl $folder).Access
foreach ($P in $TPP.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]))
{
foreach ($UP in $UGM){if($UP -eq $P){Write-Host "We got permissions here!`n";$TPP | ?{$_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -match $P}}}
}

Testing this out on my the account I added to a group worked just fine, but on my main machine it failed on me…

Poop, what happened… turned out my code filters the nitty gritty built in groups. By default all users are a part of these and aren’t generally used to define permissions (but can be, E.G Administrators). Since there was only one group my account was a member of the PS Object “$UGM” was not of a multi-value object aka an array object and doesn’t have an addition type method to complete the addition of the user SID, to the what should be array, as I have not defined it as such. So this is a good example to usually always define your variable types or fault to the problems they can inhibit. Also I was stunned I was not able to see the group SID when I called for all local groups, no matter how I called them, so doing the magic translate trick as the source above…

What the heck… None … didn’t even know such a group existed…

Code fixed by doing the following:

$folder = "c:\temp"

$CP = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
$UGM = @($CP.Identities.Groups | ?{$_.AccountDomainSid -ne $null})
$UGM = $UGM + $CP.Identities.User
$TPP = (Get-Acl $folder).Access 
foreach ($P in $TPP.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier])) 
{
 foreach ($UP in $UGM){if($UP -eq $P){Write-Host "We got permissions here!`n";$TPP | ?{$_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -match $P}}} 
}

After enough beers I got it to even let the user know if they are in the admins group but not elevated ๐Ÿ™‚

$folder = "c:\temp\None"

$CP = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
$UGM = @($CP.Identities.Groups | ?{$_.AccountDomainSid -ne $null})
$UGM = $UGM + $CP.Identities.User
if(([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")){$UGM = $UGM + ($CP.Identities.Groups | ?{$_.Value -eq "S-1-5-32-544"})}

$TPP = (Get-Acl $folder).Access

foreach ($P in $TPP.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]))
{
    foreach ($UP in $UGM){if($UP -eq $P){Write-Host "We got permissions here!";$TPP | ?{$_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -match $P}}}
}

foreach($pu in (Get-LocalGroup Administrators | Get-LocalGroupMember).SID){if($pu -eq $CP.Identities.User -And !([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")){Write-Host "You are an admin, but you are not elevated..."}}

Fixing the CSV Export

So far I have not go the permission checking working just yet. I have however managed to fix the export-csv issue.. old export…

Yeah that System.Object[] isn’t very helpful..

based on this old code:

$shit | Select-Object Name, Directory, MatchedValue| Export-Csv -Path $CSVFile -NoTypeInformation

I was trying all these weird things to try n resolve it (bad ideas)…

a lot of extra code for not good results, I figured there had to be a better way… and sure enough there was… Source

which left me with this code:

 $BadFiles | Select-Object Name, Directory, @{Expression={$_.MatchedValue -join ';'}} | Export-Csv -Path $CSVFile -NoTypeInformation

which now provides this output:

Nice! Until the moment I realized, can user use ; in file names… Ahhh they can! shoot! So now my test files grows larger, but my script help up with very lil modification! first to add it in the list of searchable characters:

function GetChars()
{
    Write-host "Characters to search for !$'&{^,}~#%][);( " -ForegroundColor Magenta -NoNewline
    $UnsupportedChars = Read-Host
    if ( !$UnsupportedChars ){ $UnsupportedChars = "\]!$'&{^,+}~#%[);(" }
    return $UnsupportedChars
}

That was it! the list output is fine:

but the export not so much as I was using ; to join those items…

K well users can use a \ so let’s see if we can use that in the export, again easy code change..

$BadFiles | Select-Object Name, Directory, @{Expression={$_.MatchedValue -join ';'}} | Export-Csv -Path $CSVFile -NoTypeInformation

Let’s test…

Nice, also added =, and let’s also test for @…

easy got em… until I realized you can name a file with a backtack `

and if I search for it on it’s own no problem…

but if I try to extend the default list of characters, I can’t seem to get it to work, even if I specify an escape \ in my list… Woah! I randomly got it!

function GetChars()
{
    Write-host "Characters to search for !$'&{^,}~#%]`[)@=;( " -ForegroundColor Magenta -NoNewline
    $UnsupportedChars = Read-Host
    if ( !$UnsupportedChars ){ $UnsupportedChars = "\]!$'&{^,+}~#%[)@=;(``" }
    return $UnsupportedChars
}

Double backticks worked, what a random thing to try and it actually worked…

That’s it for tonight! I got pretty much every special character covered. ๐Ÿ™‚

Next time I want to cover bulk confirmation with a report, since confirming every one can be annoying, but better to have confirmation then not!

PIssssss!!!!! Source of problem

Oi! I DID IT!!! YES!

From this…

        if(confirm $turd.name $turd.NameToChange)
        {
            Try { Rename-Item $turd.name -NewName ($turd.NameToChange) }
            Catch { Centeralize "Unable to change file's name"$turd.name "Red"}
        }
        else
        {
            Write-Host "Maybe Another time..."
        }

to this (after exporting to it’s own function though)…

        if(confirm $turd.name $turd.NameToChange)
        {
            
            $ThatOldName = $turd.FullName
            if (($ThatOldName).ToCharArray() -ccontains "[") {Write-host "We found a bad char";$ThatOldName -replace "\[", "`["}
            Try { Rename-Item -literalPath $ThatOldName -NewName ($turd.NameToChange) -ErrorAction Stop }
            Catch [System.UnauthorizedAccessException]
            {
                Centeralize "You do not have permission to change this file's name" "Red"
            }
            Catch 
            { 
                $ErMsg = "Unable to change file's name "+$turd.name
                Centeralize $ErMsg "Red"
                $ErrMsg = $_.Exception.Message
                Centeralize $ErrMsg "Red"
            }
        }
        else
        {
            Write-Host "Maybe Another time..."
        }
    }

I can’t believe it worked! wooo results from this:

To this!:

I can’t do much about files that already exists, besides people mentioing way to over-write existing files which I’m not coding for, nope sorry.

but so far so good. This script is awesome!

Today I discovered that Try-Catch only works on terminating exceptions…

Powershell ErrorAction

Sigh… This means I have to do additional coding if I want the script to get all items from main folder, even if there’s a permission restriction on a child folder.

Life’s rough!

Working on PowerShell scripts (ISE) w/ GitHub

GitHub

So as you all probably know GitHub has been acquired by Microsoft. I had initially groaned at this acquisition as usually a lot of things Microsoft has done lately has really bothered me (locking down APIs to O365 and not providing them to on Prem, for example) but then they also have done some good moves… .Net Core 2.0 and all the open source incentives are a nice change of pace.

And to top that with some sugar how about some private Repositories for free members! Yeah That’s right, now that this is an option I’m going to use GitHub more. Now I’ve played with it before, however this time I wanted to write this up for my own memories. Hopefully it helps someone out there too.

Let’s have some fun saving our PowerShell scripts on GitHub!

PowerShell ISE and GIT

Dependencies

So for this demo you’ll need:

1) A GitHub Account (Free)
2) PowerShell ISE (Free with Windows)
3) Git for Windows

First, install and configure Git for Windows. Mike previously covered this topic in another blog article. In this scenario, I ran the Git installer elevated so I could install it in the program files folder and I took the option to add the path for Git to the system environment variable path:

posh-git0a

Make sure that youโ€™ve configured Git as the user who is running PowerShell (I ran these commands from within my elevated PowerShell session):

4) Install the Posh-Git PowerShell module from the PowerShell Gallery:

The Fun Stuff

So I originally follow this guys blog post on how to accomplish this.

Now I had already installed git for windows so I was set there.

SharePoint Profiles

I liked the part where he had altered his console display depending on where he was located to not ensue confusion, however I wasn’t exactly sure what he meant by Profiles a lil searching and education session later I was able to verify my profile path:

$profile

Then simply edit that Microsoft.PowerShell_profile.ps1 with Mikes script:

Set-Location -Path $env:SystemDrive\
Clear-Host
$Error.Clear()
Import-Module -Name posh-git -ErrorAction SilentlyContinue
if (-not($Error[0])) {
    $DefaultTitle = $Host.UI.RawUI.WindowTitle
    $GitPromptSettings.BeforeText = '('
    $GitPromptSettings.BeforeForegroundColor = [ConsoleColor]::Cyan
    $GitPromptSettings.AfterText = ')'
    $GitPromptSettings.AfterForegroundColor = [ConsoleColor]::Cyan
    function prompt {
        if (-not(Get-GitDirectory)) {
            $Host.UI.RawUI.WindowTitle = $DefaultTitle
            "PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) "
        }
        else {
            $realLASTEXITCODE = $LASTEXITCODE
            Write-Host 'PS ' -ForegroundColor Green -NoNewline
            Write-Host "$($executionContext.SessionState.Path.CurrentLocation) " -ForegroundColor Yellow -NoNewline
            Write-VcsStatus
            $LASTEXITCODE = $realLASTEXITCODE
            return "`n$('$' * ($nestedPromptLevel + 1)) "
        }
    }
}
else {
    Write-Warning -Message 'Unable to load the Posh-Git PowerShell Module'
}

Now that we’ll have the same special console to avoid confusion let’s link a directory!

Linking GitHub Repo to Your local Directory

Then I cloned my new private Repo:

git clone https://github.com/Zewwy/Remove-SPFeature Remove-SPFeature -q

That felt awesome…

Nice, nice…

Opening scripts from the ISE

Alright. Well now that we have a repo, and are in it, how do I open a file in the very ISE we are running to edit them? Now Mike didn’t exactly cover this, cause I suppose to him this was already common knowledge… well not to me haha so it’s actually pretty simple once you know how.

psEdit .\Remove-SPFeature.ps1

Woah! Epic, it can be bothersome when dealing with length scripts, so ensure you utilize regions (w/ endregions) to allow for quick named areas to access, as you can use this command in ISE to collapse all regions once a script is loaded.

$psISE.CurrentFile.Editor.ToggleOutliningExpansion()

lets start making some changes *changes made*

Committing and Pushing

Get your mind out of the gutter!

Now I had originally did a git push, and instantly got everything is up-to-date alert, so awesome, I did not have to fight through his whole schpeal about auth (I got a prompt the very first time I attempted to clone my repo, requesting me to login into my GitHub Account). So my tokens were good right from the start after that happened. However I did make some updates to one file and was now instead presented with this after a commit:

Again a bit of searching I was able to find the answer, seem usually to be some form of ignorance, that is why I’m doing this.. to learn ๐Ÿ˜›

Now again I got a bit confused at how this worked and when I did some searching I discovered:

Don’t do a “git commit -a” from the ISE, it’ll crash asking you for a line to provide in for the description.

Do proper staged commits as described here. ๐Ÿ™‚

I hope this maybe gets some more people power shelling!

Next I should learn to use Visual Studio for more app building… but I’m more of a sys admin then a dev…

I recently took a course in resiliency, and they basically said be a tree… ok.

Branching

Whats is branching? well pretty much, try stuff without changing the source code. Backups anyone? it’s a nice way to try stuff without breaking the original code, and once tested, can be merged.

unlike a tree it’s not often a branch just becomes the trunk, but whatever…

Following this guide:

To create a branch locally

You can create a branch locally as long as you have a cloned version of the repo.

From your terminal window, list the branches on your repository.

$ git branch 
* master

This output indicates there is a single branch, the master and the asterisk indicates it is currently active.

Create a new feature branch in the repository

$ git branch <feature_branch>

Switch to the feature branch to work on it.

$ git checkout <feature_branch>

You can list the branches again with the git branch command.

Commit the change to the feature branch:

$ git add . 
$ git commit -m "adding a change from the feature branch"

Results:

Hopefully tomorrow I can cover merging. ๐Ÿ™‚

Cheers for now!