Message in The Ballon

Message in The Ballon

Reading Time: 2 minutes

Do you need something to let your end-user know what’s going on? Don’t want your end-user to see the program running? Well, PowerShell has the ability to use the system windows notify system. AKA, the message box that pops up on the right of the screen.

The first step to make this happen is we need to add the assembly, system.windows.forms. We do this by the amazing command add-type.

add-Type -AssemblyName System.Windows.Forms

Now we have the assemblies loaded into ram, we need to create an object from those assemblies. This way we can command the object later. We do this using the New-object cmdlet. We want the notification tray icons. So we want the system.windows.forms.notifyicon.

$global:balloon = New-Object System.Windows.Forms.NotifyIcon

You know that notifications have icons. Yep, It’s from the application itself. So, if you have this function in another application, it will show that the application’s icon. We call upon the System.Drawing.Icon to do this. But First, we need to know which app we are currently using. We do this with Get-Process with the ID and the current Pid.

$path = (Get-Process -id $pid).Path
$balloon.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon($path) 

What’s cool about this is we are extracting the icon of the application itself with that extract associated icon. We push it into our notify icon object. Next, we want to add the Tip Icon. That’s the ! ? so on and so forth. We do this by using the system.windows.forms.tooltipicon function. Believe it or not, we can use a validate set in the parameters to get the exact naming input as well. The property of the object is BalloonTipIcon.

$balloon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::$Messagetype

Now the message itself. At this point we have an icon, the notification icon and that’s it. This is the meat of the notification. The propriety in the balloon object is, BalloonTipText.

 $balloon.BalloonTipText = $Message

Just a quick note, to much text is ugly.

Next is the title, I like to use just the word attention as it brings the users eyes to it. The balloon’s propriety for this is BalloonTipTitle.

$balloon.BalloonTipTitle = "Attention"

Finally, we want to make this thing visible. An invisible tooltip is as useful as invisible ink without a black light. As you have guessed by now, Visible is the balloon’s propriety.

$balloon.Visible = $true 

Now the drum roll, please! It’s time to show our masterpiece that will be enjoyed by the end-user for a split second. The ShowBalloonTip(). Yes, let us do it! This is a method and will show the balloon tip for x time. This is set up in milliseconds. So, every 1000 milliseconds makes a second.

$balloon.ShowBalloonTip($Time)

Lets put it all together shall we. Behold the script of the balloon tip.

The Script

Function Invoke-SHDBallonTip {
    [cmdletbinding()]
    param (
        [string]$Message = "Your task is finished",
        [int]$Time = 10000,
        [validateset("Info", "Warning", "Error")][string]$Messagetype = "Info"
    )

    #Adds the reqruired assembly's for the ballon tips. 
    Add-Type -AssemblyName System.Windows.Forms 

    #Creates the notifiy icon that will appear in the notification tray. 
    $global:balloon = New-Object System.Windows.Forms.NotifyIcon
    $path = (Get-Process -id $pid).Path
    $balloon.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon($path) 
    $balloon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::$Messagetype
    $balloon.BalloonTipText = $Message
    $balloon.BalloonTipTitle = "Attention" 
    $balloon.Visible = $true 
    $balloon.ShowBalloonTip($Time)
}

That’s it, folks. Such a simple thing. Here are a few things you can do. You can change the $path to $Path = Get-Process -id $PID. Then in the balloon.icon, use $path.path. In the balloon tip title, you can put $path.description and now you have the description as the title. Other than that, it’s pretty straight forward.

I would love to hear back from yall. Let me know if there is anything you would like for me to write about.

Convert a Mac Address to a CIDR with Powershell

Convert a Mac Address to a CIDR with Powershell

Reading Time: 2 minutes

Today, I needed to convert a mac address to a CIDR. The lesson I learned about this is to go back to the basics instead of looking around for a quick answer. So, to the basics first.

A mac address is a 4 octet number that represents a binary position. It ranges between 1 and 255. This means something like 255.255.255.0 is represented in binary as: 1111 1111 1111 1111 1111 1111 0000 0000

It’s easier to remember the 255 number instead of all those ones and zeros. However, what’s that CIDR? Well, what’s a CIDR? A CIDR represents the mac address and how many positions there are. If you count the number above, it turns into 24 1s and 8 0s. Thus the CIDR is 24. Very simple.

Now, how do you change 240.0.0.0 to a cidr? You can go to a chart and look or we can use PowerShell. The first thing we need to do is split up this string.

