Monthly Archives: April 2016

Operational Readiness validation gotchas

Sup’ PSHomies,

Back from the PSConfEu 2016 in Hannover! It was awesome!!! It was great meeting so many in person! I highly recommend attending a conference if ever given the chance! Tobias did a great job organizing PSConfEU 2016!

The presentations were top notch! Two presentations I definitely wanted to follow were June Blenders’ Real world Pester TDD tests & Ravikanth Chaganti Operations Validation Framework. I enjoyed Ravikanth’s approach: a simplistic and a comprehensive test. June’s presentation was insightful! There were definitely a few aha moments for me! So I decided to re-evaluate what I had learned so far now that I’ve seen how it should be done! 😉 .

Simplistic tests

Think of simplistic tests as kicking the tires. Here’s where the obvious tests go:

  • Are the sets aligned?
  • Did the object count meet your expectation?

Things of that nature. Here are some tests to help you understand some gotcha moments.

Validating the count seems pretty straight forward right? Well, not always… I’ll explain…

When the sets to validate are identical validation is pretty straight forward. This is the best case scenario. I did one validation with and without Group-Object (I’ll explain later on).

Simplistic Identical sets - Count

Simplistic Indentical sets

By using Group-Object I can get the “real” count of a set. Group-Object will gauge the uniqueness of the set, but I also found a test where that might not always be useful… So my next test was to omit an entry from the Verify set

Simplistic Missing entry sets - Count

Simplistic missing an entry in Verify

The test failed as it should. Now for some fun, let’s add a double entry to the Verify set.

 

Simplistic Double entry sets - CountThis is an odd test, it could be a typo. Imagine having a list where double entries aren’t that obvious, this should catch it. Now ideally you’d use your source code that you used during deployment. I’m assuming you did automate your process eh? 😉 Without the Group-Object the count is identical. With Group-Object you only have one entry! This could explain an exception happened during deployment… Accidents will happen…

The last test is a fun one: different sets.

Simplistic Different sets - Count

Different sets

Surprise! Both validation count test passed, but the sets are totally different!

Bonus Test!

Simplistic Different sets double entry - Count

Different set with a double entry

This one almost got away. While doing the comprehensive test, it dawned on me that I should be testing both counts, with and without Group-Object. I updated the screenshot accordingly.

Take away Simplistic test:

Don’t only rely the count of a set. By using Group-Object you can gauge a set’s uniqueness. The bonus test showed that exceptions may happen. It’s totally valid as a starting point, that’s why it’s a simplistic tests. Here’s the code for the Simplistic test for count validation:

$savedADConfig= @{
   GlobalCatalogs = @(
      'DC-DSC-01.pshirwin.local'
      'DC-DSC-02.pshirwin.local'
   )
}

$verifyADConfig= @{
   GlobalCatalogs = @(
      'DC-DSC-01.pshirwin.local'
      'DC-DSC-02.pshirwin.local'
   )
}

#region Example Operational validation Simplistic test
Describe 'Active Directory configuration operational readiness' {
   Context 'Verifying GlobalCatalogs count without Group-Object'{
      it 'Total GlobalCatalogs match' {
         @($savedADConfig.GlobalCatalogs).Count |
         Should be @($verifyADConfig.GlobalCatalogs).Count
      }
   }

   Context 'Verifying GlobalCatalogs count with Group-Object'{
      it 'Total GlobalCatalogs match' {
         @($savedADConfig.GlobalCatalogs  | Group-Object).Count |
         Should be @($verifyADConfig.GlobalCatalogs | Group-Object).Count
      }
   }
}
#endregion

If you’d like to try out the simplistic tests just add/remove entries to the saved-/verifyADConfig sets. Now for the Comprehensive tests!

Comprehensive tests

Here’s where in-depth analysis goes. When I did the AD Operational Readiness test, I had a feeling I was missing something. I saw June using sort-object in one of her validations. That triggered me to re-valuate this test.

