Thursday, 27 May 2021

Searching for String within Multiple Files using Powershell

 I recently had a situation where I had to search for a particular string within a large number of files. More specifically, I was looking for what file contained a set of  text used for a menu on a website - the problem is I didn't know which file contained the text, but I knew what I was looking for. The web server had a large number of xml, html and css files, so I quickly put together a powershell script to search these files quickly and was able to find what I was looking for within a matter of minutes! Certainly faster than manually opening files and performing a CTRL + F find within them.

$files = get-childitem -path "C:\inetpub\wwwroot\*.css" -recurse
foreach ($file in $files)
    {
    $filecontent = get-content $($file.fullname)
    $lookup = $filecontent | select-string "What I'm Looking For"
    if ($lookup)
    {write-host "$($file.fullname)"}
    }

The first line searches the folder C:\inetpub\wwwroot for any file ending in .css.  The -recurse switch means it searches all subfolders within C:\inetpub\wwwroot as well. You can of course change the search directory and file type being searched for

$files = get-childitem -path "C:\inetpub\wwwroot\*.css" -recurse

The next section is a ForEach loop that goes through every file that is found within $files (as outlined above). The content of the file is read into the $filecontent variable

$filecontent = get-content $($file.fullname)

Next we perform a search of all the text within the $filecontent variable for a particular string of text. In this example the text we're looking for is "What I'm Looking For" - change this to whatever string of text you are trying to find tin these files. The result of the search is stored in the $lookup variable. If no match is found then $lookup will be null.

$lookup = $filecontent | select-string "What I'm Looking For"

Finally, if a result is found and the $lookup variable is not null, the full name of the file is written to the screen

if ($lookup)
    {write-host "$($file.fullname)"}

This little script saved me a heap of time - as built in windows search functions are pretty unreliable and don't search for content within files. Hope it is able to help you as well.

Tuesday, 11 May 2021

Managing Google Classroom with GAM and Powershell

Part of my job requires me to actively manage our instance of Google Classroom - the adding/removal of teachers, students, changing of class owners are just a couple of the every day tasks that are required. Unfortunately, Google Classroom doesn't have an included management interface for doing this - but there is a great command line utility called "GAM" which I use to accomplish these tasks.

I looked at a few different management utilities to do this - and essentially left GAM until last as I was really after a reliable (and free) utility that had a graphical user interface (GUI) instead of command line, but unfortunately at the time I was looking there was nothing that met these requirements (and worked) for me.

GAM can be downloaded from here - they have an excellent Wiki section with detailed instructions on how to get GAM setup and running within your environment.

In subsequent blog posts I'll be covering some of the scripts/commands that I use to manage and bring some control over Google Classroom such as identifying active classes, exporting them to CSV, archiving (disabling) old classes that are no longer required, synchronising members (teachers and students) and managing/maintaining the owners of Classrooms.

I'll be executing GAM commands from within powershell to do this.

What tool do you use for managing Google Classroom? Do you wish Google would create some kind of web/graphical user interface to do this?


Monday, 10 May 2021

Automatically Disable Expired Accounts in Active Directory with Powershell

Active Directory has the ability to set an expiration date on accounts so that the account becomes inactive and can't be accessed/logged into once this date has passed. The problem with the way this works, is that technically the account is still "enabled" - as it's not actually "disabled" - it's simply expired. Disabled accounts are easily identified within Active Directory Users & Computers  by a slightly different icon next to the account name. Expired accounts however, do not have any visual indication that they are expired, making them harder to identify.

I have created a powershell script which I run on a daily basis to automatically identify, and then disable any Active Directory accounts that have expired.

$report = @()
$expiredusers = get-aduser -Filter * -Properties AccountExpirationDate | where {$_.AccountExpirationDate -lt (get-date) -and $_.AccountExpirationDate -ne $null -and $_.enabled -eq $true}

if ($expiredusers)
    {
    set-aduser -identity $($user.samaccountname) -enabled $false -description $newdesc
    $report += new-object psobject -property @{Username=$($user.userprincipalname);AccountExpirationDate=$($user.accountexpirationdate);DN=$($user.distinguishedname)}
    }
The $report = @() line creates a new/blank array that we will use later on to store the results of any expired 

Next we perform a search using the get-aduser cmdlet to search all Active Directory accounts for those with an expiration date prior (less than) the current date and whose current account status is Enabled

An If statement is then run if any expired accounts are found which disables the account, and adds the account into the $report variable/array we declared earlier.

You can then export the $report variable to a CSV file, email it to yourself etc.

Friday, 7 May 2021

Connect to Office365/O365 using Powershell

A quick an easy post with details on how to connect your Office 365 (O365) environment to allow management with powershell cmdlets

Make sure you change the value next to -Credential from username@domain.com to the username you need to use to connect

$ex = New-PSSession -ConfigurationName Microsoft.Exchange -Credential username@domain.com -ConnectionUri https://outlook.office365.com/powershell -Authentication basic -AllowRedirection

import-pssession $ex

After executing this command, a popup window will appear for you to enter your password into



