Find Forwarding Rules

Find Forwarding Rules

Not too long ago, I needed to do some rule auditing for forwarders in a client’s exchange online. They believed someone had a rule in their exchange account that was forwarded to a spammer. They believed this because new employees were receiving emails within a few days of creation. So, it’s time for some PowerShell magic to save the day. It’s time to Find Forwarding Rules in your mailboxes with PowerShell.

The Script

Connect-ExchangeOnline
$Mailboxes = Get-Mailbox -ResultSize Unlimited
$ForwarderRules = foreach ($Mailbox in $Mailboxes) {
    $rules = Get-InboxRule -mailbox $Mailbox.Alias
    foreach ($rule in $rules) {
        if (($null -ne $rule.ForwardTo) -or ($null -ne $rule.ForwardAsAttachmentTo)) {
            [pscustomobject][ordered]@{
                Username = $Mailbox.Alias
                Rule = $Rule.name
                ID = $Rule.RuleIdentity
                Enabled = $rule.enabled
                ForwardTo = $rule.ForwardTo | where-object {$_ -like "*@*"}
                ForwardAsAttachmentTo = $rule.ForwardAsAttachmentTo | where-object {$_ -like "*@*"}
            }
        }
    }
}
$ats = $ForwarderRules | where-object {($null -ne $_.ForwardTo) -or ($null -ne $_.ForwardAsAttachmentTo)}
$ats

The Breakdown

The script today requires the Exchange Online Module to be installed. If you don’t have it, go install it. Once you have it, you will need to connect using the Connect-ExchangeOnline commandlet.

Connect-ExchangeOnline

By doing it this way, MFA will be triggered and we want MFA to be at this level. Security first yall. This brings me to my next point, soon exo 3 will come out and security will be improved greatly.

Once you are connected, we need now to pull all the mailboxes from the system. This command can take some time if you have a large company. In fact, this script with only 300 users took around an hour. The Larger your company is, the longer it will take. Plan ahead accordingly.

$Mailboxes = Get-Mailbox -ResultSize Unlimited

Now we have all the mailboxes, we need to go through each mailbox and get the inbox rules for that mailbox. We start a for each loop of the mailboxes.

$ForwarderRules = foreach ($Mailbox in $Mailboxes) { 

}

Next, we will need to grab the inbox rules for that mailbox. We do this with the Get-InboxRule commandlet and we feed it the mailbox alias.

$ForwarderRules = foreach ($Mailbox in $Mailboxes) { 
    $rules = Get-InboxRule -mailbox $Mailbox.Alias
}

Normally a mailbox has more than one rule. Thus, we need to make another for each loop for the rules inside our main foreach loop.

$ForwarderRules = foreach ($Mailbox in $Mailboxes) { 
    $rules = Get-InboxRule -mailbox $Mailbox.Alias
    foreach ($rule in $rules) {
    
    }
}

Afterward, we need to pull the data out of the rules and make it useful. The amount of output is large, breaking it down and making it useful is important. That’s the whole goal of this. We want to find out who has forwarders and we want to know if those forwarders are forwarding out to someone else. I want to break it up as well so I can look at all the forwarders and just the ones with email addresses.

Gathering Information

Firstly, we need to ask the question, Are we forwarding to someone as an email or an attachment? The properties we want to look at are, forwardto and forwardasattachmentto. If either of these are not null, then we want to look at that information. This allows us to Find Forwarding Rules.

$ForwarderRules = foreach ($Mailbox in $Mailboxes) { 
    $rules = Get-InboxRule -mailbox $Mailbox.Alias
    foreach ($rule in $rules) {
        if (($null -ne $rule.ForwardTo) -or ($null -ne $rule.ForwardAsAttachmentTo)) {
        
        }
    }
}

Now we are looking at a rule object that has a forwarder of some sort. It’s time to let the end user know. Next, we will create a PowerShell Custom Object. Almost every get command I have come across has produced one of these objects.

$ForwarderRules = foreach ($Mailbox in $Mailboxes) { 
    $rules = Get-InboxRule -mailbox $Mailbox.Alias
    foreach ($rule in $rules) {
        if (($null -ne $rule.ForwardTo) -or ($null -ne $rule.ForwardAsAttachmentTo)) {
            [pscustomobject][ordered]@{

            }
        }
    }
}

