Creating a Table in Word Using PowerShell

In this article, I will be showing you how to build a table in Word using PowerShell.

First we will go ahead and make the initial object creation and open up Word.

 
$Word = New-Object -ComObject Word.Application
$Word.Visible = $True
$Document = $Word.Documents.Add()
$Selection = $Word.Selection

Now that we have done that, we can start looking at building out a table to use in our document. The first example will look more like a typical table that has the headers on the first row and the rest of the rows will consist of the actual data. Of course, the first thing that I need first is some actual data that I can put into a table.

 
$Volume = Get-WmiObject Win32_Volume

I think listing out all of the volumes on my system should suffice. Now that I have this information, I can now start building out my table. One of the values that I want to document has a specific lookup that I need to quickly build using a hash table.

 
$DriveType = @{
    0x0 = 'Unknown'
    0x1 = 'No Root Directory'
    0x2 = 'Removable Disk'
    0x3 = 'Local Disk'
    0x4 = 'Network Drive'
    0x5 = 'Compact Disk'
    0x6 = 'RAM Disk'
}

I want to ensure that I am at the very end of my current spot in the document so I can put the table after whatever I may have written. I also need to make sure that the object is treated like a collection, otherwise I won’t be able to specify the index, in this case –1 for the last item in the collection.

 
$Range = @($Selection.Paragraphs)[-1].Range

The $Range will be used soon in the construction and placement of the table. Using the $Selection, I will create a new able by using the Tables.Add() method and then saving the output of the method which is the table object that I can then use to begin processing and adding data into the table.

Looking at the parameters really doesn’t help much.

 
$Selection.Tables.add

image

This is one of those cases where a trip to MSDN will provide more useful information to determine what we can actually use.

image

Now that is much more useful to us! we can see that we need to specify the Range that was found as well as some parameters for the number of Rows and Columns as well as the behavior of the table and how the items will fit. We don’t actually have to specify the Behaviors, but in this case I will be adding those as well.

As it shows, the rows and columns are pretty much a no-brainer as we can look at how many properties we are using and that will be the Column count and as for the Rows, we will take the number of objects returned and then add 1 to the count which will account for the header row. In this case, I am going to have 6 columns and 5 rows in my table.

image

Now for my Table and AutoFit behavior, I need to figure out exactly what is available to choose from. Again, I can find this inform on MSDN, but I think this is a great exercise in PowerShell’s ability to easily explore the environment to locate what we are looking for.

 
$AutoFit = [Microsoft.Office.Interop.Word.WdDefaultTableBehavior].Assembly.GetTypes() | 
Where {$_.Name -match 'autofit'}
$AutoFit | Select FullName

$DefaultTable = [Microsoft.Office.Interop.Word.WdDefaultTableBehavior].Assembly.GetTypes() | 
Where {$_.Name -match 'defaulttable'}
$DefaultTable | Select FullName

image

Perfect! Now to see what values we have to choose from.

 
[Enum]::GetNames($AutoFit)
[Enum]::GetNames($DefaultTable)

image

For my table, I am going with wdAutoFitContent and wdWord9TableBehavior when I build out my table.

 
$Table = $Selection.Tables.add(
    $Selection.Range,($Volume.Count + 1),6,
    [Microsoft.Office.Interop.Word.WdDefaultTableBehavior]::wdWord9TableBehavior,
    [Microsoft.Office.Interop.Word.WdAutoFitBehavior]::wdAutoFitContent
)

image

I am next going to pick out the table style that will be used for this specific table. Because I want a focus on the distinction between the header and the data, I am going with Medium Shading 1 – Accent 1 which means that my table goes from this…

image

…to this:

 
$Table.Style = "Medium Shading 1 - Accent 1" 

image

Ok, right now this really doesn’t look all that impressive, but it will look nicer once we have finished with the data. And speaking of data, lets start adding the header to this now.

Think of the table as a multi-dimensional array and that you have to use coordinates to properly place the data. In this case we are using the $Table.Cell(column,row) property where we have to specify the coordinates within the parenthesis to say where the data going and if we are going to add some custom formatting to that particular cell. Once each column has been completed we simply move onto the next column and perform the same operation until completed and ready for the next row.

 
## Header
$Table.cell(1,1).range.Bold=1
$Table.cell(1,1).range.text = "Drive"
$Table.cell(1,2).range.Bold=1
$Table.cell(1,2).range.text = "DriveType"
$Table.cell(1,3).range.Bold=1
$Table.cell(1,3).range.text = "Label"
$Table.cell(1,4).range.Bold=1
$Table.cell(1,4).range.text = "FileSystem"
$Table.cell(1,5).range.Bold=1
$Table.cell(1,5).range.text = "FreeSpaceGB"
$Table.cell(1,6).range.Bold=1
$Table.cell(1,6).range.text = "CapacityGB"