"240.0.0.0" -split '\.' 

We split the string up with a split command and tell it to split it up by the . item. We use an exit \ because the . is a special character for regex. Next, we loop through this list of octets with a foreach-object loop.

$Mask -split '\.' | ForEach-Object {
        
    }

Then we break down each octet into a binary number. We do this with a system.convert and tell it the number we bytes we want which is 2.

[System.Convert]::ToString($_, 2)

We then pad that information because sometimes 0s will produce more useless items.

([System.Convert]::ToString($_, 2).PadLeft(8, '0'))

The code above will give us the 1s and 0s of the binary number. Now we need to count the ones. We do this by converting the string to a char array with at .toochararray(). We search the string with a where-object for the number 1 and finally count it.

(([System.Convert]::ToString($_, 2).PadLeft(8, '0')).tochararray() | where-object { $_ -eq '1' } | measure-object).count

Now we need to add each item up. We do this by adding a variable before the loop set to 0 and then add the variable to the variable and the count. Finally, after the loop, display the information.

$Cidr = 0
    $Mask -split '\.' | ForEach-Object {
        $Cidr = $Cidr + (([System.Convert]::ToString($_, 2).PadLeft(8, '0')).tochararray() | where-object { $_ -eq '1' } | measure-object).count
    }
    $Cidr

That’s it yall! We count the number of 1s. That will give us our cidr count.

Script

Have fun with the script!

function ConvertFrom-SHDIPv4MaskToCidr {
    param (
        [string]$Mask
    )
    $Cidr = 0
    $Mask -split '\.' | ForEach-Object {
        $Cidr = $Cidr + (([System.Convert]::ToString($_, 2).PadLeft(8, '0')).tochararray() | where-object { $_ -eq '1' } | measure-object).count
    }
    $Cidr
}
1000 Faces

1000 Faces

Reading Time: 2 minutes

Need 1000 or unique user photos for your lab? There is a great website for just such a thing. https://thispersondoesnotexist.com/image. Let’s break down some code to see how we can pull a few hundred pictures.

The first thing we need to know is how many we want. We are going to get 1000 faces for this example. Next, we need a safe location. Finally, we need an sleep time for the site to regenerate an image. Lets get started.

$StartCount..$FinishCount

the two . in this code allows you to loop through two values. Next we need to pipe this command into a foreach object loop.

$StartCount..$FinishCount | foreach-object {}

Inside the for each loop we start the real work. We are going to use the Invoke-WebRequest. Our URI is the site “https://thispersondoesnotexist.com/image” We will select where we want to save the file using the -outfile location. We are going to save it in the save location we choose earlier. We will put the name as the number you are currently using. Finally we will add a -disablekeepalive flag to stop the system from keeping the connection alive. We do this out of respect for the site.

Invoke-WebRequest -Uri $URL -OutFile "$SaveFolder\$_.jpg" -DisableKeepAlive

Next we need to do a sleep cycle. We do this because if we do a request after another request, we will get the same image. 7 Seconds seems to be the magic number. We do this with Start-Sleep -Seconds 7.

Start-Sleep -Seconds 7

That’s it, It’s a simple process. The Invoke-WebRequest will get the needed image and save it to your computer. The site will generate a new picture each time you reach out.

The Scripts

Here is the script below.

function Get-Faces {
    param (
        [int]$StartCount = 1,
        [int]$FinishCount = 1000,
        [int]$SecondsToSleep = 7,
        [string]$SaveFolder = "C:\Dpb\100000 Faces"
    )
    $URL = 'https://thispersondoesnotexist.com/image'
    $StartCount..$FinishCount | foreach-object {
        Invoke-WebRequest -Uri $URL -OutFile "$SaveFolder\$_.jpg" -DisableKeepAlive
         Start-Sleep -Seconds $SecondsToSleep
    }
}

Have fun with this little guy, just remember to be respectful of the sites you are pulling information from.

Copy A User’s Group to Other Users

Copy A User’s Group to Other Users

Reading Time: 2 minutes

Often times I am asked to give user a the same rights as user b. Since everything is setup with groups, this is easy to do. All I have to do is copy all the security groups to the new users. Normally I would use the command Get-ADPrincipalGroupMembership whoever, that has become less trust worthy over time for us. So I made a different approach.

$Groups = (Get-ADUser -Identity $SourceUser -Properties memberof).memberof -Replace '^cn=([^,]+).+$', '$1' | Sort-Object | ForEach-Object { (Get-ADGroup -Filter "name -like '$_'")}