The object is ready for us. It’s time to fill it in with useful information. We need the mailbox name, the rule name, the rule’s id, if it’s enabled, and finally the forwarder information. The forwarder information is broken up into two. The “ForwardTo” and the “ForwardAsAttachmentTo”. The first forwards the email to a person. The second wraps up the email into an attachment and sends it to the person. We need to see both.

These items are arrays of email addresses and references. If the forwarder points to an external email address it will contain the @ symbol like most email addresses do. If the forwarder points to an internal address like bob in accounting, then it will not have an @ symbol unless told otherwise. This is useful. We can use a where object to pull out the lines with an @ symbol.

$ForwarderRules = foreach ($Mailbox in $Mailboxes) {
    $rules = Get-InboxRule -mailbox $Mailbox.Alias
    foreach ($rule in $rules) {
        if (($null -ne $rule.ForwardTo) -or ($null -ne $rule.ForwardAsAttachmentTo)) {
            [pscustomobject][ordered]@{
                Username = $Mailbox.Alias
                Rule = $Rule.name
                ID = $Rule.RuleIdentity
                Enabled = $rule.enabled
                ForwardTo = $rule.ForwardTo | where-object {$_ -like "*@*"}
                ForwardAsAttachmentTo = $rule.ForwardAsAttachmentTo | where-object {$_ -like "*@*"}
            }
        }
    }
}

Sorting the Sorted Information

Now it’s time to sort the sorted information. First why? Why not add it to the loop above? Two reasons. First is the time it takes to process. Second, I want to run $ForwarderRules to get information and I want to run the next line of code to see the more focused information. I like having options. Now we will take the forwarder rules we created and filter out the nulls of the forwarders. Finally, we want to display the information.

$ats = $ForwarderRules | where-object {($null -ne $_.ForwardTo) -or ($null -ne $_.ForwardAsAttachmentTo)}
$ats

Finally, you have all the email addresses and rules that have a forwarder that forwards to a real email address. You can run through each one and audit them for security.

Future Reading

Images created with Mid Journey AI

Get-Credentials to Plain Text

Get-Credentials to Plain Text

Sometimes I like to go back and see where I can improve my scripts. A few weeks ago I showed you the Unifi Controller script. Where it pulls the client information from each site. Well, I wanted to improve upon this script. A problem I see with this script is the plain text password. The unifi controller I have to test only accepts plain text, but I don’t want to have a password inside my PowerShell history. Today I want to show you how to convert Get-Credentials to Plain Text.

Get-Credentials as a Parameter

We first need to remove the Username and Password from the parameters and then create a secure credential parameter. By default, we want this to be a mandatory parameter. The type of parameter we will be using is a System Management Automation PScredential. This is what the parameter will look like.

[parameter( 
            HelpMessage = "Unifi Username and Password", 
            Mandatory = $true)][alias('UserNamePassword')][System.Management.Automation.PSCredential]$Credential

Convert Get-Credentials to Plain Text

Now it’s time to Covert the PSCredentials back to plain text. Inside our PSCredentials lives the username and password we gave the system. Thus we pull the secure string password from the PSCredentials.

$SecurePassword = $Credential.Password

Next, we are going to use the Marshal Class. This class is designed to bridge the gap between managed and unmanaged programming models. The method we will be using is the Secure String to BSTR. This method Allocates an unmanaged binary string and copies the content of a managed secure string object into it. Basically, we find where it lives in memory and combine it. For more information.

 $CodedPassword = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurePassword)

Aftward, we Take this coded data and push it into the following method. The PtrToStringAuto. This method allocates a managed string and copies all the characters up to the first null. This copy is from the string stored in unmanaged memory. This is why we need the Coded Password to tell us where the password lives in memory. Basically, I’m taking the coded password and making it human-readable.

$Password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($CodedPassword)

The Script Rebuilt

Now we have a way to decrpt the get credential command, it’s time to dive back into the script. Here is the rebuilt script.