Comprehensive Identical sets

Comprehensive Identical sets

Depending on which set you used for your enumeration you could end up with different results. When the sets are identical, all goes well. Next test, omit an entry in Verify set.

Comprehensive Missing an entry

Comprehensive missing an entry

Enumerating from the saved test caught the missing entry, enumerating from verify didn’t. Both found ‘DC-DSC-01.pshirwin.local’. The simplistic test caught this, that is why you need both! Next up: Double entry in Verify.

Comprehensive Double entry

Comprehensive double entry in verify

Enumerating from the saved set caught the double entry. Enumerating from the verify set just enumerated the entry twice. If you’re visually inclined, you might miss this.

Hey all my tests results are green and purple! Yeah…

Last test: Different sets.

Comprehensive Different sets

Different sets

At this point you’re comparing apple with oranges. This should fail.

Take away comprehensive tests:

The set you’re enumerating from matters! To cover validation, best bet is to do both! Here’s the code for the Comprehensive Test:

#region Example Operational validation Comprehensive test
$savedADConfig= @{
   GlobalCatalogs = @(
      'DC-DSC-01.pshirwin.local'
      'DC-DSC-02.pshirwin.local'
   )
}

$verifyADConfig= @{
   GlobalCatalogs = @(
      'DC-DSC-01.pshirwin.local'
      'DC-DSC-02.pshirwin.local'
   )
}

Describe 'Active Directory configuration operational readiness' {
   Context 'Verifying GlobalCatalogs enumerating from saved configuration'{
      $savedADConfig.GlobalCatalogs |
      ForEach-Object{
         it "Server $($_) is a GlobalCatalog"{
            $verifyADConfig.GlobalCatalogs.Contains($_) |
            Should be $true
         }
      }
   }
   Context 'Verifying GlobalCatalogs enumerating from verify configuration'{
      $verifyADConfig.GlobalCatalogs |
      ForEach-Object{
         it "Server $($_) is a GlobalCatalog"{
            $savedADConfig.GlobalCatalogs.Contains($_) |
            Should be $true
         }
      }
   }
}
#endregion

Validation is great, but you need to make sure your validating with the right set in the right order!

Summary

  • Create Simplistic & Comprehensive tests.
  • Simplistic tests should take care of the obvious.
  • Comprehensive tests is where in-depth analysis takes place.
  • Validate by enumerating from both sets!
  • Never trust a test that doesn’t fail 😉

I’m glad I visited both presentations! Now it’s time to update my Operational readiness tests accordingly! 🙂

Hope it’s worth something to you

Ttyl,

Urv

Active Directory Operations Test

‘Sup PSHomies,

Last blog I demonstrated how to create a HTML report from the Active Directory configuration snapshot. Here’s yet another way to get more use out of the Active Directory configuration snapshot.

I started out with the intention of reporting, then it hit me, why not use the snapshot for Operation readiness? Let’s dive in, I’ll explain as we go along…

Before we get started, you’ll need to have your Active Directory specification at hand. Modify $ADConfiguration according to your specifications.