The above command grabs the source users member of and filters it out for just the name. Then it loops through each and gets the group name. From here we can filter even farther to get the security and then loop those and added the single member making a one liner, but I want this thing to be bigger and better. A good function that can do distribution, security, universal, global and domain local groups to more than one single user with credentials or not. I want this thing to be nice. Here is the heart beat of the command:

foreach ($Target in $TargetUser) {
        $Groups | ForEach-Object {
            try {
                if ($PSBoundParameters.ContainsKey('Credential')) {
                    Write-Verbose "Add $($_.Samaccountname) a $($_.GroupCategory.tostring()) a $($_.GroupScope.tostring()) to $Target"
                    Add-ADGroupMember -Identity $_.samaccountname -Members $Target -Credential $Credential
                } else {
                    Write-Verbose "Add $($_.Samaccountname) a $($_.GroupCategory.tostring()) a $($_.GroupScope.tostring()) to $Target"
                    Add-ADGroupMember -Identity $_.samaccountname -Members $Target
                }
            } catch {
                Write-Verbose "Failed to add $($_.Samaccountname) to $Target"
                Write-Warning "$_ could not apply to $Target"
            }
        }
    }

We target each user with this information. Then we add the group accordingly. So, how do you split the Security group vs Distribution group? We do this by using PSboundparameters and a Where-object group. So we declare a parameter in the parameter area and then ask if it is being used. If it is, we use the where object to search for the groupcategory.

$Groups = (Get-ADUser -Identity $SourceUser -Properties memberof).memberof -Replace '^cn=([^,]+).+$', '$1' | Sort-Object | ForEach-Object { (Get-ADGroup -Filter "name -like '$_'")}
    if ($PSBoundParameters.ContainsKey('GroupCategory')) {$Groups = $Groups | Where-Object {$_.GroupCategory -like "$GroupCategory"}}
    if ($PSBoundParameters.ContainsKey('GroupScope')) {$Groups = $Groups | Where-Object {$_.GroupScope -like "$GroupScope"}}

Lets put it all together shall we, so you can get back on with your day.

Function Copy-SHDUserGroupToUser {
    [cmdletbinding()]
    param (
        [Parameter(
            ValueFromPipeline = $True,
            ValueFromPipelineByPropertyName = $True,
            HelpMessage = "Enter a users Name",
            Mandatory = $true)][String]$SourceUser,
        [Parameter(HelpMessage = "Target User", Mandatory = $True)][string[]]$TargetUser,
        [parameter(Helpmessage = "Group Category")][validateset("Security","Distribution")]$GroupCategory,
        [parameter(Helpmessage = "Group Scope")][validateset("Universal","Global","DomainLocal")]$GroupScope,
        [Parameter(HelpMessage = "Allows for custom Credential.")][System.Management.Automation.PSCredential]$Credential
    )
    $Groups = (Get-ADUser -Identity $SourceUser -Properties memberof).memberof -Replace '^cn=([^,]+).+$', '$1' | Sort-Object | ForEach-Object { (Get-ADGroup -Filter "name -like '$_'")}
    if ($PSBoundParameters.ContainsKey('GroupCategory')) {$Groups = $Groups | Where-Object {$_.GroupCategory -like "$GroupCategory"}}
    if ($PSBoundParameters.ContainsKey('GroupScope')) {$Groups = $Groups | Where-Object {$_.GroupScope -like "$GroupScope"}}
    foreach ($Target in $TargetUser) {
        $Groups | ForEach-Object {
            try {
                if ($PSBoundParameters.ContainsKey('Credential')) {
                    Write-Verbose "Add $($_.Samaccountname) a $($_.GroupCategory.tostring()) a $($_.GroupScope.tostring()) to $Target"
                    Add-ADGroupMember -Identity $_.samaccountname -Members $Target -Credential $Credential
                } else {
                    Write-Verbose "Add $($_.Samaccountname) a $($_.GroupCategory.tostring()) a $($_.GroupScope.tostring()) to $Target"
                    Add-ADGroupMember -Identity $_.samaccountname -Members $Target
                }
            } catch {
                Write-Verbose "Failed to add $($_.Samaccountname) to $Target"
                Write-Warning "$_ could not apply to $Target"
            }
        }
    }
} 

Examples