function Get-UnifiSitesClients {
        param (
            [parameter(
                ValueFromPipeline = $True,
                ValueFromPipelineByPropertyName = $True,    
                HelpMessage = "Unifi Username and Password", 
                Mandatory = $true)][alias('IP')][ipaddress]$IPaddress,
            [parameter( 
                HelpMessage = "Unifi Username and Password", 
                Mandatory = $true)][alias('UserNamePassword')][System.Management.Automation.PSCredential]$Credential,
            [parameter(
                HelpMessage = "Port Number", 
                Mandatory = $true)][alias('Port')][init]$portNumber
        )
        $SecurePassword = $Credential.Password
        $CodedPassword = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurePassword)
        $Password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($CodedPassword)
        $Username = $Credential.UserName

        $uri = "https://$($IPaddress):$portNumber/api/login"
        $headers = @{'Accept' = 'application/json' }
        $params = @{
            'username' = $username;
            'password' = $Password;
        }
        $body = $params | ConvertTo-Json
     
        $response = Invoke-RestMethod -Uri $uri `
            -Body $body `
            -Method Post `
            -ContentType 'application/json' `
            -Headers $headers `
            -SkipCertificateCheck `
            -SessionVariable s
        $uri = "https://$($IPaddress):$portNumber/api/self/sites"
        $sites = Invoke-RestMethod -Uri $uri `
            -Method Get `
            -ContentType 'application/json' `
            -Headers $headers `
            -SkipCertificateCheck `
            -Websession $s
        $Return = Foreach ($Site in $sites.data) {
            $Uri = "https://$($IPaddress):$portNumber/api/s/$($Site.name)/stat/sta"
            $Clients = (Invoke-RestMethod -Uri $Uri `
                -Method Get `
                -ContentType 'application/json' `
                -Headers $headers `
                -SkipCertificateCheck `
                -Websession $s).data
            Foreach ($Client in $Clients) {
                [pscustomobject][ordered]@{
                    Site = $Site.name
                    SiteDescritption = $Site.desc
                    OUI = $client.OUI
                    MacAddress = $client.mac 
                    IPAddress = $Client.IP
                    SwitchMac = $client.sw_mac
                    SwitchPort = $client.sw_port
                    WireRate = $client.wired_rate_mbps
                }
            }
        }
        $return
    }

Image created with mid-journey

LAPS Password With PowerShell

LAPS Password With PowerShell

A few of my clients use something called LAPS. Laps change the local administrator password on a computer and then store the information inside Active Directory. Since I don’t dive deep into this client’s computers often, I needed something to quickly type the first letter of the computer in question to pull up the LAPS password. Basically, I needed a list of computer names from the command itself. This is fully possible with Dynamic Parameters. So, today we will be grabbing the LAPS password with PowerShell.

Where Does LAPS Password live?

Most companies that set up LAPS do so with Active Directory. By default, Active Directory saves the password into an attribute called “ms-Mcs-AdmPwd” and LAPS also stores the expiration date in “ms-Mcs-AdmPwdExpirationTime” Thus, all you have to do is call a get-adcomputer command and pull out the information.

Get-Adcomputer -filter {name -like $Computer} -properties name,ms-Mcs-AdmPwd,ms-Mcs-AdmPwdExpirationTime | select-object name,ms-Mcs-AdmPwd,ms-Mcs-AdmPwdExpirationTime 

Now the “ms-Mcs-AdmPwdExpirationTime” is unique and needs to be parsed into something more readable. We can use the m method called [datetime] to do this.

Get-Adcomputer -filter {name -like $Computer} -properties name,ms-Mcs-AdmPwd,ms-Mcs-AdmPwdExpirationTime | select-object Name, @{l = "AdminPassword"; e = { $_."ms-Mcs-AdmPwd" } }, @{l = "AdminPasswordExpireTime"; e = { [datetime]::FromFileTime(($_."ms-Mcs-AdmPwdExpirationTime")) } }

There you have it, That’s how you get the LAPS password, But I want to take this one step further. I don’t know all the computer names. I want that information at my fingertips while I type out the command. So, I want to type something like Get-LAPS -ComputerName <here populate a name where I can tab> and bamn, it gives it to me when I hit enter. That’s where we will dive into dynamic parameters next.

Adding Dynamic Computer Name Parameters

In a previous article, we went over how to make a dynamic parameter. I want to help refresh memories by doing a single parameter and show you how it can be done with this function.

The first thing we need to do is create our form. This form allows us to use the dynamic parameters and gives us spots to pull data. This is more of a road map than anything else, but it is required for dynamics.