#region Active Directory configuration as you expect it to be. Modify to reflect your Active Directory
$ADConfiguration = @{
    Forest = @{
        FQDN = 'pshirwin.local'
        ForestMode = 'Windows2012R2Forest'
        GlobalCatalogs = @(
            'DC-DSC-01.pshirwin.local'
        )
        SchemaMaster = 'DC-DSC-01.pshirwin.local'
        DomainNamingMaster = 'DC-DSC-01.pshirwin.local'

    }
    Domain = @{
        NetBIOSName = 'PSHIRWIN'
        DomainMode = 'Windows2012R2Domain'
        RIDMaster = 'DC-DSC-01.pshirwin.local'
        PDCEmulator = 'DC-DSC-01.pshirwin.local'
        InfrastructureMaster = 'DC-DSC-01.pshirwin.local'
        DistinguishedName = 'DC=pshirwin,DC=local'
        DNSRoot = 'pshirwin.local'
        DomainControllers = @(
            'DC-DSC-01'
        )
    }
    PasswordPolicy = @{
        PasswordHistoryCount = 24
        LockoutThreshold = 0
        LockoutDuration = '00:30:00'
        LockoutObservationWindow = '00:30:00'
        MaxPasswordAge = '42.00:00:00'
        MinPasswordAge = '1.00:00:00'
        MinPasswordLength = 8
        ComplexityEnabled = $true
    }
    Sites = @('Default-First-Site-Name')
    SiteLinks = @(
       @{
            Name = 'DEFAULTIPSITELINK'
            Cost = 100
            ReplicationFrequencyInMinutes = 180
        }
    )
    SubNets = @()
}
#endregion

Quick sidestep, we’re in the middle of implementing a new Infrastructure for a customer. Some post configuration had to be done, FSMO roles rearranged, Global catalogs etc. etc., you know the drill. I got my hand on the Active Directory specifications and filled it in. I did a AD configuration snapshot and was now ready to compare. My colleagues were in the middle of post configuring Active Directory. I noticed that the FSMO roles weren’t as expected. I was missing a Domain Controller and some Sites, subnets and sitelinks. I did a AD snapshot the next day, ran my operation readiness test and surprise, everything was as expected! It wasn’t my intention to supervise my colleagues, but I could give them the good news that the Active Directory is configured as specified.

To give you an idea of what to expect, I did the operation readiness test on my lab. Here’s the script:

<#
Author: I.C.A. Strachan
Version: 1.1
Version History:
08-04-2016 1.0 - First Release
12-05-2016 1.1 - Fixed issues with Sitelinks & Subnets.
Purpose: Pester script to validate Active Directory configuration.
#>
[CmdletBinding()]
Param(
$xmlFile = 'ADReport-12052016.xml'
)
#region Active Directory configuration as you expect it to be. Modify to reflect your AD
$ADConfiguration = @{
Forest = @{
FQDN = 'pshirwin.local'
ForestMode = 'Windows2012R2Forest'
GlobalCatalogs = @(
'DC-DSC-01.pshirwin.local'
)
SchemaMaster = 'DC-DSC-01.pshirwin.local'
DomainNamingMaster = 'DC-DSC-01.pshirwin.local'
}
Domain = @{
NetBIOSName = 'PSHIRWIN'
DomainMode = 'Windows2012R2Domain'
RIDMaster = 'DC-DSC-01.pshirwin.local'
PDCEmulator = 'DC-DSC-01.pshirwin.local'
InfrastructureMaster = 'DC-DSC-01.pshirwin.local'
DistinguishedName = 'DC=pshirwin,DC=local'
DNSRoot = 'pshirwin.local'
DomainControllers = @('DC-DSC-01')
}
PasswordPolicy = @{
PasswordHistoryCount = 24
LockoutThreshold = 0
LockoutDuration = '00:30:00'
LockoutObservationWindow = '00:30:00'
MaxPasswordAge = '42.00:00:00'
MinPasswordAge = '1.00:00:00'
MinPasswordLength = 8
ComplexityEnabled = $true
}
Sites = @('Default-First-Site-Name','Branch01')
SiteLinks = @(
[PSCustomObject]@{
Name = 'DEFAULTIPSITELINK'
Cost = 100
ReplicationFrequencyInMinutes = 180
}
)
SubNets = @(
[PSCustomObject]@{
Name = '192.168.0.0/24'
Site = 'CN=Branch01,CN=Sites,CN=Configuration,DC=pshirwin,DC=local'
}
)
}
#endregion
#Import saved AD snapshot
$SavedADReport = Import-Clixml .\export\dsa\$xmlFile
Describe 'Active Directory configuration operational readiness' {
Context 'Verifying Forest Configuration'{
it "Forest FQDN $($ADConfiguration.Forest.FQDN)" {
$ADConfiguration.Forest.FQDN |
Should be $SavedADReport.ForestInformation.RootDomain
}
it "ForestMode $($ADConfiguration.Forest.ForestMode)"{
$ADConfiguration.Forest.ForestMode |
Should be $SavedADReport.ForestInformation.ForestMode.ToString()
}
}
Context 'Verifying GlobalCatalogs'{
$ADConfiguration.Forest.GlobalCatalogs |
ForEach-Object{
it "Server $($_) is a GlobalCatalog"{
$SavedADReport.ForestInformation.GlobalCatalogs.Contains($_) |
Should be $true
}
}
}
Context 'Verifying Domain Configuration'{
it "Total Domain Controllers $($ADConfiguration.Domain.DomainControllers.Count)" {
$ADConfiguration.Domain.DomainControllers.Count |
Should be @($SavedADReport.DomainControllers).Count
}
$ADConfiguration.Domain.DomainControllers |
ForEach-Object{
it "DomainController $($_) exists"{
$SavedADReport.DomainControllers.Name.Contains($_) |
Should be $true
}
}
it "DNSRoot $($ADConfiguration.Domain.DNSRoot)"{
$ADConfiguration.Domain.DNSRoot |
Should be $SavedADReport.DomainInformation.DNSRoot
}
it "NetBIOSName $($ADConfiguration.Domain.NetBIOSName)"{
$ADConfiguration.Domain.NetBIOSName |
Should be $SavedADReport.DomainInformation.NetBIOSName
}
it "DomainMode $($ADConfiguration.Domain.DomainMode)"{
$ADConfiguration.Domain.DomainMode |
Should be $SavedADReport.DomainInformation.DomainMode.ToString()
}
it "DistinguishedName $($ADConfiguration.Domain.DistinguishedName)"{
$ADConfiguration.Domain.DistinguishedName |
Should be $SavedADReport.DomainInformation.DistinguishedName
}
it "Server $($ADConfiguration.Domain.RIDMaster) is RIDMaster"{
$ADConfiguration.Domain.RIDMaster |
Should be $SavedADReport.DomainInformation.RIDMaster
}
it "Server $($ADConfiguration.Domain.PDCEmulator) is PDCEmulator"{
$ADConfiguration.Domain.PDCEmulator |
Should be $SavedADReport.DomainInformation.PDCEmulator
}
it "Server $($ADConfiguration.Domain.InfrastructureMaster) is InfrastructureMaster"{
$ADConfiguration.Domain.InfrastructureMaster |
Should be $SavedADReport.DomainInformation.InfrastructureMaster
}
}
Context 'Verifying Default Password Policy'{
it 'ComplexityEnabled'{
$ADConfiguration.PasswordPolicy.ComplexityEnabled |
Should be $SavedADReport.DefaultPassWordPoLicy.ComplexityEnabled
}
it 'Password History count'{
$ADConfiguration.PasswordPolicy.PasswordHistoryCount |
Should be $SavedADReport.DefaultPassWordPoLicy.PasswordHistoryCount
}
it "Lockout Threshold equals $($ADConfiguration.PasswordPolicy.LockoutThreshold)"{
$ADConfiguration.PasswordPolicy.LockoutThreshold |
Should be $SavedADReport.DefaultPassWordPoLicy.LockoutThreshold
}
it "Lockout duration equals $($ADConfiguration.PasswordPolicy.LockoutDuration)"{
$ADConfiguration.PasswordPolicy.LockoutDuration |
Should be $SavedADReport.DefaultPassWordPoLicy.LockoutDuration.ToString()
}
it "Lockout observation window equals $($ADConfiguration.PasswordPolicy.LockoutObservationWindow)"{
$ADConfiguration.PasswordPolicy.LockoutObservationWindow |
Should be $SavedADReport.DefaultPassWordPoLicy.LockoutObservationWindow.ToString()
}
it "Min password age equals $($ADConfiguration.PasswordPolicy.MinPasswordAge)"{
$ADConfiguration.PasswordPolicy.MinPasswordAge |
Should be $SavedADReport.DefaultPassWordPoLicy.MinPasswordAge.ToString()
}
it "Max password age equals $($ADConfiguration.PasswordPolicy.MaxPasswordAge)"{
$ADConfiguration.PasswordPolicy.MaxPasswordAge |
Should be $SavedADReport.DefaultPassWordPoLicy.MaxPasswordAge.ToString()
}
}
Context 'Verifying Active Directory Sites'{
$ADConfiguration.Sites |
ForEach-Object{
it "Site $($_)" {
$SavedADReport.Sites.Name.Contains($_) |
Should be $true
}
}
}
Context 'Verifying Active Directory Sitelinks'{
$lookupSiteLinks = $SavedADReport.Sitelinks | Group-Object -AsHashTable -Property Name
$ADConfiguration.Sitelinks |
ForEach-Object{
it "Sitelink $($_.Name)" {
$_.Name |
Should be $($lookupSiteLinks.$($_.Name).Name)
}
it "Sitelink $($_.Name) costs $($_.Cost)" {
$_.Cost |
Should be $lookupSiteLinks.$($_.Name).Cost
}
it "Sitelink $($_.Name) replication interval $($_.ReplicationFrequencyInMinutes)" {
$_.ReplicationFrequencyInMinutes |
Should be $lookupSiteLinks.$($_.Name).ReplicationFrequencyInMinutes
}
}
}
Context 'Verifying Active Directory Subnets'{
$lookupSubnets = $SavedADReport.SubNets | Group-Object -AsHashTable -Property Name
$ADConfiguration.Subnets |
ForEach-Object{
it "Subnet $($_.Name)" {
$_.Name |
Should be $lookupSubnets.$($_.Name).Name
}
it "Site $($_.Site)" {
$_.Site |
Should be $lookupSubnets.$($_.Name).Site
}
}
}
}