Copy-SHDUserGroupToUser -SourceUser bobtheuser -TargetUser test1,test2,test3 -GroupCategory Security -GroupScope Global -Verbose

This will add all of bobtheuser’s global security groups to test1, test2, and test3 users and verbose the info out.

Copy-SHDUserGroupToUser -SourceUser bobtheuser -TargetUser test1,test2,test3 

This will add all the bobtheuser’s groups to test1, test2, and test3.

Get Lock Out Info From DC

Get Lock Out Info From DC

Reading Time: 4 minutes

Have you ever had trouble getting lockout information for a single user? The DCs have the logs, but getting them can be tough. The core idea is to use the Get-Winevent command to gather the logs needed. Doing this from a remote computer is time-consuming. However, using the Invoke-Command tends to speed things up. Inside this example, I am going to use something called Splatting. Splatting allows you to drop everything a command needs into a command quickly and easily by creating a hash table. Now, not all commands like splatting, so be aware of that. Here is the heart of the code:

$parameters = @{
        ComputerName = $ComputerName
        ScriptBlock  = {
            Param ($param1)
            $FilterTable = @{
                'StartTime' = $param1
                'EndTime'   = (Get-date)
                'LogName'   = 'Security'
                'Id'        = 4740
            }
            $Events = Get-WinEvent -FilterHashtable $FilterTable
            foreach ($E in $Events) {
                [pscustomobject]@{
                    Time     = $E.TimeCreated
                    ID       = $E.ID 
                    DC       = $E.Properties[4].value
                    SID      = $E.Properties[2].value
                    Domain   = $E.Properties[5].Value
                    Username = $E.Properties[0].value
                    Computer = $E.Properties[1].value
                }
            }
        }
        ArgumentList = $time           
    }
Invoke-command @Parameters

The first thing we are doing is creating the parameters that will be splatted. Just like a hashtable, it’s something = something. So the computer name is the Array computername. The computer name inside the Invoke-command can handle arrays. Making this much easier as we want the DCs.

Next is the script block. This is where the invoke-command will be executing everything from. We start off with a Param () block. It will get it’s input from the Argumentlist later. The Argumentlist is the variables we will be dropping into the command. In this case, it will be time. As in this script you can build your time based on how far back you want to search your logs.

Next is the filter table. The Get-winevent command likes hashtables. So we start off with our starttime, basically how far back we are going to go. Then the end time which is the time of execution. The Logname is the type, there is an application, security, system, and more. Since the lockouts are security, we choose security. Finally, we want the ID. For account lockouts, the ID is 4740. There is an online Encyclopedia for windows logs. (Link)

The Get-WinEvent command is followed by the FilterHashtable flag. We dump this into the events variable for sort throu. Now we search each Event in Events. We express this with an E because $event is a system variable and we don’t want to cause any issues. We want the time it was created. The ID, what DC it came from. The SID of the user. The Domain. The username of the user, and finally we want the calling workstation. Once all that is put together we splat it into the command Invoke-Command.

That is the heart, but not the power. This script allows you to call back from the day, hour, and minute back. If you don’t put in a input, it makes it a day for you. It also can target a single user or different computers. Finally it gives you the ability to use different credentials. Lets take a look at the parameters.

param (
        [parameter(Helpmessage = "Username of target")][Alias('User', 'Samaccountname')][String[]]$Username,    
        [parameter(Helpmessage = "Days back")][int]$Day,
        [parameter(Helpmessage = "Days back")][int]$Hour,
        [parameter(Helpmessage = "Days back")][int]$Minute,
        [parameter(Helpmessage = "Computers to target, default is domain controllers.")][Alias('Computer', 'PC')][String[]]$ComputerName = (Get-ADDomain).ReplicaDirectoryServers,
        [Parameter(HelpMessage = "Allows for custom Credential.")][System.Management.Automation.PSCredential]$Credential
    )

We first are looking for a list of usernames. Notice we don’t have to declare this variable. So, if we just want all the lockouts, we just don’t delcare it. This is done by using the PSBoundParameters.ContainsKey option. So, if the script sees the username is being used, it will follow this command.

if ($PSBoundParameters.ContainsKey('Username')) {
        foreach ($user in $Username) {
            $Return | Where-Object { $_.Username -like "$user" } | Sort-Object Time | Select-Object Username, Domain, SID, Time, Computer, PSComputerName
        }
    }