function Get-LapsPassword {
    [cmdletbinding()]
    Param()
    DynamicParam {}
    Begin {}
    Process {}
    End {}
}

The first part of our Dynamics is we want to name the parameter template. From there, we want to create a new object. This object will be the system collections object model for collections. AKA system attributes. Then we want to make an attribute object to add to that object later.

Building Out Objects

$paramTemplate = 'ComputerName' 
$AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute

The ParameterAttribute will be where we add the flags like mandatory and position. We add those by dropping them directly into the ParamterAttribute object. A fun little fact, you can tab through and see what other items are available for this object. Things like the help message, the value from the pipeline, and more are hidden here. Today we only care about the mandatory and position.

$ParameterAttribute.Mandatory = $true
$ParameterAttribute.Position = 1

After we build out our parameter Attribute object, we need to add it to the Attribute Collection we made at the start. We do this by using the “.add()” function of objects.

$AttributeCollection.Add($ParameterAttribute)

Now we need to create another object. This will be the Runtime Parameter Directory. Basically, what they will be looking through. This is a system management automation object called runtime defined parameter directory. Say that 10 times fast…

More Objects

$RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary

Now we need to make our Validate Set. We will create an array of devices using the Get-adcomputer command. Here we will push (Get-adcomputer -filter {enabled -eq “true”}).name into a variable. Now we will have a list of active computers. Notice that we filter out all other information by using the “.name” call.

$ParameterValidateSet = (Get-ADComputer -Filter { enabled -eq "true" -and OperatingSystem -Like '*Windows*' -and OperatingSystem -notlike "*Server*" }).name

Next, we need to create another object. This object is the system management automation validate set attribute object. We can feed this object our Parameter Validate Set.

$ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($ParameterValidateSet)

Afterward, it’s time to feed the Validate Set attribute to the attribute collection from the beginning. We can accomplish this by using the “.add()” method.

$AttributeCollection.Add($ValidateSetAttribute)

Next, it’s time to bring our Attribute collection into the command line. It’s time to make the run-time parameter. Once again, a new object. This time it’s the Run time Defined Parameter object. Like the last object, we can place our data directly into it. We will want the parameter’s name, the type, a string in this case, and the validate set.

$RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($paramTemplate, [string], $AttributeCollection)

Afterward, we take the above parameter and place it into our directory with the “.add()” method. We need the parameter Template and the Run time Parameter.

$RuntimeParameterDictionary.Add($paramTemplate, $RuntimeParameter) 

Finally, in the dynamic parameter block, we return our directory.

return $RuntimeParameterDictionary

Beginning

We are almost done. It’s time to bring the dynamic parameter into the function and make it useable. We do this in the beginning section. We shove the PSBoundParameters of our template name into a variable.

$MemberName = $PSBoundParameters[$paramTemplate]

Then from there, we call the $memberName in our Get-adcomputer command.

The Script

It’s that time, it’s time to put it all together, so you can copy and past it into your toolbox. It’s time To Grab LAPS Password With PowerShell.

function Get-LapsPassword {
    [cmdletbinding()]
    Param()
    DynamicParam {
        # Need dynamic parameters for Template, Storage, Project Type
        # Set the dynamic parameters' name
        $paramTemplate = 'ComputerName' 
        # Create the collection of attributes
        $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        # Create and set the parameters' attributes
        $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
        $ParameterAttribute.Mandatory = $true
        $ParameterAttribute.Position = 1
        # Add the attributes to the attributes collection
        $AttributeCollection.Add($ParameterAttribute)
        # Create the dictionary 
        $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        # Generate and set the ValidateSet
        $ParameterValidateSet = (Get-ADComputer -Filter { enabled -eq "true" }).name
        $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($ParameterValidateSet)
        # Add the ValidateSet to the attributes collection
        $AttributeCollection.Add($ValidateSetAttribute) 
        # Create and return the dynamic parameter
        $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($paramTemplate, [string], $AttributeCollection)
        $RuntimeParameterDictionary.Add($paramTemplate, $RuntimeParameter) 
        return $RuntimeParameterDictionary
    } # end DynamicParam
    BEGIN {
        $MemberName = $PSBoundParameters[$paramTemplate]
    } # end BEGIN
    Process {
        $ComputerInfo = Get-ADComputer -Filter { name -like $MemberName } -Properties * 
    }
    End {
        $ComputerInfo | select-object Name, @{l = "AdminPassword"; e = { $_."ms-Mcs-AdmPwd" } }, @{l = "AdminPasswordExpireTime"; e = { [datetime]::FromFileTime(($_."ms-Mcs-AdmPwdExpirationTime")) } }
    }
}