And here’s the result:
AD Operation Readiness
My testlab is quite simple.

Validating operation readiness will definitely help you keep things in check! No second guessing: “Did I configure server x as a Global catalog? With the AD Configuration snapshot you can be certain how you left things! “I know for a fact I configured the server as a Global catalog last week.” Compare your past snapshot to what you’re expecting. Create a new snaphot and compare again. If it’s different… Well… Sometimes colleagues forget to communicate changes that have been made… At least you don’t have to second guess yourself 😉 As an OPS guys Operation readiness has my vote!

Hope it’s worth something to you

Ttyl,

Urv

Active Directory configuration report

‘Sup PSHomies,

Last blog I talked about how to create a AD configuration snapshot. I saved the AD Configuration using the Export-Clixml cmdlet. As promised here’s the follow up: How to create a report from the saved snapshot.

I’m a fan of HTML for reporting purposes. In the past I’ve dabbled in creating reports using XML in combination with CSS. The challenge was creating a well-defined XML file. If you’ve ever had the idea of using Export-Clixml to combine with CSS then you’re in for a disappointment! ConvertTo-HTML is a better fit for reporting. Having said that, creating a well-defined XML file can also be a challenge. As luck would have it there’s a mini-series on the subject, check it out if you want to go down that route.

My favorite way of creating HTML reports these days is using PScribo, brought to us  by Iain Brighton. I saw him demonstrate the module at the PowerShell Conference in Stockholm on youtube. PScribo sure makes creating reports easier! PScribo supports different output formats:

  • HTML
  • Word
  • Text
  • XML

You can also edit the style of your document. To get a better impression of all the possibilities have a look at the video. The module has enough examples to help you get started. Before you know it you’ll be hooked!

Here’s the script:

