Category Archives: Group Policies

PScribo for the win!

Sup’ PSHomies,

Is it just me or are there way too many cool development in the community happening right now? You know you’re addicted to PowerShell if your idea of unwinding on a Sunday evening is scripting! 😛

How I discovered PScribo

I was busy creating a server documentation script based on WMI. I’ve used sydi-server in the past, but I wanted it to be PowerShell-based for obvious reasons… 😉

My first idea was to create a well-defined XML document and use CSS to generate the HTML file. HTML had my preference. My first attempt was using the specialized XmlTextWriter object. That was only the first part, I had to do the CSS as well. It was a lot of code. At that time I didn’t make use of snapshots.

My next attempt was to use ConvertTo-HTML -Fragment -As (list,Table). This worked out pretty well for what I wanted, still it’s no PScribo. I’ve probably done three versions of this script.

As luck would have it, while working on the third version, the PSConfEu 2015 was taking place in StockHolm. I didn’t attend that one. Luckily for me, the presentations were uploaded. One presentation immediately caught my eye Documenting xxIT.

This is exactly what I wanted! HTML reporting capabilities!!! Having a Word version was a bonus! I downloaded the module and tried every example. Took me 2 hours to master the syntax. After that I never looked back!

PowerShell documentation made easy

PScribo has been brought to us by Iain Brighton.  Iain gave me some background information on how PScribo came to be:

I was helping Carl sort out a few “performance” issues with the Word functions he was using. As Carl’s documentation framework uses COM, it is painful! Here are a list of some of the issues:

  1. It’s slow
  2. When MS release a new version (or update) Word things can break
  3. There are localisation issues
  4. There is a reliance on Word actually being installed
  5. PS v5 has actually broken quite a lot of his code (although sped it up too)
  6. The code wasn’t factored particularly well, it is somewhat “better” now!

I told Carl I’d get rid of the Word dependency, improve performance and try to enforce a separation of concerns to aid maintenance. PScribo is the result of this.

So fun fact, I recently saw that Mr. Carl Webster is the guardian of Jeff Wouter’s ADHealthCheck script. A few lines into the script and I’m seeing a lot of Word configuration going on. I thought to myself this needs PScribo! In all my enthusiasm I sent a tweet to Mr Webster not knowing he already knew about PScribo 😛

Iain Twitt

Don’t I feel silly! I wasn’t trying to be cheeky or anything like that, I just thought I’d share what the advantages are of PScribo. I apologize for my ignorance. If I could just take a moment to demonstrate the awesomeness of PScribo, I’m sure you’ll be hooked as well!

Creating the ADHC snapshot