Additional Reading

Get Client Information From your Unifi Controller

Get Client Information From your Unifi Controller

Today I would like to go over how to get client information from your Unifi Controller. We will first connect to our Unifi Controller with Powershell using the Unifi API. Then from there we will pull each site and create a list of different sites. Finally, we will use the site information to pull the client information from the controller.

Powershell and API the Unifi Controller

The first step is to create a connection with our PowerShell. We will need a few items:

  • User Account that Has at least read access
  • The IP address of our controller
  • The API port.
  • An Unifi Controller that is configured correctly. (Most are now)

Ask your system administrator to create an account with read access. Check your configuration for the IP addresses and the Port numbers. By default, the API port is 8443. Now we have those pieces of information, its time to discuss what is needed.

Connecting to the API

To connect to the Unifi controller API, you need a Cookie. This is not like how we connected to the Graph API in our previous blog. To get this cookie, we will need to execute a post. The API URL is the IP address followed by the port number, API and login. Very standard.

$uri = "https://$($IPAddress):8443/api/login"

We will need to create a header that accepts jsons. To do this we simply say accept equal application/json.

$headers = @{'Accept' = 'application/json' }

Now we will create our Parameters for the body. Since this is going to be a Post command, we have to have a body. This is where we will place our Username and Password. We are going to build out a Hashtable and then convert that into a json since the API is expecting a json. We will use the convertto-json command to accomplish this step.

$params = @{
        'username' = $username;
        'password' = $Password;
}
$body = $params | ConvertTo-Json

Now we have our Body built out, it’s time to create a session. We will use the invoke-restmethod command to do this. We feed the URI we made a few steps back. The body will be the body we just built out. The content type will be ‘application/json’. Our headers will be the header we made a few steps back. We will skip the certificate check unless we are using a cert. Most of the time we are not. Finally, the secret sauce. We want a session. Thus, we use the Session Variable and give it a session name. Let’s go with S for the session.

$response = Invoke-RestMethod -Uri $uri `
        -Body $body `
        -Method Post `
        -ContentType 'application/json' `
        -Headers $headers `
        -SkipCertificateCheck `
        -SessionVariable s

Grabbing Sites

Now we have our Session cookie, its’ time to grab some information. We will use the same headers but this time we will use the Session Name, S. The URL for sites is /api/Self/Sites.

$uri = "https://$($IPaddress):8443/api/self/sites"

This URL will get the sites from your Unifi Controller. Once again we will use the Invoke-RestMethod to pull the data down. This time we are using a get method. The same Content type and headers will be used. Along with the skip certificate check. The Session Variable is going to be the Web Session and we will use the $S this time.

$sites = Invoke-RestMethod -Uri $uri `
        -Method Get `
        -ContentType 'application/json' `
        -Headers $headers `
        -SkipCertificateCheck `
        -Websession $s

The Sites variable will contain two items. a meta and a data variable. Everything we will need is inside the Data variable. To access it we can use the $sites.data to see all the data.

Here is an example of what the data will look like for the sites.

_id          : 5a604378614b1b0c6c3ef9a0
desc         : A descriptoin
name         : Ziipk5tAk
anonymous_id : 25aa6d32-a165-c1d5-73a0-ace01b433c14
role         : admin

Get Client Information from your Unifi Controller

Now that we have the sites, we can go one at a time and pull all the clients from that site. We will do this using a Foreach loop. There is something I want to point out first. Remember, the data that is returned is two variables, two arrays. The first one is Meta. Meta is the metadata of the session itself. It’s something we don’t need. We do need the Data. For our foreach loop, we will pull directly from the data array. We want to push everything in this loop into a variable. This way we can parse out useful data, sometime to much data is pointless.

$Clients = Foreach ($Site in $Sites.Data) {
    #Do something
}

Without using the $Sites.Data we will have to pull the data from the command itself. This can cause issues later if you want to do more complex things.

The URL we will be using is the /api/s/{site name}/stat/sta. We will be replacing the Site name with our $site.name variable and pushing all that into another Uri.

$Uri = "https://$($IPaddress):8443/api/s/$($Site.name)/stat/sta"

Then we will execute another invoke-restmethod. Same as before, we use the same headers, content type, and web session. The only difference is we wrap up the command inside preferences. This way we can pull the data directly while the command executes. Less code that way.

(Invoke-RestMethod -Uri $Uri `
            -Method Get `
            -ContentType 'application/json' `
            -Headers $headers `
            -SkipCertificateCheck `
            -Websession $s).data