<#
Author: I.C.A. Strachan
Version:
Version History:
Purpose: Create PScribo Report of saved AD Configuration
#>
[CmdletBinding()]
Param(
$snapshotDate= '29032016'
)
#PScribo link: https://github.com/iainbrighton/PScribo
Import-Module PScribo -Verbose:$false
#Get ADSnapshot
$SavedADSnapshot = Import-Clixml .\export\dsa\ADReport-$($snapshotDate).xml
$reportAD = Document "AD snapshot report - $($snapshotDate)" {
GlobalOption -ForceUppercaseSection -EnableSectionNumbering -PageSize A4 -Margin 24
BlankLine -Count 20
Paragraph "Active Directory snapshot report - $($snapshotDate)" -Style Title
BlankLine -Count 20
PageBreak
TOC -Name 'Table of Contents'
PageBreak
Section -Style Heading1 'Forest Information' {
$ADForest = [Ordered]@{
Name = $($SavedADSnapshot.ForestInformation.Name)
RootDomain = $($SavedADSnapshot.ForestInformation.RootDomain)
ForestMode = $($SavedADSnapshot.ForestInformation.ForestMode.ToString())
Domains = $($SavedADSnapshot.ForestInformation.Domains)
}
Table -Name 'AutofitWidth-AutofitCell-NoHighlighting' -List -Width 0 -Hashtable $ADForest
}
Section -Style Heading1 'Domain Information' {
$ADDomain = [Ordered]@{
NetBIOSName = $($SavedADSnapshot.DomainInformation.NetBIOSName)
DomainMode = $($SavedADSnapshot.DomainInformation.DomainMode.ToString())
DistinguishedName = $($SavedADSnapshot.DomainInformation.DistinguishedName)
DomainSID = $($SavedADSnapshot.DomainInformation.DomainSID)
}
Table -Name 'AutofitWidth-AutofitCell-NoHighlighting' -List -Width 0 -Hashtable $ADDomain
}
Section -Style Heading1 'FSMO Roles' {
$SavedADSnapshot.ForestInformation |
Select-Object DomainNamingMaster,SchemaMaster |
Table -Name 'AutofitWidth-AutofitCell-NoHighlighting' -List -Width 0
Blankline
$SavedADSnapshot.DomainInformation |
Select-Object PDCEmulator,InfrastructureMaster,RIDMaster |
Table -Name 'AutofitWidth-AutofitCell-NoHighlighting' -List -Width 0
}
Section -Style Heading1 'Global Catalogs' {
$GCs = $SavedADSnapshot.ForestInformation |
Select-Object -ExpandProperty GlobalCatalogs
Table -Name 'AutofitWidth-AutofitCell-NoHighlighting' -List -Width 0 -InputObject $GCs
}
Section -Style Heading1 'Domain Controllers' {
$SavedADSnapshot.DomainControllers |
Select-Object Name,OperatingSystem,IPv4Address,Site |
Table -Name 'AutofitWidth-AutofitCell-NoHighlighting' -List -Width 0
}
Section -Style Heading1 'Default Domain Password Policy' {
$SavedADSnapshot.DefaultPassWordPoLicy |
Select-Object ComplexityEnabled,LockoutDuration,LockoutObservationWindow,LockoutThreshold,
MaxPasswordAge,MinPasswordAge,MinPasswordLength,PasswordHistoryCount,ReversibleEncryptionEnabled |
Table -Name 'AutofitWidth-AutofitCell-NoHighlighting' -List -Width 0
}
Section -Style Heading1 'Domain Administrators' {
$SavedADSnapshot.DomainAdministrators |
Select-Object Name,DistinguishedName |
Table -Name 'AutofitWidth-AutofitCell-NoHighlighting' -Width 0
}
Section -Style Heading1 'Organizational Units' {
$SavedADSnapshot.OrganizationalUnits |
Select-Object Name,DistinguishedName |
Table -Name 'AutofitWidth-AutofitCell-NoHighlighting' -Width 0
}
Section -Style Heading1 'Sites & Subnets' {
Section -Style Heading2 'Sites' {
$SavedADSnapshot.Sites |
Select-Object Name,Description,DistinguishedName |
Table -Name 'AutofitWidth-AutofitCell-NoHighlighting' -List -Width 0
}
Section -Style Heading2 'Sitelinks' {
$SavedADSnapshot.SiteLinks |
Select-Object Name,Cost,ReplicationFrequencyInMinutes |
Table -Name 'AutofitWidth-AutofitCell-NoHighlighting' -Width 0
}
Section -Style Heading2 'Subnets' {
$SavedADSnapshot.Subnets |
Select-Object Name,Site |
Table -Name 'AutofitWidth-AutofitCell-NoHighlighting' -Width 0
}
}
}
#Render report in HTML format
$reportAD | Export-Document -Path .\export\dsa -Format Html