The next parameters are also optional. It is days, hour, and minutes back. So, you can state 1 day, 3 hours, and 2 minutes ago and it will find the times during starting at that moment forwarder. This is good if you plan to use this script as an auditor (what it was built for). How this is achived once again is the use of psboundparameters.containskey.

$time = Get-date
if ($PSBoundParameters.ContainsKey('Day')) { $time = ($time.AddDays( - ($day))) } 
    if ($PSBoundParameters.ContainsKey('Hour')) { $time = ($time.AddHours( - ($Hour))) } 
    if ($PSBoundParameters.ContainsKey('Minute')) { $time = ($time.AddMinutes( - ($Minute))) } 
    if (!($PSBoundParameters.ContainsKey('Day')) -and !($PSBoundParameters.ContainsKey('Hour')) -and !($PSBoundParameters.ContainsKey('Minute'))) {
        $time = $time.AddDays( - (1))
    }

We first declare the time with the current time/date. Then based on the input, we remove days, hours, or minutes from the current time. If there is no input, we tell time to remove 1 day. Depending on the size of your organization and how many logs you have, this can be useful. The $time will be used in the argumentlist.

Now we have the Computername parameter. If you have differentiating computers than your DCs that handles these logs, you can target them with this command. Otherwise, we grab the dc information with a single line of code.

(Get-ADDomain).ReplicaDirectoryServers

Finally we have the ability to add the credentials. Once you delcare the credentials, we add the credential flag to our splat. To do this we create a hashtable with a single item and add it to the parameter.

if ($PSBoundParameters.ContainsKey('Credential')) {
        $parameters += @{Credential = $Credential }
    }

Now, lets put all this together in a single script so you can copy it and move along with your day.

Function Get-SHDLockoutInfo {
    [cmdletbinding()]
    param (
        [parameter(Helpmessage = "Username of target")][Alias('User', 'Samaccountname')][String[]]$Username,    
        [parameter(Helpmessage = "Days back")][int]$Day,
        [parameter(Helpmessage = "Days back")][int]$Hour,
        [parameter(Helpmessage = "Days back")][int]$Minute,
        [parameter(Helpmessage = "Computers to target, default is domain controllers.")][Alias('Computer', 'PC')][String[]]$ComputerName = (Get-ADDomain).ReplicaDirectoryServers,
        [Parameter(HelpMessage = "Allows for custom Credential.")][System.Management.Automation.PSCredential]$Credential
    )
    $time = Get-date
    if ($PSBoundParameters.ContainsKey('Day')) { $time = ($time.AddDays( - ($day))) } 
    if ($PSBoundParameters.ContainsKey('Hour')) { $time = ($time.AddHours( - ($Hour))) } 
    if ($PSBoundParameters.ContainsKey('Minute')) { $time = ($time.AddMinutes( - ($Minute))) } 
    if (!($PSBoundParameters.ContainsKey('Day')) -and !($PSBoundParameters.ContainsKey('Hour')) -and !($PSBoundParameters.ContainsKey('Minute'))) {
        $time = $time.AddDays( - (1))
    }
    $parameters = @{
        ComputerName = $ComputerName
        ScriptBlock  = {
            Param ($param1)
            $FilterTable = @{
                'StartTime' = $param1
                'EndTime'   = (Get-date)
                'LogName'   = 'Security'
                'Id'        = 4740
            }
            $Events = Get-WinEvent -FilterHashtable $FilterTable
            foreach ($E in $Events) {
                [pscustomobject]@{
                    Time     = $E.TimeCreated
                    ID       = $E.ID 
                    DC       = $E.Properties[4].value
                    SID      = $E.Properties[2].value
                    Domain   = $E.Properties[5].Value
                    Username = $E.Properties[0].value
                    Computer = $E.Properties[1].value
                }
            }
        }
        ArgumentList = $time           
    }
    if ($PSBoundParameters.ContainsKey('Credential')) {
        $parameters += @{Credential = $Credential }
    }
    $Return = Invoke-Command @parameters 
    if ($PSBoundParameters.ContainsKey('Username')) {
        foreach ($user in $Username) {
            $Return | Where-Object { $_.Username -like "$user" } | Sort-Object Time | Select-Object Username, Domain, SID, Time, Computer, PSComputerName
        }
    }
    else {
        $Return | Sort-Object UserName | Select-Object Username, Domain, SID, Time, Computer, PSComputerName
    }
} 

I hope you enjoy this little script and I hope it helps you grow your company. If you find any bugs, let me know. Thank you so much and have a great day!