I just read a neat blog post entitled PowerShelling Audit Reports over on the TenBrink Tech blog. This blog post sets out three scripts: GetRecursiveGroupMembership.ps1, Audit-QuickGroup.ps1, and Audit-MultipleGroups.ps1. The second and third make use of the first. They make use of Quests’s AD tools to create CSV file(s) containing details of members in an AD Group.
Scripts to Modules
Dillon (the post’s author) has implemented all three of these as separate PS1 files. Which is so Version 1. :-)!! As I read through his scripts, I could not escape the felling that a much better approach would be to implement them as a single module with PowerShell V2. So I did! It took a bit more time than I’d hoped, but it helped me to learn a bit more about modules in PowerShell V2.
Creating a Module
I first created a module file (audit.psm1) which contained the three functions Dillon created. These were slightly modified to be functions rather than script files, but the resulting module is essentially identical to the original. I also created a module manifest, which describes the module, and indicates the functions the module should expose to the user once the module is imported. I felt the first function was a helper function so the module only exports two functions not all three.Of course, I could be wrong, but it did make an interesting challenge – just exporting two of the three functions using a manifest.
Here’s the PSM1 Module itself:
- #
- Write-Host "Importing Module Audit.psm1"
- function Get-RecursiveGroupMembership {
- <#
- .SYNOPSIS
- Gets membership of a group.
- .DESCRIPTION
- Uses recursion to handle nested groups
- .NOTES
- File Name : Audit.psm1
- Author : Dillon (@tealshark on Twitter)
- Updated by : Thomas Lee tfl@psp.co.uk
- Requires : PowerShell V2 CTP3
- This is a helper function and not exported by the module.
- .LINK
- http://www.pshscripts.blogspot.com
- http://tenbrink.us/index.php/2009/01/03/powershelling-audit-reports/
- .PARAMETER DistinguishedName
- This paramater is the DN of the group you want to expand
- .PARAMETER AddOtherTypes
- This parameter adds other types to the search
- #>
- param (
- [Parameter(Position=0, Mandatory=$TRUE, ValueFromPipeline=$TRUE)]
- [string] $distinguishedname,
- [Parameter(Position=1, Mandatory=$FALSE, ValueFromPipeline=$FALSE)]
- [bool] $addOtherTypes = $false
- )
- # Start of function
- $members = @()
- $this = (Get-QADGroup $distinguishedname).member | Get-QADObject
- $this | foreach {
- if ($_.type -eq ‘user’) {
- $members += $_
- }
- elseif ($_.type -eq ‘group’) {
- Write-Host "Adding sub group $_"
- $members += Get-RecursiveGroupMembership $_.dn $addOtherTypes
- }
- else {
- if ($addOtherTypes -eq $true) {
- $members += $_
- }
- else {
- Write-Host "Non user/group member detected. Not added. Use -addOtherTypes flag to add."
- }
- }
- }
- return $members
- }
- function Audit-QuickGroup {
- <#
- .SYNOPSIS
- This function takes the distinguishedName of a group in any domain and writes
- the results of that group membership to a csv file of the same name.
- .DESCRIPTION
- This script uses get-recursivegroupmembership function to get the group membership
- .NOTES
- File Name : audit.psm1
- Author : Dillon (@tealshark on Twitter)
- Updated by : Thomas Lee tfl@psp.co.uk
- Requires : PowerShell V2 CTP3
- This function is exported
- .LINK
- http://www.pshscripts.blogspot.com
- http://tenbrink.us/index.php/2009/01/03/powershelling-audit-reports/
- .EXAMPLE
- .PARAMETER Name
- Disginguished name of a group whose membership the script will ascertain.
- #>
- param (
- [Parameter(Position=0, Mandatory=$TRUE, ValueFromPipeline=$TRUE)]
- [string] $Name
- )
- # Start of Function
- $Csvdata = Get-RecursiveGroupMembership $name | select name,type,dn,title,office,description | convertto-csv -NoTypeInformation
- $Filename = $Name + ".csv"
- [String]$Reportdate = "Report Generated: " + [datetime]::Now
- $f = new-item -itemtype file $filename
- add-content $f "Audit Report - Active Directory Group - $name"
- add-content $f $reportdate
- add-content $f $csvdata
- }
- function Audit-MultipleGroups {
- <#
- .SYNOPSIS
- Gets membership of multiple groups
- .DESCRIPTION
- This function uses the filtering abilities of the Quest Get-QADGroup cmdlet to
- get the membership of multiple groups. These are then written to multiple files.
- .NOTES
- File Name : get-autohelp.ps1
- Author : Dillon (@tealshark on Twitter),
- Updated by : Thomas Lee tfl@psp.co.uk
- Requires : PowerShell V2 CTP3
- This function is exported
- .LINK
- http://www.pshscripts.blogspot.com
- .EXAMPLE
- Left as an exercise for the reader
- .PARAMETER GroupInput
- The groups you want to audit.
- #>
- param (
- [Parameter(Position=0, Mandatory=$TRUE, ValueFromPipeline=$TRUE)]
- [string] $GroupInput
- )
- # Start of function
- # First get groups
- $GroupList = get-qadgroup $groupinput
- # Iterate through groups, creating output
- foreach ($Group in $GroupList) {
- Write-Host $group.dn
- $GroupMembers = Get-RecursiveGroupMembership $group.DN | select name,type,dn,title,office,description | convertto-csv -NoTypeInformation
- #now create file
- $filename = $Group.Name + ".csv"
- [String]$reportdate = "Report Generated: " + [datetime]::Now
- $file = New-Item -ItemType file $filename -Force
- Add-Content $file "Audit Report - Active Directory Group Membership"
- Add-Content $file $reportDate
- Add-Content $file $groupMembers
- }
- }
- # End of Module
Creating the Manifest
I used the New-ModuleManifest cmdlet to produce the basic module manifest, the .psd1 file. I then did a bit of editing with PowerSHell plus to achieve this final manifest:
- # Module manifest for module 'audit'
- # Generated by: Thomas Lee
- @{
- # These modules will be processed when the module manifest is loaded.
- NestedModules = 'Audit.psm1'
- # This GUID is used to uniquely identify this module.
- GUID = '5eed72f9-5f1d-4819-973c-63f80ccee415'
- # The author of this module.
- Author = 'Thomas Lee (tfl@psp.co.uk), with functions by dillon.'
- # The company or vendor for this module.
- CompanyName = 'PS Partnership'
- # The copyright statement for this module.
- Copyright = '(c) PS Partnership 2009'
- # The version of this module.
- ModuleVersion = '1.0'
- # A description of this module.
- Description = 'This module is a packaging of audit scripts by Dillon into a single module.'
- # The minimum version of PowerShell needed to use this module.
- PowerShellVersion = '2.0'
- # The CLR version required to use this module.
- CLRVersion = '2.0'
- # Functions to export from this manifest.
- ExportedFunctions = ('Audit-QuickGroup', 'Audit-MultipleGroups')
- # Aliases to export from this manifest.
- ExportedAliases = '*'
- # Variables to export from this manifest.
- ExportedVariables = '*'
- # Cmdlets to export from this manifest.
- ExportedCmdlets = '*'
- # This is a list of other modules that must be loaded before this module.
- RequiredModules = @()
- # The script files (.ps1) that are loaded before this module.
- ScriptsToProcess = @()
- # The type files (.ps1xml) loaded by this module.
- TypesToProcess = @()
- # The format files (.ps1xml) loaded by this module.
- FormatsToProcess = @()
- # A list of assemblies that must be loaded before this module can work.
- RequiredAssemblies = @()
- # Lists additional items like icons, etc. that the module will use.
- OtherItems = @()
- # Module specific private data can be passed via this member.
- PrivateData = ''
- }
The Results
It turns out that converting a set of script files, like Dillon created initially, into a module (as above) is easy. The syntax of the module file turns out to be a bit tricky, and the error messages and help text in CTP3 are woefully inadequate.
Here’s what this module looks like at runtime:
PS MyMod:\> # Note the module is not yet loaded so you get no output from Get-module
PS MyMod:\> Get-Module audit
PS MyMod:\> # So import the module, then look at module details
PS MyMod:\> Import-Module audit
Importing Module Audit.psm1
PS MyMod:\> Get-Module auditName : audit
Path : C:\Users\tfl\Documents\WindowsPowerShell\Modules\audit\audit.psd1
Description : This module is a packaging of audit scripts by Dillon into a single module.
Guid : 5eed72f9-5f1d-4819-973c-63f80ccee415
Version : 1.0
ModuleBase : C:\Users\tfl\Documents\WindowsPowerShell\Modules\audit
ModuleType : Manifest
PrivateData :
AccessMode : ReadWrite
ExportedAliases : {}
ExportedCmdlets : {}
ExportedFunctions : {[Audit-QuickGroup, Audit-QuickGroup], [Audit-MultipleGroups, Audit-MultipleGroups]}
ExportedVariables : {}
NestedModules : {Audit.psm1}PS MyMod:\> # Here – use AutoHelp feature to get help on the exported function/
PS MyMod:\> Get-Help Audit-QuickGroupNAME
Audit-QuickGroupSYNOPSIS
This function takes the distinguishedName of a group in any domain and writes
the results of that group membership to a csv file of the same name.SYNTAX
Audit-QuickGroup [-Name] [<String>] [-Verbose] [-Debug] [-ErrorAction [<ActionPreference>]] [-WarningAction [<ActionPreference>]] [-ErrorVariable [<String>]] [-WarningVariable [<String>
]] [-OutVariable [<String>]] [-OutBuffer [<Int32>]] [<CommonParameters>]DETAILED DESCRIPTION
This script uses get-recursivegroupmembership function to get the group membershipRELATED LINKS
http://www.pshscripts.blogspot.com
http://tenbrink.us/index.php/2009/01/03/powershelling-audit-reports/REMARKS
To see the examples, type: "get-help Audit-QuickGroup -examples".
For more information, type: "get-help Audit-QuickGroup -detailed".
For technical information, type: "get-help Audit-QuickGroup -full".
What I learned
This was an interesting exercise. It was pretty easy, but I did stumble a bit with the module (how I wish there has been better documentation on modules with CTP3!). Here are some of my take-aways relating to PowerShell modules in CTP3:
- Turning a set of inter-related scripts into a module is both easy, and a good thing!
- If you have a module that you want to also have a manifest with, they can both have the same file name but with different extensions (the module itself in a .psm1 file and the manifest in a pds1 file).
- To export only a subset of the functions in the .psm1 file, you use the ExportedFunctions feature in the manifest.
- To export multiple functions you enclose the set of functions as a string array inside the parenthesis. See this in line 24 above. This format was not all that obvious at first.
- You can add statements into the module that are executed when you import the module. See Line 2 in the module – this prints out a short message when you import the module. This has some great potential – thanks to Jeffrey Snover for the tip!
- If you import a module (using import-module as above) you can generally remove it using remove-module. This aids in testing!
There’s certainly room for improvement in this module. One thing that could be done would be to check to see if the Quest QAD tools were installed and issue a warning message if not (even better, if the tools are not found the script could go get them and install them for you auto-magically!). There should also be some trap or try/catch statements in the functions to better handle errors. Some auditing of the functions usage could also be implemented.
Modules are pretty cool – I hope this helps you understand them a bit better!
2 comments:
Hi Thomas,
Thanks for this - and for the other articles on modules - very useful!
You can export multiple functions from the module using "Export-ModuleMember" in the psd1 file (you don't need to do it in the manifest).
See here:
http://chrisjwarwick.spaces.live.com/
Where I've written some ISE editor functions in a module using the hints from your previous article:-)
PS. Watch out for the Get-RecursiveGroupMembership function - looking at the code there's no check for mutually nested groups (A belongs to B belongs to A) so in these cases the function will loop until it hits the recursion limit (99-ish afaik).
Cheers,
Chris
Thanks for the introduction on modules for v2 Thomas.
-Dillon
Post a Comment