I’ve recently discovered markdown. If you’re comfortable creating HTML documents, then MD shouldn’t be much of a challenge ;-). BTW if you’re looking for a good MD reader, vscode has you covered. VSCode is gaining momentum in the PowerShell community. I’ll admit to being hooked on ISESteroids, still,  Tobias Weltner said there will be a major update pretty soon… So who knows what this might mean?

I decided to give it a try in MD Format as well!

<#
Author: I.C.A. Strachan
Version:
Version History:
Purpose: Create MD Report of saved AD Configuration
#>
[CmdletBinding()]
Param(
$snapshotDate= '29032016'
)
#Get ADSnapshot
$SavedADSnapshot = Import-Clixml .\export\dsa\ADReport-$($snapshotDate).xml
#MDReport Title
$MDReport = "# Active Directory snapshot report - $($snapshotDate)`n"
#region Get Forest Information
$ADForest = @"
# Forest Information
| | |
|---|---|
|Name | $($SavedADSnapshot.ForestInformation.Name) |
|RootDomain | $($SavedADSnapshot.ForestInformation.RootDomain) |
|ForestMode | $($SavedADSnapshot.ForestInformation.ForestMode.ToString()) |
|Domains | $($SavedADSnapshot.ForestInformation.Domains) |
"@
$MDReport += $ADForest
#endregion
#region Domain Information
$ADDomain = @"
# Domain Information
| | |
|---|---|
|NetBIOSName | $($SavedADSnapshot.DomainInformation.NetBIOSName) |
|DomainMode | $($SavedADSnapshot.DomainInformation.DomainMode.ToString()) |
|DistinguishedName | $($SavedADSnapshot.DomainInformation.DistinguishedName) |
|DomainSID | $($SavedADSnapshot.DomainInformation.DomainSID) |
"@
$MDReport += $ADDomain
#endregion
#region Default Password Policy
$ADPasswordPolicy = @"
# Default Password Policy
| | |
|---|---|
|ComplexityEnabled | $($SavedADSnapshot.DefaultPassWordPoLicy.ComplexityEnabled) |
|DistinguishedName | $($SavedADSnapshot.DefaultPassWordPoLicy.DistinguishedName) |
|LockoutDuration | $($SavedADSnapshot.DefaultPassWordPoLicy.LockoutDuration) |
|LockoutObservationWindow | $($SavedADSnapshot.DefaultPassWordPoLicy.LockoutObservationWindow) |
|LockoutThreshold | $($SavedADSnapshot.DefaultPassWordPoLicy.LockoutThreshold) |
|MaxPasswordAge | $($SavedADSnapshot.DefaultPassWordPoLicy.MaxPasswordAge) |
|MinPasswordAge | $($SavedADSnapshot.DefaultPassWordPoLicy.MinPasswordAge) |
|MinPasswordLength | $($SavedADSnapshot.DefaultPassWordPoLicy.MinPasswordLength) |
|PasswordHistoryCount | $($SavedADSnapshot.DefaultPassWordPoLicy.PasswordHistoryCount) |
|ReversibleEncryptionEnabled | $($SavedADSnapshot.DefaultPassWordPoLicy.ReversibleEncryptionEnabled) |
"@
$MDReport += $ADPasswordPolicy
#endregion
#region FSMO Roles
$ADFSMORoles = @"
# FSMO Roles
| | |
|---|---|
|DomainNamingMaster| $($SavedADSnapshot.ForestInformation.DomainNamingMaster) |
|SchemaMaster | $($SavedADSnapshot.ForestInformation.SchemaMaster) |
|PDCEmulator | $($SavedADSnapshot.DomainInformation.PDCEmulator) |
|RIDMaster | $($SavedADSnapshot.DomainInformation.RIDMaster) |
|InfrastructureMaster | $($SavedADSnapshot.DomainInformation.InfrastructureMaster) |
"@
$MDReport += $ADFSMORoles
#endregion
#region Global Catalogs
$ADGCs = @"
# Global Catalogs
|Global Catalogs |
|---|
$(
$SavedADSnapshot.ForestInformation.GlobalCatalogs |
ForEach-Object {
"|$_|`n"
}
)
"@
$MDReport += $ADGCs
#endregion
#region Domain Controllers
$ADDCs = @"
# Domain Controllers
|Name|OperatingSystem|IPv4Address|Site|
|---|---|---|---|
$(
$SavedADSnapshot.DomainControllers |
ForEach-Object {
"|$($_.Name)|$($_.OperatingSystem)|$($_.IPv4Address)|$($_.Site)|`n"
}
)
"@
$MDReport += $ADDCs
#endregion
#region Domain Administrators
$ADDomainAdmins = @"
# Domain Administrators
|Name|DistinguishedName|
|---|---|
$(
$SavedADSnapshot.DomainAdministrators |
ForEach-Object {
"|$($_.Name)|$($_.DistinguishedName)|`n"
}
)
"@
$MDReport += $ADDomainAdmins
#endregion
#region Sites & Subnets
$ADSitesSubnets = @"
# Sites & Subnets
## Sites
|Name|Description|DistinguishedName|
|---|---|---|
$(
$SavedADSnapshot.Sites |
ForEach-Object {
"|$($_.Name)|$($_.Description)|$($_.DistinguishedName)|`n"
}
)
## Sitelinks
|Name|Cost|ReplicationFrequencyInMinutes |
|---|---|---|
$(
$SavedADSnapshot.SiteLinks |
ForEach-Object {
"|$($_.Name)|$($_.Cost)|$($_.ReplicationFrequencyInMinutes)|`n"
}
)
## Subnets
|Name|Site|
|---|---|
$(
$SavedADSnapshot.Subnets |
ForEach-Object {
"|$($_.Name)|$($_.Site)|`n"
}
)
"@
$MDReport += $ADSitesSubnets
#endregion
#region Organizational Units
$ADOUs = @"
# Organizational Units
|Name|DistinguishedName|
|---|---|
$(
$SavedADSnapshot.OrganizationalUnits |
ForEach-Object {
"|$($_.Name)|$($_.DistinguishedName)|`n"
}
)
"@
#$MDReport += $ADOUs
#endregion
#Save MDReport
$MDReport | Out-File ".\export\dsa\AD snapshot report - $($snapshotDate).md" -Encoding utf8 -Force

Not bad…

MD Format ADConfiguration

MD format has a small footprint which could be interesting. MD files can be converted to other formats. Doug Finke has an excellent vscode extension to render md files in pdf, word or html, using PanDoc.

PScribo is definitely worth a try! MD is also an option. Generating reports in Powershell just got easier thanks to Iain & Doug! Great addition guys! Keep up the good work!

Hope it’s worth something to you

Ttyl,

Urv