You will then receive a couple of warning messages to indicate that your session has been redirected to a slightly different URL, and another about some of the imported commands. These can be safely ignored

WARNING: Your connection has been redirected to the following URI: "https://outlook.office365.com/PowerShell-LiveID?PSVersion=5.1.17763.1852 "
WARNING: The names of some imported commands from the module 'tmp_4kculvlx.2wc' include unapproved verbs that might make them less discoverable. To find the commands with unapproved verbs, run the Import-Module command again with the Verbose parameter. For a
 list of approved verbs, type Get-Verb.

You can then proceed to manage your Office 365 environment with the standard powershell commands - get-mailbox etc.


Thursday, 6 May 2021

Monitoring Hyper-V to Azure Site Replication with Powershell

Here is a simple script that I use to monitor the replication state of Hyper-V servers that I have replicated to Azure using Site Replication.

I have this script run every morning as a scheduled task which sends me an email with a nicely formatted table showing the replication status of all of my Hyper-V VM's to Azure

I have 4 Hyper-V nodes in a cluster - I have specified the 4 node names individually and used a ForEach loop to cycle through them all the generate the report. You can remove the loop if you're only using this for a single server/host.

(You will need to have the Hyper-V powershell module/cmdlets installed for this to work)

import-module Hyper-V -RequiredVersion 1.1
$repstate = @()
$date = get-date -f "dd-MM-yyyy"
$clusternodes = "node1", "node2", "node3", "node4"

foreach ($node in $clusternodes)
    {
    $repstate += get-vmreplication -computername $node | select Name, State, Health, Mode, FrequencySec, PrimaryServer, ReplicaServer, PrimaryServerName, LastReplicationTime
    }

You can then export the values from the $repstate variable to a CSV file and attach it, or as I have done, convert it to HTML and set it to the body of an email message

$mailmessage.Body = $repstate | ConvertTo-HTML -fragment



Wednesday, 5 May 2021

Storing and Using Encrypted Passwords with Powershell

There are lots of reasons why a username and password may be incorporated into a Powershell script - naturally, the quickest and easiest way to do this is to simply store the username and password in a script using a couple of variables such as;

$username = "user1"
$password = "password1"

Simple, and it works, but is definitely not secure.

There is a simple method you can use though to encrypt the password using powershell and store it as a text file on your computer. The password is encrypted using the currently logged in windows account that is running the powershell script (when you encrypt it) so keep this in mind if you are trying to decrypt the password with a different user account (because it won't work).

This is of course a 2 step process. The first step is to take your unsecured password (yourpassword) then encrypt it, and store the encrypted text value into a txt file (in C:\temp\user1-secpw.txt)

#Store Secure Credentials 
$unsecpw = "yourpassword"
$secpw = $unsecpw | convertto-securestring -asplaintext -force
$securestringtext = $secpw | convertfrom-securestring
set-content "C:\temp\user1-secpw.txt" $securestringtext

Once you've done this part and you have your password stored in the txt file, you should at the very least clear the value from the $unsecpw if you are saving this script - otherwise you're essentially defeating the purpose of encrypting it in the first place.

The second part is of course the process of importing the txt file into a new script so you can use the password. As previously mentioned, you will only be able to decrypt the password if you are executing the decryption commands under the same user security context (ie. windows user) as when you encrypted it.

$pwd = get-content "C:\temp\user1-secpw.txt" | convertto-securestring
$BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($pwd)
$Unsecpw = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)

Your password is now available to use via the $unsecpw variable

Tuesday, 4 May 2021

Reading Emails from O365 Mailbox with Powershell

Here is a script you can use to connect to an Office 365 (O365) mailbox to read/parse email messages. After the message has been read it is then moved to a specific folder within the mailbox. The full script is included first, and I will then break it down section by section afterwards

#Enable Exchange/O365 cmdlets
add-pssnapin *exchange* -erroraction SilentlyContinue
Import-Module MSOnline -ErrorAction SilentlyContinue

#Credentials
$email = email@domain.com
$password = "password"
$emaildomain = domain.com