image

Starting to look like something a little more useful now. The last piece of this is to add the data. Given that I really don’t know just how much data might be available at the time of this, I am relying on a For() loop to handle this for me.

 
## Values
For ($i=0; $i -lt ($Volume.Count); $i++) {
    $Table.cell(($i+2),1).range.Bold = 0
    $Table.cell(($i+2),1).range.text = $Volume[$i].Name
    $Table.cell(($i+2),2).range.Bold = 0
    $Table.cell(($i+2),2).range.text = $DriveType[[int]$Volume[$i].DriveType]
    $Table.cell(($i+2),3).range.Bold = 0
    $Table.cell(($i+2),3).range.text = $Volume[$i].Label
    $Table.cell(($i+2),4).range.Bold = 0
    $Table.cell(($i+2),4).range.text = $Volume[$i].FileSystem
    $Table.cell(($i+2),5).range.Bold = 0
    $Table.cell(($i+2),5).range.text = [math]::Round($Volume[$i].FreeSpace/1GB,2)
    $Table.cell(($i+2),6).range.Bold = 0
    $Table.cell(($i+2),6).range.text = [math]::Round($Volume[$i].Capacity/1GB,2)
}

Same approach as my header but not only do it increment the columns on each pass, I also have to make sure to increment the rows as well to ensure that the data is accurate. The end result is something like this:

image

Lastly, I want to make sure I am at the end of this and outside of my table so I can continue adding data or writing if necessary without writing to the table. I also want to add a space after the table for formatting purposes.

 
$Word.Selection.Start= $Document.Content.End
$Selection.TypeParagraph()

If you wanted more of a list type of format, that can also be accomplished using a similar approach. This time we will keep the number of columns to two and use as many rows as we need.

 
$BIOS = Get-WmiObject Win32_Bios

$Range = @($Selection.Paragraphs)[-1].Range
$Table = $Selection.Tables.add(
    $Selection.Range,5,2,
    [Microsoft.Office.Interop.Word.WdDefaultTableBehavior]::wdWord9TableBehavior,
    [Microsoft.Office.Interop.Word.WdAutoFitBehavior]::wdAutoFitContent
)

Another different is that I don’t want to focus on the first row, so a different style will be used.

 
$Table.Style = "Light Shading - Accent 1"

And lastly, we start building out the list table.

 
## Header
$Table.cell(1,1).range.Bold=1
$Table.cell(1,1).range.text = "Manufacturer"
$Table.cell(2,1).range.Bold=1
$Table.cell(2,1).range.text = "Name"
$Table.cell(3,1).range.Bold=1
$Table.cell(3,1).range.text = "Version"
$Table.cell(4,1).range.Bold=1
$Table.cell(4,1).range.text = "Serial Number"
$Table.cell(5,1).range.Bold=1
$Table.cell(5,1).range.text = "BIOS Version"
    
## Data
$Table.cell(1,2).range.Bold = 0
$Table.cell(1,2).range.text = $BIOS.Manufacturer
$Table.cell(2,2).range.Bold = 0
$Table.cell(2,2).range.text = $BIOS.Name
$Table.cell(3,2).range.Bold = 0
$Table.cell(3,2).range.text = $BIOS.Version
$Table.cell(4,2).range.Bold = 0
$Table.cell(4,2).range.text = $BIOS.SerialNumber
$Table.cell(5,2).range.Bold = 0
$Table.cell(5,2).range.text = $BIOS.SMBIOSBIOSVersion

$Word.Selection.Start= $Document.Content.End
$Selection.TypeParagraph()

The end result is something that looks like this:

image

Of course, this can easily start adding to your line count if you plan on having several tables that you want to add in your document and writing code isn’t about who has the most rows. Smile

I wrote a small function that takes an object and can build out a Table or List fairly easily and all it requires is the $Object to be added to the table and the $Selection object. Note that this is one that I use on my personal projects but can be altered to meet your needs. Another thing is that you will need to tailor your object to only have specific properties, otherwise your table/list will be overrun with data.