Each time the command runs, it will release the data that we need and that will all drop into the $Clients variable. From here, we pull the information we want. The information that the client’s produce includes, the match addresses, times, IP addresses, possible names, IDs and more. So, it’s up to you at this point to pick and choose what you want.

The Script

The final script is different because I wanted to add site names and data to each output. But here is how I built it out. I hope you enjoy it.

function Get-UnifiSitesClients {
    param (
        [string]$IPaddress,
        [string]$username,
        [string]$portNumber,
        [string]$Password
    )
    
    $uri = "https://$($IPaddress):$portNumber/api/login"
    $headers = @{'Accept' = 'application/json' }
    $params = @{
        'username' = $username;
        'password' = $Password;
    }
    $body = $params | ConvertTo-Json

    $response = Invoke-RestMethod -Uri $uri `
        -Body $body `
        -Method Post `
        -ContentType 'application/json' `
        -Headers $headers `
        -SkipCertificateCheck `
        -SessionVariable s
    $uri = "https://$($IPaddress):$portNumber/api/self/sites"
    $sites = Invoke-RestMethod -Uri $uri `
        -Method Get `
        -ContentType 'application/json' `
        -Headers $headers `
        -SkipCertificateCheck `
        -Websession $s
    $Return = Foreach ($Site in $sites.data) {
        $Uri = "https://$($IPaddress):$portNumber/api/s/$($Site.name)/stat/sta"
        $Clients = (Invoke-RestMethod -Uri $Uri `
            -Method Get `
            -ContentType 'application/json' `
            -Headers $headers `
            -SkipCertificateCheck `
            -Websession $s).data
        Foreach ($Client in $Clients) {
            [pscustomobject][ordered]@{
                Site = $Site.name
                SiteDescritption = $Site.desc
                OUI = $client.OUI
                MacAddress = $client.mac 
                IPAddress = $Client.IP
                SwitchMac = $client.sw_mac
                SwitchPort = $client.sw_port
                WireRate = $client.wired_rate_mbps
            }
        }
    }
    $return 
}

Additional Resource

URLs With PowerShell

URLs With PowerShell

Regex… Even the PowerShell Masters struggle with it from time to time. I helped a friend of mine with some URL chaos. The URL he had was a software download and the token was inside the URL. Yeah, it was weird but totally worth it. There were many different ways to handle this one. But the Matches was the best way to go. What was interesting about this interaction was I could later in another script. Unlike my other posts, this one’s format is going to be a little different. Just following along.

The URL (Example)

"https://.download.software.net/windows/64/Company_Software_TKN_0w6xBqqzwvw3PWkY87Tg301LTa2zRuPo09iBxamALBfs512rSgomfRROaohiwgJx9YH7bl9k72YwJ_riGzzD3wEFfXQ7jFZyi5USZfLtje2H68w/MSI/installer"

Here is the string example we are working with. Inside the software installer, we have the name of the software, “Company_Software_” and the token, “0w6xBqqzwvw3PWkY87Tg301LTa2zRuPo09iBxamALBfs512rSgomfRROaohiwgJx9YH7bl9k72YwJ_riGzzD3wEFfXQ7jFZyi5USZfLtje2H68w” The question is how do you extract the token from this URL? Regex’s Matches we summon you!

Matches is one of the regex’s powerhouse tools. It’s a simple method that allows us to search a string for a match of our Regex. This will allow us to pull the token from URLs with PowerShell. First, it’s best to link to useful documentation. Here is the official Microsoft documentation. Sometimes it’s helpful. Another very useful tool can be found here.

PowerShell’s Regex can be expressed in many different ways. The clearest and most concise way is to use the -match flag.

$String -match $Regex

This of course produces a true or false statement. Then if you call the $matches variable, you will see all of the matches. Another way of doing this is by using the type method.