#Connect to Mailbox
[void][Reflection.Assembly]::LoadFile("C:\Program Files\Microsoft\Exchange\Web Services\2.2\Microsoft.Exchange.WebServices.dll")
$s = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2007_SP1)
$s.Credentials = New-Object Net.NetworkCredential($email, $password, $emaildomain)
$s.Url = new-object Uri("https://outlook.office365.com/EWS/Exchange.asmx");
$inbox = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($s,[Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox)

#Setup folder and folder view (to move completed messages to)
$fv = new-object Microsoft.Exchange.WebServices.Data.FolderView(30)
$fv.Traversal = "Deep"
$folders = $s.findFolders([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot,$fv)
$processedfolder = $folders | where {$_.displayname -eq "ProcessedEmails"}

#Get unread messages
$msgs = $null
$iv = new-object Microsoft.Exchange.WebServices.Data.ItemView(50)
$inboxfilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+SearchFilterCollection([Microsoft.Exchange.WebServices.Data.LogicalOperator]::And)
$ifisread = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::IsRead,$false)
$inboxfilter.add($ifisread)
$msgs = $s.FindItems($inbox.Id, $inboxfilter, $iv)

#If unread message(s) found, do stuff
if ($msgs.totalcount -ne 0)
    {
    $psPropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
    $psPropertySet.RequestedBodyType = [Microsoft.Exchange.WebServices.Data.BodyType]::Text;
    $s.LoadPropertiesForItems($msgs,$psPropertySet)
    $msgs = $msgs.items
    }

#Move Message to Processed Folder
$msg.Move($processedfolder.Id)


The #Enable Exchange/O365 Cmdlets section as the comment suggests is to enable the Microsoft Exchange & Office 365 cmdlets

add-pssnapin *exchange* -erroraction SilentlyContinue
Import-Module MSOnline -ErrorAction SilentlyContinue

The #Credentials section is where you enter the details (username, password and email domain) used to connect to the mailbox

#Credentials
$email = email@domain.com
$password = "password"
$emaildomain = domain.com

Next, we will connect to the mailbox (and look at the Inbox folder specifically), using the credentials specified above and Microsoft Exchange Web Services. If you don't already have the Exchange Web Services files installed you will need to install them

#Connect to Mailbox
[void][Reflection.Assembly]::LoadFile("C:\Program Files\Microsoft\Exchange\Web Services\2.2\Microsoft.Exchange.WebServices.dll")
$s = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2007_SP1)
$s.Credentials = New-Object Net.NetworkCredential($email, $password, $emaildomain)
$s.Url = new-object Uri("https://outlook.office365.com/EWS/Exchange.asmx");
$inbox = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($s,[Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox)

Now, a folder view is setup to obtain the list of all the folders in the mailbox. All the list of available folders are stored in the $folders variable. We then look for a folder called "ProcessedTickets" and set this folder to be the $processedfolder variable. We use this variable (folder) later on to move processed emails into. You can of course change this folder name to be anything you like.

#Setup folder and folder view (to move completed messages to)
$fv = new-object Microsoft.Exchange.WebServices.Data.FolderView(30)
$fv.Traversal = "Deep"
$folders = $s.findFolders([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot,$fv)
$processedfolder = $folders | where {$_.displayname -eq "ProcessedEmails"}

Now we will retrieve any unread messages within the mailbox - filters are set using the $inboxfilter and $ifisread variables. All messages matching the search criteria are stored in the $msgs variable

#Get unread messages
$msgs = $null
$iv = new-object Microsoft.Exchange.WebServices.Data.ItemView(50)
$inboxfilter = new-object
Microsoft.Exchange.WebServices.Data.SearchFilter+SearchFilterCollection([Microsoft.Exchange.WebServices.Data.LogicalOperator]::And)
$ifisread = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::IsRead,$false)
$inboxfilter.add($ifisread)
$msgs = $s.FindItems($inbox.Id, $inboxfilter, $iv)

If messages are found in the $msgs variable, the properties for those messages are then loaded back into the $msgs variable

#If unread message(s) found, do stuff
if ($msgs.totalcount -ne 0)
    {
    $psPropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
    $psPropertySet.RequestedBodyType = [Microsoft.Exchange.WebServices.Data.BodyType]::Text;
    $s.LoadPropertiesForItems($msgs,$psPropertySet)
    $msgs = $msgs.items
    }

If you look at the $msgs variable at this point, it should contain the properties of all mail messages found in the inbox that are unread. You can then use a foreach loop to cycle through each message and action accordingly.

Properties such as subject, body text, from address are all available and can be used to further filter entries

Once you've actioned the email and done what you need, you can then move the email into the processedfolders folder we previously specified

#Move Message to Processed Folder
$msg.Move($processedfolder.Id)


Monday, 3 May 2021

Write XML Files with Powershell - The Easy Way

 I needed to create a custom XML file using powershell as part of a script I was developing. I found it difficult to understand and use the in-built XML functionality with powershell so I attempted to simply use write-output commands to write the XML file. It worked - mostly, but the application I was submitting the XML file wasn't recognizing it. As it turns out there's some hidden properties within a properly formed XML file which requires a little trick to get right when forming it this way - so here's how I did it..

            $xmlfile = "C:\temp\test.xml"
            write-output '<?xml version="1.0"?>' | out-file "$xmlfile"
            write-output '<test>' | out-file "$xmlfile" -append
            write-output "  <version>1.0</version>" | out-file "$xmlfile" -append
            write-output '</test>' | out-file "$xmlfile" -append

            $xml = [xml](Get-Content $xmlfile)
            $xml.Save($xmlfile)

The first part of the script is fairly straightforward - declare the path to your XML file in the $xmlfile variable.

Then, use write-output commands to enter the text data into your XML file - make sure you include the -append argument after the first line to ensure the lines are appended onto the end of the file - you can open the file in a text editor such as NotePad++ to make sure it appears as you expect. Make sure you close off all your tags so that formatting is valid.

The last 2 lines is the trick I previously mentioned - use the [xml] class with get-content to import the XML file into the $xml variable. Once the data is in the $xml variable, it is then re-saved - these 2 lines are what puts the hidden formatting into the XML file to make it valid.