An example of it in use is here:

 
$Volume = @(Get-WmiObject Win32_Volume  | ForEach {
    [pscustomobject]@{
        Drive = $_.Name
        DriveType = $DriveType[[int]$_.DriveType]
        Label = $_.label
        FileSystem = $_.FileSystem
        "FreeSpace(GB)" = "{0:N2}" -f ($_.FreeSpace /1GB)
        "Capacity(GB)" = "{0:N2}" -f ($_.Capacity/1GB)
    }
})

New-WordTable -Object $Volume -Columns 6 -Rows ($Volume.Count+1) –AsTable

And here:

 
$BIOS = @(Get-WmiObject Win32_Bios | ForEach {
    [pscustomobject] @{
        Manufacturer = $_.Manufacturer
        Name = $_.Name
        Version = $_.Version
        SerialNumber = $_.SerialNumber
        BIOSVersion = $_.SMBIOSBIOSVersion
    }
})

New-WordTable -Object $BIOS -Columns 2 -Rows ($BIOS.PSObject.Properties | Measure-Object).Count -AsList

Source Code

Function New-WordTable {
[cmdletbinding(
DefaultParameterSetName=’Table’
)]
Param (
[parameter()]
[object]$WordObject,
[parameter()]
[object]$Object,
[parameter()]
[int]$Columns,
[parameter()]
[int]$Rows,
[parameter(ParameterSetName=’Table’)]
[switch]$AsTable,
[parameter(ParameterSetName=’List’)]
[switch]$AsList,
[parameter()]
[string]$TableStyle,
[parameter()]
[Microsoft.Office.Interop.Word.WdDefaultTableBehavior]$TableBehavior = ‘wdWord9TableBehavior’,
[parameter()]
[Microsoft.Office.Interop.Word.WdAutoFitBehavior]$AutoFitBehavior = ‘wdAutoFitContent’
)
#Specifying 0 index ensures we get accurate data from a single object
$Properties = $Object[0].psobject.properties.name
$Range = @($WordObject.Paragraphs)[-1].Range
$Table = $WordObject.Tables.add(
$WordObject.Range,$Rows,$Columns,$TableBehavior, $AutoFitBehavior)

Switch ($PSCmdlet.ParameterSetName) {
‘Table’ {
If (-NOT $PSBoundParameters.ContainsKey(‘TableStyle’)) {
$Table.Style = “Medium Shading 1 – Accent 1″
}
$c = 1
$r = 1
#Build header
$Properties | ForEach {
Write-Verbose “Adding $($)”
$Table.cell($r,$c).range.Bold=1
$Table.cell($r,$c).range.text = $

$c++
}
$c = 1
#Add Data
For ($i=0; $i -lt (($Object | Measure-Object).Count); $i++) {
$Properties | ForEach {
$Table.cell(($i+2),$c).range.Bold=0
$Table.cell(($i+2),$c).range.text = $Object[$i].$_
$c++
}
$c = 1
}
}
‘List’ {
If (-NOT $PSBoundParameters.ContainsKey(‘TableStyle’)) {
$Table.Style = “Light Shading – Accent 1″
}
$c = 1
$r = 1
$Properties | ForEach {
$Table.cell($r,$c).range.Bold=1
$Table.cell($r,$c).range.text = $_
$c++
$Table.cell($r,$c).range.Bold=0
$Table.cell($r,$c).range.text = $Object.$_
$c–
$r++
}
}
}
}

Posted in powershell | Tagged , , , | Leave a comment

Speaking at Nebraska Code Conference in March

imageI’ll be speaking at  the Nebraska Code Conference in March and I’ll be talking Trend Reporting Using PowerShell and SQL. This will be my first in person speaking gig at a conference so I am both excited and a little nervous about it but definitely looking forward to talking about PowerShell and showing something cool in the process.

If you happen to be in the area of Lincoln, Nebraska during 19 – 21 March, be sure to attend this conference and check out my session!

My Session: http://nebraskacode.com/sessions/trend-reporting-using-powershell-and-sql

Nebraska Code Conference site: http://nebraskacode.com/

Posted in powershell, News | Tagged , , , , , | Leave a comment

Creating a Table of Contents in Word Using PowerShell

Continuing on from my last article, this one will show you how to build a Table of Contents in Word using PowerShell. We will be using the same techniques and some of the same code to set up our word document and I will be pointing out some areas of interest that will be used to help create a table of contents.