[regex]::Matches($String, $Regex)

This method is the same as above, but sometimes it makes more sense when dealing with complex items. The types method is the closest to the “.net” framework.

The Regex

Next, let’s take a look at the Regex itself. We are wanting to pull everything between TKN_ and the next /. This was a fun one.

'_TKN_([^/]+)'

The first part is the token. We want our search to start at _TKN_. This clears out all of the following information automatically: https://.download.software.net/windows/64/Company_Software. A next part is a group. Notice the (). This creates a group for us to work with. Inside this group, we are searching for all the characters inside []. We are looking for Everything starting at where we are, the TKN_ to a matching /. We want all the information so we place a greedy little +. This selects our token. This regex produces two matches. One with the word _TKN_ and one without. Thus we will want to capture the second output, aka index of 1.

$String -match '_TKN_([^/]+)'
$Matches[1]

Another way to go about this is the method-type model.

$Token = [regex]::Matches($String, '_TKN_([^/]+)') | ForEach-Object { $_.Groups[1].Value }

It same concept, instead this time we are able to parse the output and grab from group one.

Replace Method

Another way to go about this is by using replace. This method is much easier to read for those without experience in regex. I always suggest making your scripts readable to yourself in 6 months. Breaking up the string using replace makes sense in this light. The first part of the string we want to pull out is everything before the _TKN_ and the TKN itself. The second part is everything after the /. Thus, we will be using two replaces for this process.

$String -replace(".*TKN_",'')

Here we are removing everything before and the TKN_ itself. The .* ensures this. Then we wrap this code up and run another replace.

$Token = ($String -replace(".*TKN_",'')) -replace('/.*','')

Now we have our token. This method is easier to follow.

Conclusion

In Conclusion, parsing URLs With PowerShell can be tough, but once you get a hang of it, life gets easier. Use the tools given to help understand what’s going on.

Additional Regex Posts

Image created with midjourney AI.

Comments in Group Policy

Comments in Group Policy

Documentation is a big deal in the world of IT. There are different levels of documentation. I want to go over in-place documentation for group policy. Comments in Group Policy are in-place documentation.

How to comment on a Group Policy

This process is not straightforward by any stretch of the imagination. The first and foremost way to add comments to a Group Policy is to use the GUI.

  1. Open Group Policy Management Console
  2. Select the policy you wish to comment
  3. Right-click the policy in question and click edit
  4. Inside the group policy management editor, right-click the policy name and click properties
  5. Click the comment tab
  6. Now enter your comment.
  7. click Apply and Ok

The second way to add a comment in group policy is by using PowerShell. The Description of a policy is where the comment lives. Thus using the command Get-GPO will produce the comment. We will dig more into that later.

Get-GPO -name "Control Panel Access"

Using the Get-Member command we can pipe our Get-GPO command and see what is doable. You will be treated to a list of member types and what they are capable of. The description has a get and a set method to it. This means, you can set the description, aka comment.

(Get-GPO -name "Control Panel Access").Description = "This is my comment"

Suggestions

Here are a few suggestions for documenting the policy like this.

  1. Use the(Get-date).ToString(“yyyy-MM-dd_hh_mm_ss”) at the beginning to setup your date time.
  2. Then, I would add the author of the policy/comment
  3. A quick description of the policy
  4. Whether it’s a user or computer policy.
  5. Any WMI filters.

More information here helps the next person or even yourself months down the road. Don’t go overboard as it can cause issues later. Using the ‘`n’ will create a new line which can be helpful as well.

Pulling Comments with PowerShell

Now that we have all the policies documented, we can pull the information from the in-place documentation. We do this by using the GPO-Get -All command. One way to do this is by using the select-object command and passing everything into a csv. I personally don’t like that, but it works.

Get-GPO -All | select-object DisplayName,Description | export-csv c:\temp\GPO.csv

I personally like going through the GPO’s and creating a txt file with the comment and the file name being the display name.

Get-GPO -All | foreach-object {$_.Description > "c:\temp\$($_.Displayname).txt"}

Conclusion

I would like to go deeper into In-Place Documentation as it is very useful down the road. Powershell uses the #, other programs use different methods as well. If you get a chance to place in place documentation, life becomes easier when you are building out the primary documentation as you have a point of reference to look back at.

Future Reading