My mantra these days is to gather now and process later. I already did most of the grunt work with the Active Directory configuration snapshot. I just needed to extend it with Users, Privileged groups and Computers. With the snapshot, there’s no limitation where you get your source from. For Forest information, I gathered data using the Get-ADForest cmdlet and also [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest(). The latter also has sites,sitelinks and subnets data, which comes in handy if the Get-ADReplication* cmdlets aren’t available… Gather now, process later… 😉

Here’s what I came up with:

<#
Author: I.Strachan
Version:
Version History:
Purpose: Get Snapshot of Active Directory current Health
#>
[cmdletbinding()]
Param()
Import-Module ActiveDirectory,GroupPolicy -Verbose:$false
#region Helper Functions. Ideally this would be a module. You can also . Source an external script
Function Get-SitesStats{
#Ahsley McGlone Freaky neat AD site links
#https://blogs.technet.microsoft.com/ashleymcglone/2012/09/10/freaky-neat-active-directory-site-links-with-powershell/
Get-ADObject -LDAPFilter '(objectClass=site)' -SearchBase (Get-ADRootDSE).ConfigurationNamingContext -Properties WhenCreated, Description |
Select-Object Name,
@{label='IsEmpty';expression={If ($(Get-ADObject -Filter {ObjectClass -eq 'nTDSDSA'} -SearchBase $_.DistinguishedName)) {$false} else {$true}}},
@{label='DCCount';expression={@($(Get-ADObject -Filter {ObjectClass -eq 'nTDSDSA'} -SearchBase $_.DistinguishedName)).Count}},
@{label='SubnetCount';expression={@($(Get-ADObject -Filter {ObjectClass -eq 'subnet' -and siteObject -eq $_.DistinguishedName} -SearchBase (Get-ADRootDSE).ConfigurationNamingContext)).Count}},
@{label='SiteLinkCount';expression={@($(Get-ADObject -Filter {ObjectClass -eq 'sitelink' -and siteList -eq $_.DistinguishedName} -SearchBase (Get-ADRootDSE).ConfigurationNamingContext)).Count}},
WhenCreated,Description
}
Function Get-GPOsSoM {
#Ashley McGlone GPO Report
#https://blogs.technet.microsoft.com/ashleymcglone/2013/05/29/dude-wheres-my-gpo-using-powershell-to-find-all-of-your-group-policy-links/
BEGIN{
#region Get a list of all GPOs
$GPOs = Get-GPO -All |
Select-Object ID, Path, DisplayName, GPOStatus, WMIFilter
#endregion
#Array for GPLinks results
$gPLinks = @()
#region GPO Linked to the Domain
$domainGPO = @{
Identity = ((Get-ADDomain).distinguishedName)
Properties = @('name', 'distinguishedName', 'gPLink', 'gPOptions', 'canonicalname')
}
$gPlinks += Get-ADObject @domainGPO |
Select-Object 'name', 'distinguishedName', 'gPLink', 'gPOptions', 'canonicalname',
@{name='Depth';expression={0}}
#endregion
#region GPO Linked to OUs
$ouGPOs = @{
Filter = '*'
Properties = @('name', 'distinguishedName', 'gPLink', 'gPOptions', 'canonicalname')
}
$gPLinks += Get-ADOrganizationalUnit @ouGPOs |
Select-Object name, distinguishedName, gPLink, gPOptions ,canonicalname ,
@{name='Depth';expression={($_.distinguishedName -split 'OU=').count - 1}}
#endregion
#region GPOs linked to sites
$siteGPOs = @{
LDAPFilter = '(objectClass=site)'
SearchBase = "CN=Sites,$((Get-ADRootDSE).configurationNamingContext)"
SearchScope = 'Onelevel'
Properties = @('name', 'distinguishedName', 'gPLink', 'gPOptions', 'canonicalname')
}
$gPLinks += Get-ADObject @siteGPOs |
Select-Object name, distinguishedName, gPLink, gPOptions ,canonicalname,
@{name='Depth';expression={0}}
#endregion
#Hashtable to lookup GPOs
$lookupGPO = $GPOs | Group-Object -AsHashTable -Property 'Path'
}
PROCESS{
#Get the Scope of Management of each gPLink
ForEach ($SOM in $gPLinks) {
if ($SOM.gPLink) {
If ($SOM.gPLink.length -gt 1) {
$links = @($SOM.gPLink -split {$_ -eq '[' -or $_ -eq ']'} | Where-Object {$_})
For ( $i = $links.count - 1 ; $i -ge 0 ; $i-- ) {
$GPOData = $links[$i] -split {$_ -eq '/' -or $_ -eq ';'}
[PSCustomObject]@{
Depth = $SOM.Depth;
Name = $SOM.Name;
DistinguishedName = $SOM.distinguishedName;
canonicalName = $SOM.canonicalname;
PolicyDN = $GPOData[2];
LinkOrderNr = $links.count - $i
GUID = $lookupGPO.$($GPOData[2]).ID;
DisplayName = $lookupGPO.$($GPOData[2]).DisplayName;
GPOStatus = $lookupGPO.$($GPOData[2]).GPOStatus;
WMIFilter = $lookupGPO.$($GPOData[2]).WMIFilter.Name;
Config = $GPOData[3];
LinkEnabled = [bool](!([int]$GPOData[3] -band 1));
Enforced = [bool]([int]$GPOData[3] -band 2);
BlockInheritance = [bool]($SOM.gPOptions -band 1)
}
}
}
}
}
}
END{}
}
Function Get-PrivilegedGroups{
#Jeff Wouters script.
Param (
$Domain
)
BEGIN{
$PrivilegedGroups = @(
"$($Domain.DomainSID)-512" #Domain Admins
"$($Domain.DomainSID)-518" #Schema Admins
"$($Domain.DomainSID)-519" #Enterprise Admins
"$($Domain.DomainSID)-520" #Group Policy Creatr Owners
'S-1-5-32-544' #Builtin\Administrators
'S-1-5-32-548' #Builtin\Account Operators
'S-1-5-32-549' #Builtin\Server Operators
'S-1-5-32-550' #Builtin\Print Operators
'S-1-5-32-551' #Builtin\Backup Operators
'S-1-5-32-552' #Builtin\Replicators
'S-1-5-32-556' #Builtin\Network Configuration Operations
'S-1-5-32-557' #Builtin\Incoming Forest Trust Builders
'S-1-5-32-573' #Builtin\Event Log Readers
'S-1-5-32-578' #Builtin\Hyper-V Administrators
'S-1-5-32-580' #Builtin\Remote Management Users
)
}
PROCESS{
$objDomainPrivilegedGroups = @{
DomainSID = $Domain.DomainSID
NETBIOSName = $Domain.Name
FQDN = $Domain.DNSRoot
}
$colPrivilegedGroups = @()
foreach($group in $PrivilegedGroups){
$colPrivilegedGroups += Get-ADGroup -Identity $group -Properties Members,MemberOf
}
$objDomainPrivilegedGroups.Groups = $colPrivilegedGroups
[PSCustomObject]$objDomainPrivilegedGroups
}
END{}
}
#endregion
$snapshot = @{
ADDS = @{}
Users = @{}
Groups = @{}
GPOs = @{}
Computers = @{}
}
#region: snapshotADDS
$snapshot.ADDS.RootDSE = $(Get-ADRootDSE)
$snapshot.ADDS.Forest = $(Get-ADForest)
$snapshot.ADDS._Forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
$snapshot.ADDS.Domain = $(Get-ADDomain)
$snapshot.ADDS._Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
$snapshot.ADDS.DomainControllers = $(Get-ADDomainController -Filter *)
$snapshot.ADDS.DomainTrusts = (Get-ADTrust -Filter *)
$snapshot.ADDS.DefaultPassWordPoLicy = $(Get-ADDefaultDomainPasswordPolicy)
$snapshot.ADDS.AuthenticationPolicies = $(Get-ADAuthenticationPolicy -LDAPFilter '(name=AuthenticationPolicy*)')
$snapshot.ADDS.AuthenticationPolicySilos = $(Get-ADAuthenticationPolicySilo -Filter 'Name -like "*AuthenticationPolicySilo*"')
$snapshot.ADDS.CentralAccessPolicies = $(Get-ADCentralAccessPolicy -Filter *)
$snapshot.ADDS.CentralAccessRules = $(Get-ADCentralAccessRule -Filter *)
$snapshot.ADDS.ClaimTransformPolicies = $(Get-ADClaimTransformPolicy -Filter *)
$snapshot.ADDS.ClaimTypes = $(Get-ADClaimType -Filter *)
$snapshot.ADDS.DomainAdministrators =$( Get-ADGroup -Identity $('{0}-512' -f (Get-ADDomain).domainSID) | Get-ADGroupMember -Recursive)
$snapshot.ADDS.OrganizationalUnits = $(Get-ADOrganizationalUnit -Filter *)
$snapshot.ADDS.OptionalFeatures = $(Get-ADOptionalFeature -Filter *)
$snapshot.ADDS.Sites = $(Get-ADReplicationSite -Filter *) #Applies To: Windows 8.1, Windows PowerShell 4.0, Windows Server 2012 R2
$snapshot.ADDS.Subnets = $(Get-ADReplicationSubnet -Filter *) #Applies To: Windows 8.1, Windows PowerShell 4.0, Windows Server 2012 R2
$snapshot.ADDS.SiteLinks = $(Get-ADReplicationSiteLink -Filter *) #Applies To: Windows 8.1, Windows PowerShell 4.0, Windows Server 2012 R2
$snapshot.ADDS.ReplicationMetaData = $(Get-ADReplicationPartnerMetadata -Target (Get-ADDomain).DNSRoot -Scope Domain)
$snapshot.ADDS.SitesStats = $(Get-SitesStats) #Courtesy of Ashley McGlone
$snapshot.ADDS.repadmin = $(repadmin.exe /showrepl * /csv | ConvertFrom-CSV) #Courtesy of Ashley McGlone
#endregion
#region snapshotGPOs
$snapshot.GPOs.GPOsSoM = Get-GPOsSoM
$snapshot.GPOs.All = Get-GPO -All | Select-Object '*'
#endregion
#region snapshotUsers
$snapshot.Users.Disabled = Search-ADAccount -AccountDisabled
$snapshot.Users.Expired = Search-ADAccount -AccountExpired
$snapshot.Users.Expiring = Search-ADAccount -AccountExpiring
$snapshot.Users.NoExpireDate = Get-ADUser -LDAPFilter '(|(accountExpires=0)(accountExpires=9223372036854775807))'
$snapshot.Users.Inactive = Search-ADAccount -AccountInactive
$snapshot.Users.NoKerberosPreAuth = get-aduser -filter * -properties DoesNotRequirePreAuth | Where-Object {$_.DoesNotRequirePreAuth}
$snapshot.Users.MustChangePassWord = Get-ADUser -Filter {pwdLastSet -eq 0}
$snapshot.Users.CannotChangePassWord = Get-ADUser -Filter * -Properties CannotChangePassword |Where-Object {$_.CannotChangePassword}
$snapshot.Users.All = Get-ADUser -Filter '*' | Select-Object '*'
#endregion
#region snapshotGroups
$snapshot.Groups.All = Get-ADGroup -Filter '*'
$snapshot.Groups.Privileged = Get-PrivilegedGroups -Domain $(Get-ADDomain)
#endregion
#region snapshotComputers
$snapshot.Computers.All = Get-ADComputer -Filter * -Properties OperatingSystem
$snapshot.Computers.Disabled = Search-ADAccount -AccountDisabled -ComputersOnly
$snapshot.Computers.Expired = Search-ADAccount -AccountExpired -ComputersOnly
$snapshot.Computers.Expiring = Search-ADAccount -AccountExpiring -ComputersOnly
#endregion
#region Export to XML. Change folder to reflect your location
$exportDate = Get-Date -Format ddMMyyyy
$snapshot | Export-Clixml .\export\adds\ADHC-$($exportDate).xml -Encoding UTF8
#endregion
#region Querying snapshot. Try each one seperately!
"`nDisabled users`n"
$snapshot.Users.Disabled | Select-Object Name
"`nExpired users`n"
$snapshot.Users.Expired | Select-Object Name
"`nExpiring users`n"
$snapshot.Users.Expiring | Select-Object Name
"`nPrivilegedGroup`n"
$snapshot.Groups.Privileged.Groups | Select-Object Name
"`nPrivilegedGroup Members count`n"
$snapshot.Groups.Privileged.Groups |
Foreach-object {
[PSCustomObject]@{
Name = $_.Name
MemberCount = @($_.Members).Count
}
} #From here you can do your own filtering
"`nGPOs`n"
$snapshot.GPOs.All | Select-Object DisplayName,ID
"`nGPOs Scope of Management Inheritance blocked`n"
$snapshot.GPOs.GPOsSoM | Select-Object Displayname,BlockInheritance,GUID
"`nMember servers`n"
$snapshot.Computers.All |
Where-Object {
($_.OperatingSystem -like '*server*') -and
(!($_.DistinguishedName -like '*OU=Domain Controllers*'))
} |
Select-Object Name
#endregion
view raw Get-ADHCSnapshot.ps1 hosted with ❤ by GitHub

The snapshot helps me concentrate on the task at hand. It’s also a great way to separate the documentation process from gathering all the Active Directory bits and pieces. This makes a huge difference in maintenance! If you need to add anything just update the snapshot.

Now for the fun part!

Creating the PScribo ADHC report

First I need to import the saved snapshot. Export-CliXml & Import-CliXml are growing on me!

#Get ADSnapshot
$ADHCSnapshot = Import-Clixml .\export\adds\ADHC-$($snapshotDate).xml

Next I need to initiate the Document. PScribo is DSL (Domain-specific Language) oriented. This makes the flow quite logical.

#region Create PScribo Document
$reportAD = Document &amp;amp;quot;ADHC snapshot report - $($snapshotDate)&amp;amp;quot; {
   GlobalOption -ForceUppercaseSection -EnableSectionNumbering -PageSize A4 -Margin 24
   BlankLine -Count 20
   Paragraph &amp;amp;quot;Active Directory Health report - $($snapshotDate)&amp;amp;quot;  -Style Title
   BlankLine -Count 20
   PageBreak
   TOC -Name 'Table of Contents'
   PageBreak

A “Document” is an object that contains one or more “sections”, TOC and Paragraphs just to name a few. Just have a look at the README.MD on github get an idea of what is possible and by all means try the examples!

To find the commands available in PScribo run

get-command -Module PScribo

Quick update: Iain just updated PScribo to include landscape page orientation!

Iain Twitt - Landscape

Now to break the document down into sections. Sections can be nested to create sub-sections much like you would do in Word with Header1, Header2, Header 3 etc. The TOC (Table of Content) will be generated according to how the sections are nested. You can also exclude a section from the TOC.

For ADHC, I started with the Forest Information

   Section -Style Heading1 'Forest Information' {
      $ADForest = [Ordered]@{
         Name = $($ADHCSnapshot.ADDS.Forest.Name)
         RootDomain = $($ADHCSnapshot.ADDS.Forest.RootDomain)
         ForestMode = $($ADHCSnapshot.ADDS.Forest.ForestMode.ToString())
         Domains = $($ADHCSnapshot.ADDS.Forest.Domains)
      }

      Table -Name 'Forest Information' -List -Width 0 -Hashtable $ADForest

For this part I created an ordered hashtable to generate a list

My favorite part is the following: Selecting the properties I want to report!

      Section -Style Heading2 'FSMO Roles' {
         $ADHCSnapshot.ADDS.Forest |
         Select-Object DomainNamingMaster,SchemaMaster |
         Table -Name 'FSMO Roles Forest' -List -Width 0

         Blankline

         $ADHCSnapshot.ADDS.Domain |
         Select-Object PDCEmulator,InfrastructureMaster,RIDMaster |
         Table -Name 'FSMO Roles Domain' -List -Width 0
      }

I demonstrated at the end of the ADHC snapshot how you can extract the information you want.

&amp;amp;quot;`nDisabled users`n&amp;amp;quot;
$snapshot.Users.Disabled | Select-Object Name

&amp;amp;quot;`nExpired users`n&amp;amp;quot;
$snapshot.Users.Expired  | Select-Object Name

&amp;amp;quot;`nExpiring users`n&amp;amp;quot;
$snapshot.Users.Expiring | Select-Object Name

To document, it’s as simple as selecting the properties and sending them down the pipeline.

Here’s where you do your formatting and filtering as well:

         Section -Style Heading3 'Privileged groups count'{

            #Create Style for Privileged Groups count greater than 5
            Style -Name PrivilegedGroupsGT5 -Color White -BackgroundColor Firebrick

            $PrivilegedGroupsGT5 = $ADHCSnapshot.Groups.Privileged.Groups |
            Foreach-object {
               [PSCustomObject]@{
                  Name = $_.Name
                  MemberCount = @($_.Members).Count
               }
            }

            #Set Style for Privileged Groups count greater than 5
            $PrivilegedGroupsGT5 | Where-object{ $_.MemberCount -gt 5} | Set-Style -Style 'PrivilegedGroupsGT5'

            Table -InputObject $PrivilegedGroupsGT5 -Name 'Privileged groups count' -Width 0
         }

For the privileged group member count I created a special style that would higlight any row where the count is greater than five.

Privileged Groups.PNG
How cool is that?!

Want a table with all the sites without a description?

         Section -Style Heading3 'Sites without a description' {
            $ADHCSnapshot.ADDS.Sites.Where{$_.Description -eq $null} |
            Select-Object Name |
            Table -Name 'Sites without a description' -Width 0
         }

Here the reporting part:

<#
Author: I.Strachan
Version: 1.0
Version History:
Purpose: Active Directory Health Check PScribo/Excel report
#>
[CmdletBinding()]
Param(
$snapshotDate= '26052016'
)
#PScribo link: https://github.com/iainbrighton/PScribo
Import-Module PScribo,ImportExcel -Verbose:$false
#ImportExcel link: https://github.com/dfinke/ImportExcel
Import-Module ImportExcel -Verbose:$false
#Get ADSnapshot
$ADHCSnapshot = Import-Clixml .\export\adds\ADHC-$($snapshotDate).xml
#region Create PScribo Document
$reportAD = Document "ADHC snapshot report - $($snapshotDate)" {
GlobalOption -ForceUppercaseSection -EnableSectionNumbering -PageSize A4 -Margin 24
BlankLine -Count 20
Paragraph "Active Directory Health report - $($snapshotDate)" -Style Title
BlankLine -Count 20
PageBreak
TOC -Name 'Table of Contents'
PageBreak
Section -Style Heading1 'Forest Information' {
$ADForest = [Ordered]@{
Name = $($ADHCSnapshot.ADDS.Forest.Name)
RootDomain = $($ADHCSnapshot.ADDS.Forest.RootDomain)
ForestMode = $($ADHCSnapshot.ADDS.Forest.ForestMode.ToString())
Domains = $($ADHCSnapshot.ADDS.Forest.Domains)
}
Table -Name 'Forest Information' -List -Width 0 -Hashtable $ADForest
Section -Style Heading2 'FSMO Roles' {
$ADHCSnapshot.ADDS.Forest |
Select-Object DomainNamingMaster,SchemaMaster |
Table -Name 'Forest FSMO Roles' -List -Width 0
Blankline
$ADHCSnapshot.ADDS.Domain |
Select-Object PDCEmulator,InfrastructureMaster,RIDMaster |
Table -Name 'Domain FSMO Roles' -List -Width 0
}
Section -Style Heading2 'Global Catalogs' {
$ADHCSnapshot.ADDS._Forest.GlobalCatalogs |
Select-Object Name |
Table -Name 'Global Catalogs' -Width 0
}
}
PageBreak
Section -Style Heading1 'Domain Information' {
$ADDomain = [Ordered]@{
NetBIOSName = $($ADHCSnapshot.ADDS.Domain.NetBIOSName)
DomainMode = $($ADHCSnapshot.ADDS.Domain.DomainMode.ToString())
DistinguishedName = $($ADHCSnapshot.ADDS.Domain.DistinguishedName)
DomainSID = $($ADHCSnapshot.ADDS.Domain.DomainSID)
}
Table -Name 'Domain Information' -List -Width 0 -Hashtable $ADDomain
Section -Style Heading2 'Domain Controllers' {
$ADHCSnapshot.ADDS.DomainControllers |
Select-Object Name,OperatingSystem,IPv4Address,Site |
Table -Name 'Domain Controllers' -List -Width 0
}
Section -Style Heading2 'Default Domain Password Policy' {
$ADHCSnapshot.ADDS.DefaultPassWordPoLicy |
Select-Object ComplexityEnabled,LockoutDuration,LockoutObservationWindow,LockoutThreshold,
MaxPasswordAge,MinPasswordAge,MinPasswordLength,PasswordHistoryCount,ReversibleEncryptionEnabled |
Table -Name 'Default Domain Password Policy' -List -Width 0
}
Section -Style Heading2 'Domain Administrators' {
$ADHCSnapshot.ADDS.DomainAdministrators |
Select-Object Name,DistinguishedName |
Table -Name 'Domain Administrators' -Width 0
}
PageBreak
Section -Style Heading2 'Organizational Units' {
$ADHCSnapshot.ADDS.OrganizationalUnits |
Select-Object Name,DistinguishedName |
Table -Name 'Organizational Units' -Width 0
}
PageBreak
Section -Style Heading2 'Groups' {
$ADHCSnapshot.Groups.Privileged |
Select-Object DomainSID,NETBIOSName,FQDN |
Table -Name 'Groups' -list -Width 0
Section -Style Heading3 'Privileged groups'{
$ADHCSnapshot.Groups.Privileged.Groups |
Foreach-Object{
[PSCustomObject]@{
Name = $_.Name
Category = $_.GroupCategory.ToString()
Scope = $_.GroupScope.ToString()
SID = $_.SID
}
} |
Table -Name 'Privileged groups' -Width 0
}
Section -Style Heading3 'Privileged groups count'{
#Create Style for Privileged Groups count greater than 5
Style -Name PrivilegedGroupsGT5 -Color White -BackgroundColor Firebrick
$PrivilegedGroupsGT5 = $ADHCSnapshot.Groups.Privileged.Groups |
Foreach-object {
[PSCustomObject]@{
Name = $_.Name
MemberCount = @($_.Members).Count
}
}
#Set Style for Privileged Groups count greater than 5
$PrivilegedGroupsGT5 | Where-object{ $_.MemberCount -gt 5} | Set-Style -Style 'PrivilegedGroupsGT5'
Table -InputObject $PrivilegedGroupsGT5 -Name 'Privileged groups count' -Width 0
}
}
}
PageBreak
Section -Style Heading1 'Sites & Subnets' {
Section -Style Heading2 'Sites' {
$ADHCSnapshot.ADDS.Sites |
Select-Object Name,Description,DistinguishedName |
Table -Name 'Sites & Subnets' -Width 0
Section -Style Heading3 'Sites without a description' {
$ADHCSnapshot.ADDS.Sites.Where{$_.Description -eq $null} |
Select-Object Name |
Table -Name 'Sites without a description' -Width 0
}
if($ADHCSnapshot.ADDS._Forest.Sites.Where{@($_.Subnets).Count -eq 0 }){
Section -Style Heading3 'Sites without a subnet' {
$ADHCSnapshot.ADDS._Forest.Sites.Where{@($_.Subnets).Count -eq 0 } |
Select-Object Name |
Table -Name 'Sites without a subnet' -Width 0
}
}
if($ADHCSnapshot.ADDS._Forest.Sites.Where{@($_.Servers).Count -eq 0 }){
Section -Style Heading3 'Sites without a DC' {
$ADHCSnapshot.ADDS._Forest.Sites.Where{@($_.Servers).Count -eq 0 } |
Select-Object Name |
Table -Name 'Sites without a DC' -Width 0
}
}
Section -Style Heading3 'Sites Stats' {
$ADHCSnapshot.ADDS.SitesStats |
Table -Name 'Sites Stats' -Width 0
}
}
Section -Style Heading2 'Sitelinks' {
$ADHCSnapshot.ADDS.SiteLinks |
Select-Object Name,Cost,ReplicationFrequencyInMinutes |
Table -Name 'Sitelinks' -Width 0
}
Section -Style Heading2 'Subnets' {
$ADHCSnapshot.ADDS.Subnets |
Select-Object Name,Site |
Table -Name 'Subnets' -Width 0
}
}
PageBreak
Section -Style Heading1 'Group Policies' {
$ADHCSnapshot.GPOs.All |
Select-Object DisplayName,Description,GPOStatus,ModificationTime |
Table -Name 'Group Policies' -Width 0
Section -Style Heading2 'Group Policy Scope of Management' {
$ADHCSnapshot.GPOs.GPOsSoM |
Select-Object DisplayName,LinkOrderNr,GPOStatus,LinkEnabled,Enforced,BlockInheritance |
Table -Name 'Group Policy Scope of Management' -Width 0
}
}
}
#endregion
#region Render report in HTML,Word & XML format
$reportAD | Export-Document -Path .\export\adds -Format Html,Word,XML
#Or save it and render later on.
$reportAD | Export-Clixml .\export\adds\ADHCRawPScriboDocument-$($snapshotDate).xml -Encoding UTF8
#endregion
#region Export Users,Group & Computers to Excel
$xlsxUserFile = ".\export\adds\ADHC - UserReport - $($snapshotDate).xlsx"
$xlsxComputerFile = ".\export\adds\ADHC - ComputerReport - $($snapshotDate).xlsx"
$xlsxGroupMembersFile = ".\export\adds\ADHC - PrivilegedMembersReport - $($snapshotDate).xlsx"
$xlsxGroupMemberOfFile = ".\export\adds\ADHC - PrivilegedMemberOfReport - $($snapshotDate).xlsx"
#Users xlsx file
foreach($entry in $ADHCSnapshot.Users.Keys){
$WorkSheetName = $entry
If($ADHCSnapshot.Users.$entry){
$ADHCSnapshot.Users.$entry |
ConvertTo-Csv -Delimiter ';' -NoTypeInformation |
ConvertFrom-Csv -Delimiter ';' |
Export-Excel -Path $xlsxUserFile -WorkSheetname $WorkSheetName -AutoSize -BoldTopRow -FreezeTopRow
}
}
#Computers xlsx file
foreach($entry in $ADHCSnapshot.Computers.Keys){
$WorkSheetName = $entry
if($ADHCSnapshot.Computers.$entry){
$ADHCSnapshot.Computers.$entry |
ConvertTo-Csv -Delimiter ';' -NoTypeInformation |
ConvertFrom-Csv -Delimiter ';' |
Export-Excel -Path $xlsxComputerFile -WorkSheetname $WorkSheetName -AutoSize -BoldTopRow -FreezeTopRow
}
}
#Groups xlsx file
$snapshot.Groups.Privileged.Groups.ForEach{
if($_.Members){
$_.Members |
ForEach-Object{
$_ | Get-ADObject |
Select-Object Name,DistinguishedName
} |
Export-Excel -Path $xlsxGroupMembersFile -WorkSheetname $_.Name -AutoSize -BoldTopRow -FreezeTopRow
}
if($_.MemberOf){
$_.MemberOf |
ForEach-Object{
$_ | Get-ADObject |
Select-Object Name,DistinguishedName
} |
Export-Excel -Path $xlsxGroupMemberOfFile -WorkSheetname $_.Name -AutoSize -BoldTopRow -FreezeTopRow
}
}
foreach($entry in $ADHCSnapshot.Computers.Keys){
$WorkSheetName = $entry
if($ADHCSnapshot.Computers.$entry){
$ADHCSnapshot.Computers.$entry |
ConvertTo-Csv -Delimiter ';' -NoTypeInformation |
ConvertFrom-Csv -Delimiter ';' |
Export-Excel -Path $xlsxComputerFile -WorkSheetname $WorkSheetName -AutoSize -BoldTopRow -FreezeTopRow
}
}
#endregion

Some cool tip & tricks courtesy of Iain

Well-defined XML File

Here’s something else you can do with PScribo: Generate a well-defined XML File!

Iain stated that there are bits of functionality missing. With a well-defined XML file you could do your own XML mapping as part of your Word document generation solution.

Export at a later time

You can export the “document” object with Export-CliXml if you wanted to generate a report later. That way you can recreate the “document” in any format, at any time! Gotta love Export-, Import-CliXml! 😉

Manipulate the object directly

It’s just a [PSCustomObject], but why mess with a good thing eh? Hehe…

Bonus: Excel reports for user, groups and computers

As a added bonus I also threw in some excel files for reporting Users, Groups and Computers using ImportExcel by  Doug Finke. Instead of exporting to csv just to cut/paste in Excel, why not just export directly to a xlsx file!

I hope this gives you an idea what the possibilities are when it comes to reporting. PScribo is awesome! Add ImportExcel to the mix and you’ve got all you need when it comes to documentation!

Hope it’s worth something to you

Ttyl,

Urv

 

Pester script to validate GPOs Scope of Management

So here’s another spin on using Pester to validate operational readiness… 😉

Group policies can be pretty tricky! Troubleshooting can be a challenge. There might be even times that you start doubting yourself. Depending on the link order of your Policies, you might not get what you expected…

Operations is dynamic, things get moved around, enabled/disabled, blocked, name it and it’s bound to happen.

How about… some way to validate your GPOs Scope of Management! Once everything is working as it should, create a validation set you can verify later on. Trust me, I’ve been there… Using Pester will definitely give you that edge…

So I improvised a little on Ashley’s McGlone’s GPO Report and made a function Get-GPOsSoM. Just be sure to save it in the same folder as Domain-GPOSoM.Tests.ps1

<#
Author: I.C.A. Strachan
Version: 1.0
Version History: Based on Ashley McGlone's Get-GPOReport. Here's a shortlink to it: http://tinyurl.com/ofpfnf4
Purpose: Get all GPOs that are linked to Domain, Sites and/or OUs
#>
Function Get-GPOsSoM {
BEGIN{
Import-Module GroupPolicy Verbose:$false
Import-Module ActiveDirectory Verbose:$false
#region Get a list of all GPOs
$GPOs = Get-GPO All |
Select-Object ID, Path, DisplayName, GPOStatus, WMIFilter
#endregion
#Array for GPLinks results
$gPLinks = @()
#region GPO Linked to the Domain
$domainGPO = @{
Identity = ((Get-ADDomain).distinguishedName)
Properties = @('name', 'distinguishedName', 'gPLink', 'gPOptions', 'canonicalname')
}
$gPlinks += Get-ADObject @domainGPO |
Select-Object 'name', 'distinguishedName', 'gPLink', 'gPOptions', 'canonicalname',
@{name='Depth';expression={0}}
#endregion
#region GPO Linked to OUs
$ouGPOs = @{
Filter = '*'
Properties = @('name', 'distinguishedName', 'gPLink', 'gPOptions', 'canonicalname')
}
$gPLinks += Get-ADOrganizationalUnit @ouGPOs |
Select-Object name, distinguishedName, gPLink, gPOptions ,canonicalname ,
@{name='Depth';expression={($_.distinguishedName -split 'OU=').count 1}}
#endregion
#region GPOs linked to sites
$siteGPOs = @{
LDAPFilter = '(objectClass=site)'
SearchBase = "CN=Sites,$((Get-ADRootDSE).configurationNamingContext)"
SearchScope = 'Onelevel'
Properties = @('name', 'distinguishedName', 'gPLink', 'gPOptions', 'canonicalname')
}
$gPLinks += Get-ADObject @siteGPOs |
Select-Object name, distinguishedName, gPLink, gPOptions ,canonicalname,
@{name='Depth';expression={0}}
#Hashtable to lookup GPOs
$lookupGPO = $GPOs | Group-Object AsHashTable Property 'Path'
}
PROCESS{
#Get the Scope of Management of each gPLink
ForEach ($SOM in $gPLinks) {
if ($SOM.gPLink) {
If ($SOM.gPLink.length -gt 1) {
$links = @($SOM.gPLink -split {$_ -eq '[' -or $_ -eq ']'} | Where-Object {$_})
For ( $i = $links.count 1 ; $i -ge 0 ; $i ) {
$GPOData = $links[$i] -split {$_ -eq '/' -or $_ -eq ';'}
[PSCustomObject]@{
Depth = $SOM.Depth;
Name = $SOM.Name;
DistinguishedName = $SOM.distinguishedName;
canonicalName = $SOM.canonicalname;
PolicyDN = $GPOData[2];
LinkOrderNr = $links.count $i
GUID = $lookupGPO.$($GPOData[2]).ID;
DisplayName = $lookupGPO.$($GPOData[2]).DisplayName;
GPOStatus = $lookupGPO.$($GPOData[2]).GPOStatus;
WMIFilter = $lookupGPO.$($GPOData[2]).WMIFilter.Name;
Config = $GPOData[3];
LinkEnabled = [bool](!([int]$GPOData[3] -band 1));
Enforced = [bool]([int]$GPOData[3] -band 2);
BlockInheritance = [bool]($SOM.gPOptions -band 1)
}
}
}
}
}
}
END{}
}

view raw
Get-GPOsSoM.ps1
hosted with ❤ by GitHub

Now for the fun part! 🙂

<#
Author: I.C.A. Strachan
Version:
Version History:
Purpose: Pester script to validate Group Polcies status and Link on Domain,Sites and OUs
#>
[CmdletBinding()]
Param()
Describe 'Group Policies Scope of Management validation' {
BeforeAll {
#region Get GPOs Producution Validation set
$gpoValidationSet = @'
DisplayName,DistinguishedName,GPOStatus,BlockInheritance,LinkEnabled,Enforced,LinkOrderNr
Default Domain Policy,"DC=pshirwin,DC=local",AllSettingsEnabled,FALSE,TRUE,FALSE,1
Default Domain Controllers Policy,"OU=Domain Controllers,DC=pshirwin,DC=local",AllSettingsEnabled,FALSE,TRUE,FALSE,1
WinRM Listeners,"OU=Servers,DC=pshirwin,DC=local",AllSettingsEnabled,FALSE,TRUE,FALSE,1
RemoteDesktop,"OU=Servers,DC=pshirwin,DC=local",AllSettingsEnabled,FALSE,TRUE,FALSE,2
Firewall,"OU=Servers,DC=pshirwin,DC=local",UserSettingsDisabled,FALSE,TRUE,TRUE,3
'@ | ConvertFrom-Csv Delimiter ','
#endregion
#Dot source Function Get-GPOsSOM.ps1
. $PSScriptRoot\Get-GPOsSOM.ps1
#Create hashtable for lookup
$lookupGPOInReport = Get-GPOsSOM | Group-Object AsHashTable Property 'DisplayName'
}
It 'GPOs Scope of Managment has been retrieved' {
$lookupGPOInReport | should not BeNullOrEmpty
}
It 'GPO validation set has been retrieved' {
$gpoValidationSet | Should not BeNullOrEmpty
}
foreach($set in $gpoValidationSet){
Context "GPO: $($set.DisplayName)" {
it "GPO $($set.DisplayName) exists" {
$lookupGPOInReport.$($set.DisplayName) | Should Not BeNullOrEmpty
}
it "GPO is linked to $($set.DistinguishedName)"{
$lookupGPOInReport.$($set.DisplayName).DistinguishedName | Should be $set.DistinguishedName
}
it "BlockInheritance: $($set.BlockInheritance)" {
$lookupGPOInReport.$($set.DisplayName).BlockInheritance | Should be $set.BlockInheritance
}
it "LinkEnabled: $($set.LinkEnabled)" {
$lookupGPOInReport.$($set.DisplayName).LinkEnabled | Should be $set.LinkEnabled
}
it "Group policy Enforced: $($set.Enforced)" {
$lookupGPOInReport.$($set.DisplayName).Enforced | Should be $set.Enforced
}
it "Group policy LinkOrder nr: $($set.LinkOrderNr)" {
$lookupGPOInReport.$($set.DisplayName).LinkOrderNr | Should be $set.LinkOrderNr
}
it "Group policy status: $($set.GPOStatus)" {
$lookupGPOInReport.$($set.DisplayName).GPOStatus | Should be $set.GPOStatus
}
}
}
}

So here’s the result:

Pester Test GPO SoM

Now Imagine someone changed your GPO link order:

Pester Test GPO Change Link Order

Run Pester test script again:

Pester Test GPO Change Link Order -Detected

No more doubt! The link order has been tampered with! This is definitely a game changer for Operations!

My new motto : “If you can automate it, you should test it” 😛

Pester for everyone!

Hope it’s worth something to you

Ttyl,

Urv

DSC vs GPO, can they play nicely together?

I have no doubt in my mind that DSC is going to be a private/public cloud infrastructure designer’s best friend.

What makes DSC a necessity? Haven’t we been doing well so far? Steven Murawski does a far better job of explaining this (just gonna paraphrase here for a minute…)

“The reason DSC is important is because the more steps involving human interaction, allows for more potential failure points.”

So minimizing human interaction is essential to minimizing failure points. That’s always a good thing. The lesser the failure points the better the process. Makes sense.

Now Group Policy has been around for quite some time. When it comes to troubleshooting and editing, gpmc.msc is your go-to tool. With Group Policies its all about managing Domains. If you’re not part of a domain, well then you’re out of luck… that is until DSC.

Ok, I’m new to blogging game so I need to be careful posting information from other sources. The DSC Resource book at powershell.org explains the DSC vs GPO quite well. Have at it 🙂

My first thought on DSC in an environment using GPO’s is… Will they get in each others way? and if so who has the last say in the matter? Troubleshooting just got a whole lot complicated! All of a sudden, I’m starting to think about refresh intervals of both GPO & DSC and how or if I should think about manipulating them… What happens if my GPO settings resets my DSC settings? Or vice versa? This is madness!!! Ok Urv, take a deep breath… Whooozaaa…

I guess DSC isn’t going to replace GPO any time soon. I found a link from Darren Mar-Elia on the subject. He made some very valid points concerning the strength and weakness of both products. DSC is excellent as a configuration management platform for Windows Servers. DSC will also work for Workstation, but Servers are the obvious target here.

So let’s not abandon the old for the shiny just yet shall we? Remember “use the right tool for the right job”

I will say this in DSC’s defense, it got me thinking about the life cycle of resources. It’s one thing to have a resource in a desired state, disposing of them also needs to be handled properly.

I guess time will tell if DSC will be the ultimate configuration tool… Learning DSC will be a good investment, Microsoft is betting everything on it…

Ttyl,

Urv

Generating random passwords

I recently did an Active Directory migration and one of the requirements was that the passwords are complex and random.

Ok… So the first thing i did was googled “Powershell Generate Random Password” because imitation is the sincerest form of flattery… 😉 Ah a hit!

Function random-password ($length = 8)
{
    $punc = 46..46
    $digits = 48..57
    $letters = 65..90 + 97..122

    # Thanks to
    # https://blogs.technet.com/b/heyscriptingguy/archive/2012/01/07/use-pow
    $password = get-random -count $length `
        -input ($punc + $digits + $letters) |
            % -begin { $aa = $null } `
            -process {$aa += [char]$_} `
            -end {$aa}

     return $password
}

Script is pretty straightforward the function random-password will generate a password of said length. Nice!

Ok so far so good. I also needed to set the complex password as the user’s Default password when logging in the first time. I just smiled and asked are you certain you want to do that? (I’ll explain later…)

Quick sidestep: The function is great at generating random passwords! I did noticed however when setting the new password I got an error at times (randomly… go figure…)

Turns out it wasn’t an error, was just that the password wasn’t totally compliant.
Then I remembered complex passwords have some requirements. So I fired up GMPC just to see what they were again…
Complexity Password Dictates that it must:

Contain characters from three of the following four categories:

1: English uppercase characters (A through Z)
2: English lowercase characters (a through z)
3: Base 10 digits (0 through 9)
4: Non-alphabetic characters (for example, !, $, #, %)

Aaaah… So that’s why it failed randomly! Some of the passwords only had two out of the four categories. Ok so I just need to make sure the password contains at least three of the four characters.

Ok I knew then and there I had to do something with regular expressions. I’ll admit regular expressions isn’t my forte. Lucky for me I have an “ Arco” :-). Arco is a colleague of mine that is the personification of the UberGeek. His idea of “fun” is reprogramming HP SmartArray controllers. He can also program in assembly… Oh and he’s a whizz at regular expressions!!!
Ok truth be told I did try figuring it out for like 3 minutes then I went straight to Arco! Don’t judge me…

Turns out that google is truly your friend! (Yeah… I know… in hindsight…) So Arco sent me this link:
http://stackoverflow.com/questions/5142103/regex-for-password-strength
Hey, why reinvent the wheel eh?

function random-password {
    param(
        $length = 8
    )
    $punc = 46..46
    $digits = 48..57
    $lowercase = 97..122
    $uppercase = 65..90
    $symbols = 35..37

    # Thanks to
    # https://blogs.technet.com/b/heyscriptingguy/archive/2012/01/07/use-pow
    do {
        $password = get-random -count ($length) `
            -input ($punc +$digits+$uppercase+$symbols+$lowercase) |
                % -begin { $aa = $null } `
                -process {$aa += [char]$_} `
                -end {$aa}
    }
    until( $password -match "^(?=.*[A-Z])(?=.*[0-9])(?=.*[a-z]).{$length}$")

    return $password
}

So I tweaked the regular expression string to have at least a character from uppercase, lowercase and numbers. symbols are just a bonus. If it meets the requirements, then return it.

The main part of the script looks like this:

foreach ($user in $csvUsers)
{
    $newUserPassWord =  random-password -length $PasswordLength

    $objRandomPW = new-object psobject -Property @{
        SamAccountName = $user.SamAccountName
        Name = $user.Name
        Password = $newUserPassWord
    }

    $arrExportRandomPW += $objRandomPW
}

$arrExportRandomPW | out-gridview

I edited it somewhat, but you get the general idea. Remember csv is my friend!

Ok, I did what was asked. Password random? Check! Complex and compliant? Check!

But…

Now I want to get into something different…

As scripters we’re eager to script! If I have to repeat anything twice I’m already thinking how should I script this.

Password are sacred. So the first thing that popped into my head is how are we going to distribute these passwords without compromising them? If I export them to a csv file and they fall into the wrong hands then what?

“We’ll email them their passwords…” Ok… Problem was that we’re also migrating their mail system to Exchange… So yeah… there’s that…

“We’ll generate Word documents containing their password…” Still tricky but ok. Whose generating the word document, I asked with a smirk on my face? “We’ll take care that…” Nice!
BTW if you’re gonna print passwords please use courier font! There’s is nothing more frustrating than trying to figure out if it’s the letter I (Irwin) or l (live) , case in point Il.

“I could also save the password encrypted in csv file, but then you’d need the key to decrypt” Hmmm… is it worth the hassle? Who’ll do the decrypting? If the key and code falls into the wrong hands then we’re back at square one!

I’ve been in a situation where I needed to report asap which accounts were enabled and didn’t log in yet. Someone got fired (Name wasn’t disclosed) and they needed to take some legal actions. So in this case having a default generic password was kinda of a big issue… At times like that adsi filter is your friend!

Here’s my take on it all. I’d like to hear from you what your thought are about this.

The safest bet is this to:

  • Enable accounts JIT (Just in time). Yes it might get a little busy at the beginning but you won’t have any rogue accounts activated.
  • Use a generic password agreed upon. The key is not to enable the account too soon or a colleague may be able to log in with mal intent. Using a generic password just helps with the logistics. I’ve seen first hand what happens when complex passwords aren’t typed in correct, accounts get locked out etc etc…  It’s a lot easier to enable JIT, use the time for AD to sync to explain the user about his new configuration.
  • Make sure the user is required to change password at logon. Just make sure to set the password ahead of time. Depending on your group policy setting you might have to wait a day.

Ok, I think that covers that. It wasn’t my intention to go on and on. Scripting is awesome but.. “With scripting comes responsibility…” Make sure your scripts do no harm as far as it’s up to you.

Hope it’s worth something to you…

Ttyl,

Urv