First off, let’s go ahead and get our Word object ready to go:

 
$Word = New-Object -ComObject Word.Application
$Word.Visible = $True
$Document = $Word.Documents.Add()
$Selection = $Word.Selection
New-WordText -Text "$($Env:Computername)" -Style 'Title'
New-WordText -Text "Report compiled at $(Get-Date)."

image

The New-WordText is not a built-in function or cmdlet that you can use natively in PowerShell. It is simply a helper function that I have in a different script that works well for what I want to do here. The source code for it is here:

 
Function New-WordText {
    Param (
        [string]$Text,
        [int]$Size = 11,
        [string]$Style = 'Normal',
        [Microsoft.Office.Interop.Word.WdColor]$ForegroundColor = "wdColorAutomatic",
        [switch]$Bold,
        [switch]$Italic,
        [switch]$NoNewLine
    )  
    Try {
        $Selection.Style = $Style
    } Catch {
        Write-Warning "Style: `"$Style`" doesn't exist! Try another name."
        Break
    }

    If ($Style -notmatch 'Title|^Heading'){
        $Selection.Font.Size = $Size  
        If ($PSBoundParameters.ContainsKey('Bold')) {
            $Selection.Font.Bold = 1
        } Else {
            $Selection.Font.Bold = 0
        }
        If ($PSBoundParameters.ContainsKey('Italic')) {
            $Selection.Font.Italic = 1
        } Else {
            $Selection.Font.Italic = 0
        }          
        $Selection.Font.Color = $ForegroundColor
    }

    $Selection.TypeText($Text)

    If (-NOT $PSBoundParameters.ContainsKey('NoNewLine')) {
        $Selection.TypeParagraph()
    }
}

Using this function will make writing text to the word document simpler and save on code later on. With this done, now we can focus on building out the Table of Contents in the word document. Because I want this to be at the front of the document, I need to build it out now rather than later on in the document or after I am finished.

In order to do this I will be using the $Document.TablesOfContents.Add() method which has a single required parameter: Range and several other optional parameters that you can supply if you want extra configuration of the table of contents. For more information, you can check out the Add() method information here. I won’t be using any of those here and will just focus on the default configuration of the table of contents so only the Range is important to me. The range is nothing more than a selection of where I want the table of contents to be at.  Speaking of the range, I can pull that information by using the following code:

 
$range = $Selection.Range

With that out of the way, I can now create the table of contents. When I do this, I need to keep the resulting object that is outputted when using the Add() method.

 
$toc = $Document.TablesOfContents.Add($range)
$Selection.TypeParagraph()

image

Nothing to worry about here. This is just letting use know that while the table of contents has been created, we haven’t actually defined anything  that should appear there yet. That will change shortly as we begin adding data to the word document.

A quick view of the table of contents object shows some nice information such as the Update() method. This may be useful at some point in time to us.

image

Moving on, we now need to set up a way to have specific items appear in the table of contents. But what do we need to do in order for this to happen? The answer lies in the Heading style that exists in Word. Depending on your Word configuration, you may have a set number of heading styles available to use. In my case, I have quite a few.

image

I actually have 6 heading styles but the other one is on the second row. Regardless of that, I now know what I can work with in my document and at the end, we can see how this affects the table of contents.

With that, I am going to randomly create some heading styles with random values so we can have some data available.

 
For($i=0;$i-lt 8;$i++) {
    New-WordText -Text "Main Heading $i" -Style 'Heading 1'
    For($i=0;$i-lt 8;$i++) {
        New-WordText -Text "Secondary Heading $i" -Style 'Heading 2'
        For($i=0;$i-lt 8;$i++) {
            New-WordText -Text "Third Heading $i" -Style 'Heading 3'
        }
    }
}

Now I have about 5 pages of useless information in my document! Now comes the fun part where I tell the table of contents to update itself based on all of the heading information and see what happens!

 
$toc.Update()

You won’t actually see anything return after you run this, so it is assumed that if no errors occurred, then it at least made the attempt to catalog all of the heading styles and constructed a table of contents. In this case, it worked like a champ and we can now see the table of contents listed at the top of the document.

image

I have truncated the rest of the output shown here as it spans a couple of pages to support the five pages of heading information, but you can get the idea of what it can do. This provides a great way to add some additional configuration to a report that is generated via a script which is especially useful if you have large amounts of data that covers a lot of pages and helps whoever views it to navigate more quickly to the information that they may need to see.

Hopefully you find this information useful in your future Word reporting needs!

Posted in powershell | Tagged , , , | Leave a comment

Beginning with PowerShell and Word

This first article will dip our toes into creating the Word Com object, looking at sending text to Word and adjusting some of the various fonts and styles to give you a stepping stone on what you can do. I will be using Word 2013 in this article, so your mileage may vary if using older versions.

Creating the Word Object

Like any COM object, we must use New-Object with the –ComObject parameter to tell PowerShell that we are loading up a Com object.

 
$Word = New-Object -ComObject Word.Application

Now I have created my object as shown in the image below.

image

One thing that you might note is that we can’t see Word opened up, even though the process is actually active.

image

To make Word visible, we have to set the Visibility of the object to $True.

 
$Word.Visible = $True

Now you should see that Word is visible. But we don’t actually have a document loaded yet to start writing. We need to fix that first. We do this by calling the Documents.Add() method. I am keeping the output of this saved to another variable that will be used later on. I then want to select the current pane in Word so I can begin writing data to it.

 
$Document = $Word.Documents.Add()
$Selection = $Word.Selection

Writing to Word

Writing to word is as simple as calling TypeText() and supplying a parameter which is text.

 
$Selection.TypeText("Hello")

image

Ok, nothing really mind blowing here, but this is how we can write in word using PowerShell. If I attempt to do this again, it will not actually start on the next line but append to the existing line.

image

We work around this by calling TypeParagraph().

 
$Selection.TypeParagraph()
$Selection.TypeText("Hello1")
$Selection.TypeParagraph()
$Selection.TypeParagraph()
$Selection.TypeText("Hello2")
$Selection.TypeParagraph()
$Selection.TypeParagraph()
$Selection.TypeText("Hello3")
$Selection.TypeParagraph()

image

Working with Styles

So lets start with a fresh slate (actually I am just going to delete all of the words here instead of rebuilding the object) and look at some of the Styles that we can use.

First off, we should see what kind of styles are available to use.

 
[Enum]::GetNames([Microsoft.Office.Interop.Word.WdBuiltinStyle]) | ForEach {
    [pscustomobject]@{Style=$_}
} | Format-Wide -Property Style -Column 4

image

This is just one of the possible options to set a style that also includes using an integer or a string value that matches the names of what you would see in the toolbar for styles.

image

Whatever approach you are comfortable with will be just fine. Just make sure if it the setting isn’t that readable (such as using an integer) that you comment what the style is supposed to be for whoever might be looking at the code later on.

Moving on from that, we will set a couple of styles to show off what we can do. I think we at least need a Title and perhaps some sort of Header to show off what we are doing.

 
$Selection.Style = 'Title'
$Selection.TypeText("Hello")
$Selection.TypeParagraph()
$Selection.Style = 'Heading 1'
$Selection.TypeText("Report compiled at $(Get-Date).")
$Selection.TypeParagraph()

image

Feel free to explore and find some styles that will work with what you are trying to do!

Exploring Fonts

The last item that I will cover today is working with Fonts such as changing the color and going with Bold or Italics.

We can locate the fonts using $Selection.Font

 
$Selection.Font

image

Ok, there is obviously a lot of stuff available here that we can manipulate, but I just focus on the basics. Notice that the Bold and Italic are specified by an integer. Currently, a 0 means that this is turned off. If we want to turn it on, we specify something other than a 0, such as a 1.

 
$Selection.Font.Bold = 1
$Selection.TypeText('This is Bold')
$Selection.Font.Bold = 0
$Selection.TypeParagraph()
$Selection.Font.Italic = 1
$Selection.TypeText('This is Italic')

image

Lastly, we will check out adjusting the colors of the text in word and we can find out the available colors using the following code:

 
[Enum]::GetNames([Microsoft.Office.Interop.Word.WdColor]) | ForEach {
    [pscustomobject]@{Color=$_}
} | Format-Wide -Property Color -Column 4

image

Now we can write some code to take a look at some of these colors.

 
[Enum]::GetNames([Microsoft.Office.Interop.Word.WdColor]) | ForEach {
    $Selection.Font.Color = $_
    $Selection.TypeText("This is $($_)")
    $Selection.TypeParagraph()    
} 
$Selection.Font.Color = 'wdColorBlack'
$Selection.TypeText('This is back to normal')

SNAGHTML48723c4d

Actually, I decided to just go through all of the colors so we can see each and every one!

One last thing! It may be a good idea to save our good work so we can view it later, right? So with that we can use the following code to save the document. We do this using the SaveAs method in the $Document object that we had created earlier. Aren’t you glad we saved that output earlier?

We do need to specify a reference variable which is the path and name of the file as well as a second parameter specifying the save format of the document. as…you guest it…another reference variable! We can find the possible format types using this code.

 
[Enum]::GetNames([microsoft.office.interop.word.WdSaveFormat])

image

And now I can save this document using the following code:

 
$Report = 'C:\Users\proxb\Desktop\ADocument.doc'
$Document.SaveAs([ref]$Report,[ref]$SaveFormat::wdFormatDocument)
$word.Quit()

I am calling the Quit() method to actually quit the application. Unfortunately, this doesn’t actually mean that the memory used to create this object has been freed up. Com objects play a little differently than .Net objects that we create. So with that in mind, I am going to do the following to free up that memory:

 
$null = [System.Runtime.InteropServices.Marshal]::ReleaseComObject([System.__ComObject]$word)
[gc]::Collect()
[gc]::WaitForPendingFinalizers()
Remove-Variable word 

There you go! That was a little dip into the pool of PowerShell and Word to get you going on building out something cool!

Posted in powershell | Tagged , , | 3 Comments

Retrieving a Registry Key LastWriteTime Using PowerShell

While navigating through the registry, you may have noticed that there is something missing from there that you normally see in other places such as the file system. That little thing is a timestamp that might tell you when a particular key has been modified. In this case, you can’t see it because it is not openly visible to be seen. In fact, the timestamp is only available on the registry key itself, not on the values within the key. If a value gets updated, removed or added under the key, the timestamp on the key gets updated.

In order to actually view this information you would have to export the registry key in question and make sure to save it as a txt file so that when you open it up, it would list the lastwritetime of the key for you to see.

image

Now wouldn’t be great if we could somehow use some sort of command line approach to retrieving the same timestamp? Well, with a little bit of work, we can accomplish this using PowerShell with some help from our friends with p/invoke and some reflection!

First let’s start off with getting the proper signature which happens to be RegQueryInfoKey from pinvoke.net

image

More information about this actual function can be found at the following link:

http://msdn.microsoft.com/en-us/library/windows/desktop/ms724902%28v=vs.85%29.aspx

I will be making a small change to this with the last parameter: lpftLastWriteTime. Instead of an IntPtr I will be using long as the Ref so it will be easier to convert the value to a DateTime object.

With that, I am going to build out the signature using reflection. Unlike some of my other scripts that use this approach, I only have to build the RegQueryInfoKey signature and have no need to worry about any Enums or Structs.

 
#region Create Win32 API Object
Try {
    [void][advapi32]
} Catch {
    #region Module Builder
    $Domain = [AppDomain]::CurrentDomain
    $DynAssembly = New-Object System.Reflection.AssemblyName('RegAssembly')
    $AssemblyBuilder = $Domain.DefineDynamicAssembly($DynAssembly, [System.Reflection.Emit.AssemblyBuilderAccess]::Run) # Only run in memory
    $ModuleBuilder = $AssemblyBuilder.DefineDynamicModule('RegistryTimeStampModule', $False)
    #endregion Module Builder
 
    #region DllImport
    $TypeBuilder = $ModuleBuilder.DefineType('advapi32', 'Public, Class')
 
    #region RegQueryInfoKey Method
    $PInvokeMethod = $TypeBuilder.DefineMethod(
        'RegQueryInfoKey', #Method Name
        [Reflection.MethodAttributes] 'PrivateScope, Public, Static, HideBySig, PinvokeImpl', #Method Attributes
        [IntPtr], #Method Return Type
        [Type[]] @(
            [Microsoft.Win32.SafeHandles.SafeRegistryHandle], #Registry Handle
            [System.Text.StringBuilder], #Class Name
            [UInt32 ].MakeByRefType(),  #Class Length
            [UInt32], #Reserved
            [UInt32 ].MakeByRefType(), #Subkey Count
            [UInt32 ].MakeByRefType(), #Max Subkey Name Length
            [UInt32 ].MakeByRefType(), #Max Class Length
            [UInt32 ].MakeByRefType(), #Value Count
            [UInt32 ].MakeByRefType(), #Max Value Name Length
            [UInt32 ].MakeByRefType(), #Max Value Name Length
            [UInt32 ].MakeByRefType(), #Security Descriptor Size           
            [long].MakeByRefType() #LastWriteTime
        ) #Method Parameters
    )
 
    $DllImportConstructor = [Runtime.InteropServices.DllImportAttribute].GetConstructor(@([String]))
    $FieldArray = [Reflection.FieldInfo[]] @(       
        [Runtime.InteropServices.DllImportAttribute].GetField('EntryPoint'),
        [Runtime.InteropServices.DllImportAttribute].GetField('SetLastError')
    )
 
    $FieldValueArray = [Object[]] @(
        'RegQueryInfoKey', #CASE SENSITIVE!!
        $True
    )
 
    $SetLastErrorCustomAttribute = New-Object Reflection.Emit.CustomAttributeBuilder(
        $DllImportConstructor,
        @('advapi32.dll'),
        $FieldArray,
        $FieldValueArray
    )
 
    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)
    #endregion RegQueryInfoKey Method
 
    [void]$TypeBuilder.CreateType()
    #endregion DllImport
}
#endregion Create Win32 API object

If you are interested in a more detailed explanation of this process, just check out one of my other articles with the reflection tag to learn more.

I can verify that I at least have everything compiled correctly by looking at the method.

 
[advapi32]::RegQueryInfoKey

image

At least this looks good. We now need to grab a registry key and then figure out what the timestamp is.

 
$RegistryKey = Get-Item 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\IniFileMapping\Autorun.inf'
$RegistryKey

image

What better key to use than the one that I showed in my previous example. Now I will create some common variables that will be used with the method.

 
#region Constant Variables
$ClassLength = 255
[long]$TimeStamp = $null
#endregion Constant Variables

With my registry key, I need the registry handle and the name that will be supplied to the method.

 
$ClassName = New-Object System.Text.StringBuilder $RegistryKey.Name
$RegistryHandle = $RegistryKey.Handle

I had to use a StringBuilder because it is required by one of the method parameters. Now that we have enough information, we can proceed with gathering the timestamp.

 
[advapi32]::RegQueryInfoKey(
    $RegistryHandle,
    $ClassName,
    [ref]$ClassLength,
    $Null,
    [ref]$Null,
    [ref]$Null,
    [ref]$Null,
    [ref]$Null,
    [ref]$Null,
    [ref]$Null,
    [ref]$Null,
    [ref]$TimeStamp
)

SNAGHTML5719a54

Most of the parameters here can be $Null, which is why they are that way. We know that this was successful by the return of the IntPtr 0. Any other value would mean that something happened and that would need to be investigated.

We are not quite done yet! We have the timestamp, but it is in a non-usable format and should be converted into a DateTime object using [datetime]::FromFileTime().

 
#Convert to DateTime Object
$LastWriteTime = [datetime]::FromFileTime($TimeStamp)
 
#Return object
$Object = [pscustomobject]@{
    FullName = $RegistryKey.Name
    Name = $RegistryKey.Name -replace '.*\\(.*)','$1'
    LastWriteTime = $LastWriteTime
}
$Object.pstypenames.insert(0,'Microsoft.Registry.Timestamp')
$Object

image

If you compare this to what I showed earlier with the manual exporting of the registry key to a text file, you will see that these times are exactly the same. Well, this also includes the seconds, which the exported file does not.

Now wouldn’t it be nice to have a function that does all of this work for you? Of course you would! I wrote a function called Get-RegistryKeyLastWriteTime that will allow you to get the registry key timestamp from either a remote or local system.

Let’s give it a couple of runs to see how it works. The first example shows how you can pipe an existing registry key object into the function to get the timestamp.

 
$RegistryKey = Get-Item "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\IniFileMapping\Autorun.inf"
$RegistryKey | Get-RegistryKeyTimestamp | Format-List

image

The next example runs against a remote system and allows you to specify the hive and subkey that you wish to query.

 
Get-RegistryKeyTimestamp -Computername boe-pc -RegistryHive LocalMachine –SubKey  'SOFTWARE\Microsoft\Windows NT\CurrentVersion\IniFileMapping\Autorun.inf' |
Format-List

image

Works like a champ! Be sure to download this and give it a run and let me know what you think!

Download Get-RegistryKeyLastWriteTime

https://gallery.technet.microsoft.com/scriptcenter/Get-RegistryKeyLastWriteTim-63f4dd96

Posted in powershell | Tagged , , , , , | Leave a comment