Category Archives: Uncategorized

PSConfAsia 2017

I recently had the pleasure of presenting at PSConf.asia in Singapore. This was my very first conference as a speaker! As a first time speaker a great way to get started.

Jaap Brasser approached me at PSConf.Eu about doing a session (or two) at PSConf.asia. At first I was hesitant…  me, a speaker… Hmmm… I need to think about… Let me get back to you on that… Jaap was clever enough to appease my ego, now how could I say no?

I decided to get out of my comfort zone and said yes, much to Jaap’s delight! Alright! Another PSConf, this time in Singapore, how cool is that!

So after I said yes I did some googling… Let’s see Singapore… Wait what? 12 Hours??? Oh boy… Flying isn’t really my favorite passtime…

ba plane

Hehe… Fortunately I had night flights so…

Singapore has my kinda weather, tropical! I came well prepared. Poor Rob Sewell was melting in Singapore. Transportation is awesome in Singapore. From and to the conference was about 1 SGD.

The organizers did an awesome job! Great location! I met up with old acquaintances and made some new ones!

Finally got to interact with David das Neves. I wanted to attend his session but I opted for Steve Hosking session on Graph. In case you missed it Graph is going to interconnect everything. Definitely have a look at it. I liked David’s approach on having a Class for different types of log format. So I asked him “Hey no  love for Robocopy?” Next thing I knew it was in it! You can read all about hereRaimund Andrée was also there. I told him how NTFSSecurity module saved me on quite a few occasions. He has some serious plans for the module on PowerShell core, so stay tuned!

Rick Taylor won a book on Windows PowerShell for Developers by Doug Finke! Rick was like: “Wait I know this guy!” So I said we need to take a pic featuring Doug’s book! :-). Max Trinidad is a great guy! Been in IT for some time and has loads and loads of skills and it definitely shows!

The keynote is always the highlight of the Conference. Angel Calvo delivered the keynote like a boss! How can I do the key note justice? It’s all about Digital Transformation.

Digital Transformation

Digital tranformation

This is something we all need to consider. It’s not a question of if but when and how. If you’re content with where you’re at right now then you will be obsolete in the near future. What I really appreciated, was the fact that Angel acknowledged that this change can be overwhelming, but it isn’t an all-or-nothing situation. Start where you can, with what you can and take it from there. This digital transformation may take you places you never thought possible. I spoke to Amanda Debler , her transformation has her working more with kurbernetes, how cool is that?

Azure is about adding value to your business

If you’re still under the impression that Azure is just for offloading servers to the cloud, you’re sorely mistaken. We got some excellent demos by Micheal Greene and Ravi Kiran Chintalapudi on Azure Management Services. Azure is about delivering value to your business. And what has value? Data! Lots and lots of it! Having your servers, application, services just to name a few in Azure, will give you the chance to transform metric and monitoring data into added value for your business. As an OPS guy I always thought of monitoring as a necessary evil. Ravi showed us how to manipulate Azure data that is at your deposition and make it valuable! That one server that isn’t patched adequately or that server that’s missing a configuration, the data is there, it’s up to you to turn that into valuable information.

ChatOps

Michael Greene’s demo on ChatOps blew my mind!

ChatOps

I’ve seen chatbots in action but this will take things to the next level! Here are a few links Michael shared to give you an idea where they’re heading:

PowerShell and the future

When Jeffrey Snover said that PowerShell is finished, I died a little inside. Fortunately I was better prepared this time around when Angel said the same 🙂 . PowerShell has gained critical mass. Moving forward, PowerShell Core is where all attention will be placed. Steve Lee had some interesting charts and number he pulled from GitHub (It’s all about data). Mark Kraus is the nr 1 contributor on GitHub! Michael Greene’s go-to PowerShell version is Core! Joey even had a demo on cross-platform administration starting a session on a Windows and Linux. Unfortunately Joey forget about the Timezone difference, so his servers were down.

Keynote conclusion

Conclusion

Pretty much sums it up, no need to reiterate… 🙂

Networking event

Conferences are a great way  to interact with the Microsoft Team and delegates. It was awesome meeting Michael Greene and Steve lee in the flesh! I decided on a different approach when it came to mingling. I wanted to connect on a personal level. I can always reach out when I’m working on something. So here I am sitting in an Irish pub in Singapore knocking over a few with Michael, Amanda, Jason and Max! I asked Michael  what his thoughts were on running Windows for Workgroup 3.11 in Azure… Hypothetically that is :-P. What followed was a lively discussion! Jason had some real great stories to tell! Max told us about his first job in IT back in ’78 when debugging was literally ‘debugging’. Amanda was like: “I wasn’t even born yet…”

Benjamin had Joey finish his demo in the pub. Joey’s demo didn’t go through because of timezone difference (His Azure Servers shutdown automatically after-hours), but Benjamin wasn’t haven’t that. Joey complied and did the demo in the pub, with all attending cheering him on like proper hooligans! For outsiders we may as well have been watching a football game.

I even have my own fan club!

Suresh

Suresh follows my blog and was pretty excited to meet me! Lil’ ol’ me! Suresh made the 12 hour flight worth it! I enjoy meeting my PowerShell heroes in the flesh, I just never considered myself special… Thanks for the support!!! Appreciate it!

Slides & Code

Before I forget here’s the GitHub link to all my presentation slides and code. So Fabian Dibot was at my Infrastructure session and I almost didn’t recognize him! Both him and Mathias Jessen had fun sending twitter messages during my session 😛 . Tip: make sure you disable browser notification before starting you presentation… I know, a rookie mistake, you caught me guys… 😛

Bartek Bielawski was at my Dependencies session! That was a big honor for me! Bartek is the guy you go to when you’re really stuck! While doing my session I was trying to get a read on Bartek’s facial expression. At some point I could see him scanning the code… was that a smile? Ah! no comment whew! I couldn’t wait to ask him for feedback. Getting a compliment from Bartek on presentation preparation  & code definitely gave me a confidence boost! IMHO I enjoyed presenting this session the most. I did this as a flash session at one of our DuPSUG gatherings. I took a different route using AzureAD instead of plain ol’ AD and came across some fun stuff. AD and AzureAD have different parametersets 😉

I’m really glad I did the PSConf.asia sessions. Milton Goh was hinting on a surprise next year? 🙂 If I’m fortunate to be asked next year, I won’t hesitate!

Thanks for a wonderful experience PSConf.asia! I can’t wait to see where your Digital Transformation takes you!

PSConfAsia

Ttyl,

Urv

 

 

ACLs Folder class

‘Sup PSHomies,

I recently had to make a quick Backup & Restore of ACLs three levels deep. Nothing fancy, just two functions, but that got me thinking…

Why not make a class of this?

And so I did! Here’s the framework for the class:

classACLFolders

Here’s a list of the methods:

  • Backup. Backup SDDL from the property $Folder of the class
  • Restore. Restore SDDL to the property $Folder of the class
  • Clone. Clone will take a target Folder and clone the SDDL of $Folder to it
  • ConvertSDDLToAccess. This will enumerate what the SDDL stands for

Default Constructor

classACLDefConstructor

The default constructor will evaluate the folder used to instantiate the object. If successful, the SDDL, Owner and Access is retrieved using  the Backup() method. All actions are registered for future reference.

Instantiating is pretty straightforward:

instantiateClass

Backup()

classACLBackup

This will retrieve the SDDL for the folder and enumerate the Access.

Restore()

classACLRestore

Restore is a bit tricky. For one you need to make sure it isn’t empty. Set-Acl has no problem setting an empty SDDL, blowing everything open (worst case scenario, and that’s why you should test this in a lab first!). The other challenge is having a valid SDDL string. You can change the SDDL string if you want to something gibberish, hence the typecast as a precaution.

Clone()

classACLDefClone

The same goes for cloning. In this case we need to test the target path. Alternatively, you could  also change the Folder to a new path… It works, you’d just have misleading ActionHistory entries… I wonder if there’s a way to make this read-only,  just thinking out loud here… (note to self)

ConvertSDDLToAccess()

This is just a lil’ something extra. Like I said in a previous blog SDDL really gives more information. For one, the SID will let you know which domain this object belongs to. One thing I ran into with ReACL is that SIDHistory will resolve to the current NTAccount. This had me puzzled for a while until I saw that the SIDs in SDDL where different.

Here’s what the ouput looks like:

convertSDDLToAccess

Now for those of you that are wondering just what is this AccessMask, wonder no more! 🙂

Remember the RoboCopy ExitCodes blog I did a while back? Well it’s the same principal 🙂 This is why classes & everything related should be on your radar…

enumAccessmask

Here’s how this works…

Say I wanted to evaluate the AccessMask of a SDDL entry

AccessMaskEnum
classACLConvertSDDLToAccess

Here I have the SID & the NTAccount. This is the builtin administrators account but it also works for Domain accounts.

There’s a private function that will translate the SID accordingly.

To see what the account can actually do we can enumerate the AccessMask

AccessMaskEnum1
AdvancedPermissions

This is what we’d see using the advanced Security GUI tab of a folder.

Not bad… Not bad at all…

I can’t state this enough, SDDL is NOT to be be trifled with. Yes you need admin rights and please try this is a testlab first.  SDDL is very potent, if used with caution, it could do a whole bit of good!

So finally, here’s the code…

[Flags()] Enum AccessMask{
Read = 1
Write = 2
Append = 4
ReadExtendedAttributes = 8
WriteExtendedAttributes = 16
Execute = 32
DeleteDirectory = 64
ReadAttributes = 128
WriteAttributes = 256
Delete = 65536
ReadSecurity = 131072
WriteACL = 262144
WriteOwner = 524288
Synchronize = 1048576
}
class aclsFolder{
[String]$Folder
[String]$SDDL
[String]$Owner
[PSObject[]]$Access
[PSObject[]]$ActionHistory
#Default Constructor
aclsFolder($fldr){
if(Test-Path -Path $fldr){
$this.Folder = $fldr
$Tags = @('Default','Constructor','valid')
$MessageData = "Path $($fldr) found"
$this.ActionHistory += Write-Information -MessageData $MessageData 6>&1 -Tags $Tags | Select-Object *
$this.Backup()
}
else{
$Tags = @('Default','Constructor','invalid')
$MessageData = "Path $($fldr) not found"
$this.ActionHistory += Write-Information -MessageData $MessageData 6>&1 -Tags $Tags | Select-Object *
}
}
#Methods
Backup(){
if(Test-Path $this.Folder){
$result = Get-Acl $this.Folder
$this.SDDL = $result.Sddl
$this.Owner = $result.Owner
$this.Access = $($result.Access | Select-Object File*,Access*,Identity*,IsInherited,*Flags)
$Tags = @('Backup','Success')
$MessageData = "Backup SDDL of $($this.Folder) was successful"
$this.ActionHistory += Write-Information -MessageData $MessageData 6>&1 -Tags $Tags | Select-Object *
}
else{
Write-Warning "Invalid Path $($this.Folder)"
$Tags = @('Backup','Failed')
$MessageData = "Backup SDDL of $($this.Folder) has failed"
$this.ActionHistory += Write-Information -MessageData $MessageData 6>&1 -Tags $Tags | Select-Object *
}
}
Restore(){
if((Test-Path $this.Folder) -and
![string]::isNullOrEmpty($this.SDDL) -and
[Security.AccessControl.RawSecurityDescriptor]$this.SDDL){
$acl = Get-Acl -Path $this.Folder
$acl.SetSecurityDescriptorSddlForm($this.SDDL)
Set-Acl -Path $($this.Folder) -AclObject $($acl)
$Tags = @('Restore', 'Success')
$MessageData = "Restoring SDDL on $($this.Folder) was succesful"
$this.ActionHistory += Write-Information -MessageData $MessageData 6>&1 -Tags $Tags | Select-Object *
#Reset values
$this.Backup()
}
else{
Write-Warning "Invalid Path $($this.Folder) or SDDL is invalid"
$Tags = @('Restore', 'Failed')
$MessageData = "Restoring SDDL on $($this.Folder) has failed"
$this.ActionHistory += Write-Information -MessageData $MessageData 6>&1 -Tags $Tags | Select-Object *
}
}
Clone($tgt){
if((Test-Path -Path $tgt) -and
![string]::isNullOrEmpty($this.SDDL) -and
[Security.AccessControl.RawSecurityDescriptor]$this.SDDL){
$acl = Get-Acl -Path $tgt
$acl.SetSecurityDescriptorSddlForm($this.SDDL)
Set-Acl -Path $($tgt) -AclObject $($acl)
$Tags = @('Clone', 'Success')
$MessageData = "Cloning SDDL on $($this.Folder) was succesful"
$this.ActionHistory += Write-Information -MessageData $MessageData 6>&1 -Tags $Tags | Select-Object *
}
else{
Write-Warning "Invalid Path $($this.Folder) or SDDL is invalid"
$Tags = @('Clone', 'Failed')
$MessageData = "Cloning SDDL on $($this.Folder) has failed"
$this.ActionHistory += Write-Information -MessageData $MessageData 6>&1 -Tags $Tags | Select-Object *
}
}
[PSObject]ConvertSDDLToAccess(){
Function Convert-SID2NTAccount{
param(
$SID
)
$objSID = New-Object System.Security.Principal.SecurityIdentifier($SID)
$objUser = $objSID.Translate( [System.Security.Principal.NTAccount])
$objUser
}
$accessSDDL = ([Security.AccessControl.RawSecurityDescriptor]$this.SDDL).DiscretionaryAcl |
ForEach-Object{
[PSCustomObject]@{
SID = $_.SecurityIdentifier
NTAccount = (Convert-SID2NTAccount -SID $_.SecurityIdentifier)
AceQualifier = $_.AceQualifier
AccessMask = $_.AccessMask
AceType = $_.AceType
AceFlags = $_.AceFlags
IsInherited = $_.IsInherited
InheritanceFlags = $_.InheritanceFlags
}
}
return $accessSDDL
}
}
view raw classACLsFolders.ps1 hosted with ❤ by GitHub

Hope it’s worth something to you…

Ttyl,

Urv

AD Security Group matrix

‘Sup PSHomies,

So last blog was about adding members to a security group efficiently. Which got me thinking, can I reverse this process? If given the security groups with specified members, can I recreate the csv? I do love me a challenge!

So to pick up where we left off, I’ll be using the $addADGroupMembers to repopulate the csv…

#Security Matrix
$Groups = $addADGroupMembers.Keys
function Convert-ArrayToHash($a){
    begin { $hash = @{} }
    process { $hash[$_] = $null }
    end { return $hash }
}

$template = [PSCustomObject]([Ordered]@{UserID=$null} + $($Groups | Convert-ArrayToHash))

$addADGroupMembers has all the group names we need. I’m converting the group names into an empty hashtable. The $template variable is a custom object I’ll be using to move things  along, I’ll explain as we go…

matrix-template

Now for the tricky part…

$arrMatrix = @()

$Groups |
ForEach-Object{
   $GroupName = $_
   if($addADGroupMembers.$_){
      $addADGroupMembers.$_ |
      ForEach-Object{
         if($arrMatrix.Count -eq 0) {
            $newItem = $template.PSObject.Copy()
            $newItem.UserID = $_
            $newItem.$GroupName = '1'
            $arrMatrix += $newItem
         }
         else{
            if($arrMatrix.UserID.contains($($_))){
               $index = [array]::IndexOf($arrMatrix.UserID, $_)
               $arrMatrix[$index].$GroupName = '1'
            }
            else{
               $newItem = $template.PSObject.Copy()
               $newItem.UserID = $_
               $newItem.$GroupName = '1'
               $arrMatrix += $newItem
            }
         }
      }
   }
}

First we have an array to save the results. We only need to worry about groups with members. The $template has been initialized with $null. The first time it runs, $arrMatrix.Count will be zero, so just add this group to get things started. Here’s where it gets interesting, in order to add a newItem to the array I have to clone it first. Adding and saving this way makes sure I have a row with a unique UserID. Truth be told I had to google to figure this one out. Modifying $template and then assigning it to $newItem will only assign the reference. Change the value once more and every item in the array changes! I read it somewhere, it was fun to stumble on this… The more you know…

The next trick was to find the index of a UserID already saved. Google to the rescue! I found this neat trick of using [array]::IndexOf(). This will give you the first index with that value. Lucky for me, my UserIDs are unique 😉
Once I have my index I can add a value of ‘1’ to the group if the UserID is a member. If I can’t find a UserID then a new unique UserID is added to the $arrMatrix

Ok enough chit-chat here’s some code to play with

#region Create GroupMember Hashtable
$csvMatrix = @"
UserID APP-MS Excel APP-MS Outlook APP-MS Powerpoint APP-MS Visio Viewer APP-MS Word APP-Adobe Reader
ejboogers 1 1 1 1 1
dlbouchlaghmi 1 1 1 1 1 1
sideroij 1 1 1 1 1
barnhoorn 1 1 1 1
bofechter 1 1 1 1
asschonewille 1 1 1 1
rrchouiter 1 1 1 1
mgragt 1 1 1 1 1
tpggrimbergen 1 1 1
bvanderhassel 1 1 1 1 1 1
jvderwilk 1 1 1 1 1
cdvanderheijden 1 1 1 1 1 1
thvjanssen 1 1 1 1
skalac-ivor 1 1 1 1 1
nvanderspeklap 1 1 1 1
mkrunder 1 1 1 1
nkoelewijk 1 1 1 1 1
jlekkerkernij 1 1 1 1
"@ | ConvertFrom-Csv -Delimiter "`t"
$csvMatrix |
Sort-Object -Property UserID |
Format-Table
$Header = $csvMatrix |
Get-Member -MemberType NoteProperty |
Where-Object{$_.Name -ne 'UserID'} |
Select-Object -ExpandProperty Name
$addADGroupMembers = @{}
$delADGroupMembers = @{}
$Header |
ForEach-Object{
$Group = $_
$addADGroupMembers.$Group = $csvMatrix.Where{$_.$Group -eq '1'} | Select-Object -ExpandProperty 'UserID'
$delADGroupMembers.$Group = $csvMatrix.Where{$_.$Group -ne '1'} | Select-Object -ExpandProperty 'UserID'
}
#endregion
#region Security Matrix
$Groups = $addADGroupMembers.Keys
function Convert-ArrayToHash($a){
begin { $hash = @{} }
process { $hash[$_] = $null }
end { return $hash }
}
$template = [PSCustomObject]([Ordered]@{UserID=$null} + $($Groups | Convert-ArrayToHash))
$arrMatrix = @()
$Groups |
ForEach-Object{
$GroupName = $_
if($addADGroupMembers.$_){
$addADGroupMembers.$_ |
ForEach-Object{
if($arrMatrix.Count -eq 0) {
$newItem = $template.PSObject.Copy()
$newItem.UserID = $_
$newItem.$GroupName = '1'
$arrMatrix += $newItem
}
else{
if($arrMatrix.UserID.contains($($_))){
$index = [array]::IndexOf($arrMatrix.UserID, $_)
$arrMatrix[$index].$GroupName = '1'
}
else{
$newItem = $template.PSObject.Copy()
$newItem.UserID = $_
$newItem.$GroupName = '1'
$arrMatrix += $newItem
}
}
}
}
}
$arrMatrix |
Sort-Object -Property UserID |
Select-Object 'UserID', 'APP-MS Excel','APP-MS OutLook','APP-MS Powerpoint','APP-MS Visio Viewer','APP-MS Word','APP-Adobe Reader' |
Format-Table
#endregion

This should be your endresult

securitymatrixresults

No too shabby eh? 😛

Ok Urv that’s all good and well but when am I going to use this?

Why thank you for asking!

If you’ve ever been in charge of implementing Role Based Access Control then you could appreciate this. A security matrix like this is where I’d start, only now you don’t have to start from scratch… 😉

Here’s how it works…

adsecuritymatrix

I created a security group Rol-Consultant for RBAC purposes. This group is a member of all the APP-* groups giving any member access by way of group nesting. Users who are a member of Rol-Consultant don’t have to be a direct member for access. The down side of RBAC is it’s all or nothing, exceptions are real deal breakers…

I did a blog about reporting a user’s nested group membership. Let take user ‘dlbouchlaghmi’. This is what his effective user group membership looks like in list form

nestusergroupmembership

The security matrix makes it a bit more visual. Granted, it takes some getting use to but the information is there. Now you can ‘fix’ any issues and reapply the way you see fit! 😉

This has been on my radar for quite some time. Processing security groups this way, makes scripting, I wouldn’t say easier, but more easier to manipulate if you catch my drift…

Ok here’s the code to get the ADSecuirtyMatrix. Do be careful with groups with large memberships. I tried my hand at a group with more than 3800 member, took a couple of minutes, but it worked.

#Security Matrix
#Specify your security groups. try small groups first
$Groups = @(
'APP-MS Outlook','APP-Adobe Reader','APP-MS Word',
'APP-MS Powerpoint','APP-MS Excel','APP-MS Visio Viewer','Rol-Consultant'
)
function Convert-ArrayToHash($a){
begin { $hash = @{} }
process { $hash[$_] = $null }
end { return $hash }
}
$template = [PSCustomObject]([Ordered]@{UserID=$null} + $($Groups | Convert-ArrayToHash))
$arrMatrix = @()
#Get current Group memberships
$SnapshotADGroupMembers = @{}
$Groups |
ForEach-Object{
$SnapshotADGroupMembers.$($_) = Get-ADGroupMember -Identity $_ | Select-Object -ExpandProperty SamAccountName
}
$Groups |
ForEach-Object{
$GroupName = $_
if($SnapshotADGroupMembers.$_){
$SnapshotADGroupMembers.$_ |
ForEach-Object{
if($arrMatrix.Count -eq 0) {
$newItem = $template.PSObject.Copy()
$newItem.UserID = $_
$newItem.$GroupName = '1'
$arrMatrix += $newItem
}
else{
if($arrMatrix.UserID.contains($($_))){
$index = [array]::IndexOf($arrMatrix.UserID, $_)
$arrMatrix[$index].$GroupName = '1'
}
else{
$newItem = $template.PSObject.Copy()
$newItem.UserID = $_
$newItem.$GroupName = '1'
$arrMatrix += $newItem
}
}
}
}
}
$arrMatrix |
Select-Object 'UserID', 'APP-MS Outlook','APP-Adobe Reader','APP-MS Word',
'APP-MS Powerpoint','APP-MS Excel','APP-MS Visio Viewer','Rol-Consultant' |
Out-GridView

I got one more use to go… Some Operation validation! Stay tuned…

Hope it’s worth something to you…

Ttyl,

Urv

Get DSA object creation date

‘Sup PSHomies,

The project manager is on to me! 😛

PM: “PowerShell is awesome!!!”

Me: “You know it!!!”

PM: “Glad you agree! So here’s what I need, We need a weekly update of all users, groups in the source domain based on their creation date…”

Me: “Wait, what just happened?

PM: “You were agreeing that PowerShell is awesome? (Grinning)”

Me: “Well played… I ain’t even mad mad at ya…”

And so begins another PowerShell journey 🙂

The idea here is to be proactive in our migration. Instead of asking for a list of newly created users/groups we’ll gather the information ourselves. All the customer has to do now is validate our list as to who is relevant. Which got me thinking, why stop at user/groups?

Remember I needed to recreate the DFS Structure? The data that I used was from February. What if there were newly created DFS links in the mean time? Turns out my hunch paid off!

Now curiosity got the better of me… “What about accounts that were deleted?” We’re migrating in batches. My first action is always to verify that the accounts in the batch exist. Having a list of deleted objects helps verifying it wasn’t a typo and that this object isn’t relevant anymore…

Here’s what I came up with:

<#
Author: I. Strachan
Version:
Version History:
Purpose: Export DSA Objects
#>
[CmdletBinding()]
param(
$domain = 'pshirwin.local'
)
#region Initiate HashTables & variables
$exportDate = Get-Date -Format ddMMyyyy
$xlsxFile = ".\export\dsa\source\$domain - DSAObjects - $exportDate.xlsx"
$xmlFile = ".\export\dsa\source\$domain - DSAObjects - $exportDate.xml "
$DSAObjects = @{}
$Select = @{}
#endregion
#region Main
$DSAObjects.DFSLinks = Get-ADObject -LDAPFilter '(objectClass=msDFS-LInkv2)'-Properties msDFS-LinkPathv2,msDFS-Propertiesv2,whenChanged,whenCreated |
ForEach-Object{
[PSCustomObject]@{
DFSLink = '\\pshirwin{0}' -f ($_.'msDFS-LinkPathv2').Replace('/','\')
State = $_.'msDFS-Propertiesv2' -join ','
Changed = $_.whenChanged
Created = $_.whenCreated
CreatedDate = Get-Date (Get-Date $_.whenCreated).Date -format yyyyMMdd
}
}
$DSAObjects.PrintQueues = Get-ADObject -LDAPFilter '(objectClass=printQueue)' -Properties printerName,portName,printShareName,uNCName,serverName,whenChanged,whenCreated |
ForEach-Object{
[PSCustomObject]@{
PrinterName = $_.printerName
PortName = $_.portName -join ','
PrintShareName = $_.printShareName -join ','
ServerName = $_.serverName
UNCName = $_.uNCName
Changed = $_.whenChanged
Created = $_.whenCreated
CreatedDate = Get-Date (Get-Date $_.whenCreated).Date -format yyyyMMdd
}
}
$DSAObjects.Contacts = Get-ADObject -LDAPFilter '(objectClass=contact)' -Properties DisplayName,givenName,sn,DistinguishedName,mail,whenChanged,whenCreated |
Foreach-Object {
[PSCustomObject]@{
GivenName = $_.givenName
SurName = $_.sn
DistinguishedName = $_.DistinguishedName
DisplayName = $_.DisplayName
EmailAddress = $_.mail
Changed = $_.whenChanged
Created = $_.whenCreated
CreatedDate = Get-Date (Get-Date $_.whenCreated).Date -format yyyyMMdd
}
}
$DSAObjects.Users = Get-ADUser -LDAPFilter '(objectClass=user)' -Properties accountExpirationDate,LastLogonDate,Initials,Description,EmailAddress,Enabled,DisplayName,OfficePhone,MobilePhone,Department,whenChanged,whenCreated,DistinguishedName,canonicalname |
Foreach-Object {
[PSCustomObject]@{
SamAccountName = $_.SamAccountName
DistinguishedName = $_.DistinguishedName
CanonicalName = $_.CanonicalName
Enabled = $_.Enabled
GivenName = $_.GivenName
Initials = $_.Initials
SurName = $_.SurName
EmailAddress = $_.EmailAddress
Description = $_.Description
Displayname = $_.DisplayName
OfficePhone = $_.OfficePhone
MobilePhone = $_.MobilePhone
Department = $_.Department
LastLogonDate = $_.LastLogonDate
AccountExpiresOn = $_.accountExpirationDate
Changed = $_.whenChanged
Created = $_.whenCreated
CreatedDate = Get-Date (Get-Date $_.whenCreated).Date -format yyyyMMdd
}
}
$DSAObjects.Groups = Get-ADGroup -LDAPFilter '(objectClass=group)' -Properties Member,MemberOf,whenChanged,whenCreated |
Foreach-Object {
[PSCustomObject]@{
DistinguishedName = $_.DistinguishedName
SamAccountName = $_.SamAccountName
Name = $_.Name
Changed = $_.whenChanged
Created = $_.whenCreated
CreatedDate = Get-Date (Get-Date $_.whenCreated).Date -format yyyyMMdd
Member = $_.Member
MemberOf = $_.MemberOf
}
}
$DSAObjects.Deleted = Get-ADObject -Filter * -IncludeDeletedObjects -Properties CN,SamAccountName,LastKnownParent | Where-Object{$_.Deleted -eq $true} |
ForEach-Object{
[PSCustomObject]@{
CN = $(($_.CN -split "`n") -join '; ')
SamAccountName = $_.SamAccountName
Deleted = $_.Deleted
ObjectClass = $_.ObjectClass
LastKnownParent = $_.LastKnownParent
}
}
#endregion
#region Export Object and to Excel
$Select.DFSLinks = '*'
$Select.Users = '*'
$Select.Contacts = '*'
$Select.Groups = @('ObjectClass', 'DistinguishedName', 'Name', 'Changed', 'Created')
$Select.PrintQueue = @('ObjectClass', 'PrinterName','PortName','PrintShareName','ServerName','UNCName','Changed','Created')
$Select.Deleted = @('CN','SamAccountName','Deleted','ObjectClass','LastKnownParent')
#XML File
$DSAObjects |
Export-Clixml -Path $xmlFile -Encoding UTF8
#Excel File
$DSAObjects.Keys |
ForEach-Object{
if($DSAObjects.$_){
$DSAObjects.$_ |
Select-Object $Select.$_ |
Export-Excel -Path $xlsxFile -WorkSheetname $_ -AutoSize -BoldTopRow -FreezeTopRow
}
}
#endregion
view raw Get-DSAObjects.ps1 hosted with ❤ by GitHub

I added Contact and PrintQueues as a bonus 😉

For reporting it’s ImportExcel to the rescue!

dsaobjects

At first I added a extra property in order to lookup the creation date using yyyyMMdd date format. Turns out it wasn’t necessary. You can just a easily use a filter on Created

dsaobjects-filter

The PM is quite pleased… PowerShell is Awesome!!! But you already knew that… 😉

Hope it’s worth something to you

Ttyl,

Urv

 

DFSn links Operational readiness

‘Sup PSHomies,

So here is the follow up on the previous blog.

I recreated the shares, check! Now it’s time to create the DFSn Links & targets!

Here’s the code to create the necessary links and targets:

<#
Author: I. Strachan
Version:
Version History:
Purpose: Create DFS link with specified target
#>
[cmdletbinding()]
Param(
$csvFile = 'DFSnLinksTargets.csv'
)
Import-Module DFSN -Verbose:$false
#region Import
$csvDFSTargets = Import-Csv -Path .\sources\csv\$($csvFile) -Delimiter "`t" -Encoding UTF8
$DfsFolders = Get-DfsnRoot | ForEach-Object {Get-DfsnFolder -Path "$($_.Path)\*"}
#endregion
#region Main
foreach($dfs in $csvDFSTargets){
#Test if target exists first
if(!(Test-Path -LiteralPath $dfs.Target)){
Write-Warning "Target $($dfs.Target) doesn't exists"
Write-Verbose "Creating target $($dfs.Target)"
New-Item -Path $dfs.Target -ItemType Directory -Force
if(!($DfsFolders.Path -contains $($dfs.link))){
Write-Verbose "Creating $($dfs.link) with target $($dfs.Target)"
New-DfsnFolder -Path $dfs.Link -TargetPath $Dfs.Target
}
}
Else{
Write-Warning "Folder '$($dfs.Target)' already exists"
#Check if the DFS Target already exists
$CheckDFSTarget = Get-DfsnFolderTarget -Path $dfs.Link
if($CheckDFSTarget.TargetPath -eq $dfs.Target){
Write-Warning "DFSn Target is already set to $($dfs.Target)"
}
else{
Write-Verbose "Setting $($dfs.link) with target $($dfs.Target)"
Set-DfsnFolderTarget -Path $dfs.Link -TargetPath $dfs.Target
}
}
}
#endregion

Quick run down. The csv is straight forward Link, Target (tab delimited). I created a few checkpoints (again, better safe than sorry).First, I get a list of all actual DFS links & targets for reference. If the target doesn’t exist create it. That’s where the SMB share preparation came in 😉 If the link doesn’t exist create it. If the target exist, verify that it’s set to the specified link.

In my case I only have 1:1 Dfs link/target relationships. So this worked for me. Be sure to test this before trying it out in production.

I needed to recreate 1386 DFS links. This took a few minutes. 10 failed, not bad considering the amount. Now the old Irwin would have panicked at the sight of something going wrong (Whooosahhh). I kept it together and let script ride out. Once it was done I did my OVF to see which ones  failed 😉

<#
Author: I. Strachan
Version:
Version History:
Purpose: OVF DFS links & targets
#>
Param(
$csvFile = 'DFSnLinksTargets.csv'
)
Import-Module DFSN -Verbose:$false
#region Import CSV and saved Credentials
$csvDFSnTargets = Import-Csv -Path .\sources\csv\$($csvFile) -Delimiter "`t" -Encoding UTF8
$DfsnLinkTargets = Get-DfsnRoot |
ForEach-Object{
Get-DfsnFolder -Path "$($_.Path)\*" |
Get-DfsnFolderTarget |
Select-Object Path,TargetPath
}
#endregion
$lookupDFSn = $DfsnLinkTargets | Group-Object -AsHashTable -AsString -Property Path
#region Main
$csvDFSnTargets |
ForEach-Object{
Describe "DFSn Link & Target Operation validation share $($_.Link)" -Tags 'DFSnLinkTargets' {
Context "Verifying DFS Link $($_.Link)" {
It "DFS Link $($_.Link) exists" {
$lookupDFSn.$($_.Link).Path | Should be $_.Link
}
It "DFS Target $($_.Target) exists" {
$lookupDFSn.$($_.Link).TargetPath | Should be $_.Target
}
}
}
}
#endregion

This is one of those rare instances that I created the missing links by hand (don’t judge me I was on a tight schedule). Turns out a few had some form of a special character in the name. Creating them by hand didn’t raise an issue.

Last but not least I wanted to generate a ReportUnit HTML of the DFSn test results. Generating the ReportUnit HTML file didn’t go as I expected. It did succeed in the end., but generating a HTML report for 2772 was a bit too much. Having said that…

There’s a module for Formatting Pester result using PScribo created by Erwan Quélin. Definitely check it out if you want to do everything in PowerShell (Who doesn’t? :-P)

Usage is pretty easy:

dfsformatpesterps1

Here’s a quick screenshot of Format-Pester output:

dfsformatpester

Nice!

Once I resolved the failed test, I ran the test again, nothing but purple and green!

A quick notification in Slack using PSSlack by @pscookiemonster (Even though I’m the only one using Slack at the moment, give it time…)

Awesome!

So I learned that the size of the test when using ReportUnit.exe could be an issue, but if you want to do everything in PowerShell then Format-Pester is a better fit.

Hope it’s worth something to you

Ttyl,

Urv

File servers configuration report

Sup’ PSHomies,

I recently blogged about how awesome PScribo is, couldn’t forget my other favorite go to module when it comes to reporting: ImportExcel brought to us by Doug Finke!

I’ve blogged about my love for csv files and what the possibilities are when it comes to exporting and what to look out for.

My reason for exporting to csv is mostly to create Excel reports. Well if that’s your case you might as well just cut out the middle-man and go straight to the source!

Excel is a great way to analyze your data. I know my way around Excel so that helps.

The title of this blog is about file servers configuration. Think volumes, shares etc etc. The idea is to gather all File server related data (Now where have you seen that before 😛 ) and create a xlsx file report.

I don’t know about you but it seems like the community is producing more and more high quality blogs on just about every subject you can think of! I can barely keep up!

I enjoyed Jaap Brasser’s take on storing your credentials. Thanks for sharing Jaap! 😉

I’m using a scriptblock in combination with invoke-command to gather the needed information . The scriptblock returns a custom object.

$sbStorage = {
    [PSCustomObject]@{
        Volume = $(Get-Volume | Select-Object *)
        SMBShare = $(Get-SmbShare | Select-Object *)
        SMBShareAccess = $(Get-SmbShare | Get-SmbShareAccess)
        SMBShareNTFS = $(Get-SmbShare |
            Where-Object{ $_.Name -ne 'IPC$'} |
            Get-Acl |
                Select-Object @{Name='Path';Expression={($_.Path).Replace('Microsoft.PowerShell.Core\FileSystem::','')}},
                Owner,Access
        )
        Disk = $(Get-Disk | Select-Object *)
        VSSShadows = $(vssadmin.exe list Shadows)
        VSSWriters = $(vssadmin.exe list Writers)
    }
}

#region Main
$snapshotStorage = $Servers |
ForEach-Object{
  Invoke-Command  -ComputerName $_.ComputerName -ScriptBlock $sbStorage -Credential $cred
}
#endregion

I’m using admin credentials in my test lab so this will also work without -Credential. This was sufficient in my case.

Here’s a quick rundown on what the script basically does. Let’s take a look at documenting Shares to get the general idea…

#region Get Shares
$snapshotStorage |
ForEach-Object{
    $ComputerName = $_.PSComputerName

    $_.SMBShare |
    ForEach-Object{
        [PSCustomObject]@{
            ComputerName = $ComputerName
            Name = $_.Name
            Path = $_.Path
            Description = $_.Description
            ShareState = $_.ShareState
        }
    }
}|
Export-Csv .\export\storage\FS_SMBShares-$($exportDate).csv -Encoding UTF8 -Delimiter &quot;`t&quot; -NoTypeInformation

#Export to xlsx file
Import-Csv .\export\storage\FS_SMBShares-$($exportDate).csv -Encoding UTF8 -Delimiter &quot;`t&quot; |
Export-Excel -Path $xlsxFile -WorkSheetname SMBShares -AutoSize -BoldTopRow -FreezeTopRow
#endregion

For each server we’ve captured share information to $_.SMBShares. Simply select the properties you want and save to a custom object. Saving to csv and then exporting to Excel makes sure that everything is a string . This is one of those times that having your object cast as string comes in handy ;-). Also, most of us don’t have Excel on every server. Having the csv files let’s you quickly assess what you can expect in your Excel report.

Export-Excel has quite an extensive parameterset

Export-Excel syntax

No need to open the xlsx file, autosize and freeze to the toprow, just add the following switches -AutoSize -BoldTopRow -FreezeTopRow and your good to go!

<#
Author: I. Strachan
Version:
Version History:
Purpose: Get File Servers storage configuration
#>
#region FileServers to process
$Servers = @"
ComputerName
DC-DSC-01
MEM-DSC-01
"@ | ConvertFrom-Csv -Delimiter "`t"
#endregion
#region import saved credential and initialize variables
#Jaap Brassers blog on saving credentials.
#http://www.jaapbrasser.com/quickly-and-securely-storing-your-credentials-powershell/
$Hash = Import-CliXml -Path "${env:\userprofile}\Hash.Cred"
$cred = $Hash.'pshirwin-admin'
$exportDate = Get-Date -Format ddMMyyyy
$xlsxFile = ".\export\storage\PSHIRWIN - StorageReport - $($exportDate).xlsx"
$null = Get-SmbShare | Get-SmbShareAccess
#endregion
#region Helper functions and scriptblock
filter Get-CapacitySize {
'{0:N2} {1}' -f $(
if ($_ -lt 1kb) { $_, 'Bytes' }
elseif ($_ -lt 1mb) { ($_/1kb), 'KB' }
elseif ($_ -lt 1gb) { ($_/1mb), 'MB' }
elseif ($_ -lt 1tb) { ($_/1gb), 'GB' }
elseif ($_ -lt 1pb) { ($_/1tb), 'TB' }
else { ($_/1pb), 'PB' }
)
}
$sbStorage = {
[PSCustomObject]@{
Volume = $(Get-Volume | Select-Object *)
SMBShare = $(Get-SmbShare | Select-Object *)
SMBShareAccess = $(Get-SmbShare | Get-SmbShareAccess)
SMBShareNTFS = $(Get-SmbShare |
Where-Object{ $_.Name -ne 'IPC$'} |
Get-Acl |
Select-Object @{Name='Path';Expression={($_.Path).Replace('Microsoft.PowerShell.Core\FileSystem::','')}},
Owner,Access
)
Disk = $(Get-Disk | Select-Object *)
VSSShadows = $(vssadmin.exe list Shadows)
VSSWriters = $(vssadmin.exe list Writers)
}
}
#endregion
#region Main
$snapshotStorage = $Servers |
ForEach-Object{
Invoke-Command -ComputerName $_.ComputerName -ScriptBlock $sbStorage -Credential $cred
}
#endregion
#region Export snapshot to XML
$snapshotStorage | Export-Clixml .\export\storage\StorageSnapshot-$($exportDate).xml -Encoding UTF8
#endregion
#region Create CSV reports
#export Server names to xlsxFile
$Servers|
Export-Excel -Path $xlsxFile -WorkSheetname Servers -AutoSize -BoldTopRow
#region Get Volume Names
$snapshotStorage |
ForEach-Object{
$ComputerName = $_.PSComputerName
$_.Volume |
ForEach-Object{
[PSCustomObject]@{
ComputerName = $ComputerName
DriveLetter = $_.Driveletter
FileSystemLabel = $_.FileSystemLabel
DiskSize = $_.Size | Get-CapacitySize
UsedSpace = $_.SizeRemaining | Get-CapacitySize
FreeSpace = ($_.Size - $_.SizeRemaining) | Get-CapacitySize
}
}
} |
Export-Csv .\export\storage\FS_VolumeNames-$($exportDate).csv -Encoding UTF8 -Delimiter "`t" -NoTypeInformation
#Export to xlsx file
Import-Csv .\export\storage\FS_VolumeNames-$($exportDate).csv -Encoding UTF8 -Delimiter "`t" |
Export-Excel -Path $xlsxFile -WorkSheetname VolumeNames -AutoSize -BoldTopRow -FreezeTopRow
#endregion
#region Get Shares
$snapshotStorage |
ForEach-Object{
$ComputerName = $_.PSComputerName
$_.SMBShare |
ForEach-Object{
[PSCustomObject]@{
ComputerName = $ComputerName
Name = $_.Name
Path = $_.Path
Description = $_.Description
ShareState = $_.ShareState
}
}
}|
Export-Csv .\export\storage\FS_SMBShares-$($exportDate).csv -Encoding UTF8 -Delimiter "`t" -NoTypeInformation
#Export to xlsx file
Import-Csv .\export\storage\FS_SMBShares-$($exportDate).csv -Encoding UTF8 -Delimiter "`t" |
Export-Excel -Path $xlsxFile -WorkSheetname SMBShares -AutoSize -BoldTopRow -FreezeTopRow
#endregion
#region Get Sharesaccess
$snapshotStorage |
ForEach-Object{
$ComputerName = $_.PSComputerName
$_.SMBShareAccess |
ForEach-Object{
[PSCustomObject]@{
ComputerName = $ComputerName
Name = $_.Name
AccountName = $_.AccountName
AccessControlType = $_.AccessControlType
AccessRight = [Microsoft.PowerShell.Cmdletization.GeneratedTypes.SmbShare.ShareAccessRight]$_.AccessRight
}
}
}|
Export-Csv .\export\storage\FS_SMBSharesAccess-$($exportDate).csv -Encoding UTF8 -Delimiter "`t" -NoTypeInformation
#Export to xlsx file
Import-Csv .\export\storage\FS_SMBSharesAccess-$($exportDate).csv -Encoding UTF8 -Delimiter "`t" |
Export-Excel -Path $xlsxFile -WorkSheetname SMBSharesAccess -AutoSize -BoldTopRow -FreezeTopRow
#endregion
#region Get Shares NTFS Access
$snapshotStorage |
ForEach-Object{
$ComputerName = $_.PSComputerName
$_.SMBShareNTFS |
ForEach-Object{
$fullPathName = $_.Path
$pathOwner = $_.Owner
$_.Access|
ForEach-Object{
[PSCustomObject]@{
ComputerName = $ComputerName
Path = $fullPathName
Owner = $pathOwner
IdentityReference = $_.IdentityReference
FileSystemRights = $_.FileSystemRights
AccessControlType = $_.AccessControlType
IsInherited = $_.IsInherited
InheritanceFlags = $_.InheritanceFlags
PropagationFlags = $_.PropagationFlags
}
}
}
}|
Export-Csv .\export\storage\FS_SMBSharesNTFS-$($exportDate).csv -Encoding UTF8 -Delimiter "`t" -NoTypeInformation
#Export to xlsx file
Import-Csv .\export\storage\FS_SMBSharesNTFS-$($exportDate).csv -Encoding UTF8 -Delimiter "`t" |
Export-Excel -Path $xlsxFile -WorkSheetname SMBSharesNTFS -AutoSize -BoldTopRow -FreezeTopRow
#endregion
#region VSS Shadow copies
$shadowCopyProperties = [Ordered]@{
ShadowCopySetID = ''
CreationTime = ''
ShadowCopyID = ''
OriginalVolume = ''
ShadowCopyVolume = ''
Machine = ''
Provider = ''
Type = ''
Attributes = ''
}
$snapshotStorage |
ForEach-Object{
foreach($line in $_.VSSShadows){
switch($line){
{$_ -match 'Contents of shadow copy set ID:'}
{
$shadowCopy = New-Object -TypeName psobject -Property $shadowCopyProperties
$shadowCopy.ShadowCopySetID = $_.Substring(32).Trim()
}
{$_ -match ' Contained 1 shadow copies at creation time:'}
{ $shadowCopy.CreationTime = $_.Substring(46).Trim() }
{$_ -match ' Shadow Copy ID:'}
{ $shadowCopy.ShadowCopyID = $_.Substring(21).Trim() }
{$_ -match ' Original Volume:'}
{ $shadowCopy.OriginalVolume = $_.Substring(26).Trim() }
{$_ -match ' Shadow Copy Volume:'}
{ $shadowCopy.ShadowCopyVolume = $_.Substring(29).Trim() }
{$_ -match ' Originating Machine: '}
{ $shadowCopy.Machine = $_.Substring(30).Trim() }
{$_ -match ' Provider:'}
{ $shadowCopy.Provider = $_.Substring(19).Trim() }
{$_ -match ' Type:'}
{ $shadowCopy.Type = $_.Substring(15).Trim() }
{$_ -match ' Attributes:'}
{
$shadowCopy.Attributes = $_.Substring(21).Trim()
[PSCustomObject]$shadowCopy
}
}
}
}|
Export-Csv .\export\storage\FS_VSSShadowCopies-$($exportDate).csv -Encoding UTF8 -Delimiter "`t" -NoTypeInformation
#Export to xlsx file
Import-Csv .\export\storage\FS_VSSShadowCopies-$($exportDate).csv -Encoding UTF8 -Delimiter "`t" |
Export-Excel -Path $xlsxFile -WorkSheetname 'VSS Shadow copies' -AutoSize -BoldTopRow -FreezeTopRow
#endregion
#region VSS Writers
$shadowWriterProperties = [Ordered]@{
ComputerName = ''
Name = ''
ID = ''
InstanceID = ''
State = ''
LastError = ''
}
$snapshotStorage |
ForEach-Object{
$ComputerName = $_.PSComputerName
foreach($line in $_.VSSWriters){
switch($line){
{$_ -match 'Writer name:'}
{
$shadowWriter = New-Object -TypeName psobject -Property $shadowWriterProperties
$shadowWriter.ComputerName = $ComputerName
$shadowWriter.Name = $_.Substring(12).Replace("'",'').Trim()
}
{$_ -match ' Writer Id:'}
{ $shadowWriter.ID = $_.Substring(13).Trim() }
{$_ -match ' Writer Instance Id:'}
{ $shadowWriter.InstanceID = $_.Substring(22).Trim() }
{$_ -match ' State: '}
{ $shadowWriter.State = $_.Substring(13).Trim() }
{$_ -match ' Last error:'}
{
$shadowWriter.LastError = $_.Substring(14).Trim()
[PSCustomObject]$shadowWriter
}
}
}
}|
Export-Csv .\export\storage\FS_VSSShadowWriters-$($exportDate).csv -Encoding UTF8 -Delimiter "`t" -NoTypeInformation
#Export to xlsx file
Import-Csv .\export\storage\FS_VSSShadowWriters-$($exportDate).csv -Encoding UTF8 -Delimiter "`t" |
Export-Excel -Path $xlsxFile -WorkSheetname 'VSS Shadow writers' -AutoSize -BoldTopRow -FreezeTopRow
#endregion
#endregion

Here’s a quick impression of the xlsx file

File Server Excel Report

I can’t say enough good things about Import-Excel. Doug Finke actively maintains this module and is open to suggestions to make it better! Thanks for making reporting in Excel so much easier Doug!

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

Pester to validate ADUser properties

‘Sup PSHomies,

See if you can relate. You’re in the middle of a migration, the users need to be created asap. You get a xlsx file with all the necessary properties. A quick Copy/Paste to csv file, Import-Csv  user.csv -Delimiter "`t" | New-ADUser and presto! Whew! Glad we got that out of the way 😉

Feels pretty awesome right? 15 minutes after, your project manager comes asking: “Say, which file did you use?” The one you sent me last week, why? “Uh, there’s a new version on sharepoint, did you use that one?” Well I did ask which file I should use (in my defense I did, that’s why I always email, written proof!). “Well there’s an updated version, could you make sure the users get updated? Thanks!!!” Sigh, here we go again…

At this point I can do two things:

  1. Just delete and recreate. Thing is you’ll loose SIDs and access to homedirectory etc etc. Not exactly ideal.
  2. Update the user properties. Definitely a better option. Still tricky especially using the Set-ADUser cmdlet, but that’s another story.

But before you go off to update the user settings, how about validating what has been changed? Maybe the damage isn’t that bad. I mean if it’s under five changes, I just might  do it manually… Oh who am I kidding? Wait, gimme a minute to catch my breathe from laughing! 😛

Enter Pester for ADUser validation!

With a Pester script to validate your ADUser settings, you’ll never have to second guess if the settings are as they should.

<#
Author: I.C.A. Strachan
Version: 1.0
Version History:
Purpose: Pester script to validate ADUser properties.
#>
[CmdletBinding()]
Param(
[string]
$csvFile = 'Users.csv',
[Microsoft.PowerShell.Commands.FileSystemCmdletProviderEncoding]
$Encoding = 'UTF8'
)
$csvParam = @{
Path = ".\source\csv\$csvFile"
Delimiter = "`t"
Encoding = $Encoding
}
$csvADUsers = Import-Csv @csvParam
$ADPropertiesToVerify = ($csvADUsers | Get-Member | Where-Object {$_.membertype -eq 'noteproperty'}).name
Foreach ($user in $csvADUsers){
#Get AD User attirbutes
try{
$verify = Get-ADUser Identity $user.SamAccountName Properties *
if ($verify) {
Describe "AD User operational readiness for $($user.DisplayName)" {
Context 'Verifying ADUser Attributes'{
ForEach($attribute in $ADPropertiesToVerify){
if (([string]::isNullOrEmpty($user.$attribute))) {
$user.$attribute = $null
}
if($attribute -eq 'Path'){
it "User is located in $($user.$attribute)" {
$verify.DistinguishedName.Contains($user.$attribute)
}
}
else{
it "User property $($attribute) value is $($verify.$attribute)" {
$user.$attribute | Should be $verify.$attribute
}
}
}
}
Context 'Verifying ADUser HomeDirectory Security'{
it 'User HomeDirectory attribute is not empty'{
$user.HomeDirectory | Should not be $null
}
It "Homedirectory $($user.HomeDirectory) exists"{
Test-Path $user.HomeDirectory | Should be $true
}
It "User is owner of $($user.HomeDirectory)"{
(Get-Acl $user.HomeDirectory).Owner| Should be "$($env:USERDOMAIN)\$($user.sAMAccountName)"
}
}
}
}
}
catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException]{
Write-Error Message "User $($user.SamAccountName) account NOT present"
}
catch {
Write-Error Message "Unhandled exception looking up $($user.SamAccountName)) account."
throw $_
}
}

Here’s the result:ADPesterResults

Here’s a quick rundown of the script:

First I’ll just get all the user settings using $verify = Get-ADUser -Identity $user.SamAccountName -Properties *.

$ADPropertiesToVerify =  ($csvADUsers | Get-Member | Where-Object {$_.membertype -eq 'noteproperty'}).name will get me all the properties in the csv file. No need to map properties manually. Now I can loop through any amount of properties!

Next up, making sure empty properties get $null

if (([string]::isNullOrEmpty($user.$attribute))) {
   $user.$attribute = $null
}

$null isn’t equal to empty (Ofcourse you already knew that!)

Now compare what’s in the csv to what Get-ADUser found:

if($attribute -eq 'Path'){
   it &quot;User is located in $($user.$attribute)&quot; {
      $verify.DistinguishedName.Contains($user.$attribute)
   }
}
else{
   it &quot;User property $($attribute) value is $($verify.$attribute)&quot; {
      $user.$attribute | Should be $verify.$attribute
   }
}

Quick note: I used Path to create the user in a specific OU. There’s no Path property in Get-ADUser. So I did the next best thing, just verify that path is part of the user’s distinguishedname 😉

I also added a little bonus to verify the user’s homedirectory exists and that the user is also the owner.

Being able to validate will definitely give you peace of mind…

Hope it’s worth something to you

Ttyl,

Urv

Enumerations in PowerShell

‘Sup PSHomies,

I’m trying to get a grip on PowerShell Classes and all the new stuff we have available in version 5.

So June Blender posted this link in one of her tweets by @psCookieMonster. Guess Enum is a good place to start!

Using .NET enumerations to improve a user’s experience is definitely worth investing in, less error prone. I got a great tip how to create Custom Intellisense experience by @pcgeek86

I’ve used validation sets in the past. My “Aha” moment was the fact that you can just as easily typecast using the enumeration. You should check out powershell.com tip on the subject.

So I can create my own Enum and use it as a validation set? Let’s give it a try!

Enum PizzaSize {
Small = 10
Medium
Regular
Large = 3
ExtraLarge = 4
}
function Get-PizzaSize{
param(
[PizzaSize]
$PizzaSize
)
"You chose $PizzaSize"
}

view raw
Enum.ps1
hosted with ❤ by GitHub

Hey! who doesn’t love a good pizza eh? 😉

Ok, there’s an upside and downside to this approach. The upside is that it keeps your code clean. The downside is that it has to be available prior to using. Another point to consider is that this is Syntax is only available in version 5.

Note: Enumerations are case-sensitive.

As luck would have it, I was perusing Microsoft’s pester scripts GitHub repository when I came across Pester for Classes! Here’s one from the pester scripts on Enumeration found in scripting.enums.tests.ps1

enum E3{
   e0
   e1 = 5
   e2 = ([int]::MaxValue) - 1
   e4
   e3 = 1    # This shouldn't be an error even though previous member was max int
}

So what’s going here? Well for starters I never paid much attention to the value being assigned. The first entry is assigned the value 0 if it hasn’t been given one. The second will be an increment of +1 of the first, that is, if it hasn’t been assigned another value.

So what are the current values?

[Enum]::GetValues('E3') | ForEach-Object {'{0} {1}' -f $_, ([E3]$_).value__ }

E3Values
e2 has the MaxValue of [Int] minus one. e4 wasn’t assigned a value yet. Its value is an increment of e2 (the MaxValue of [Int]). If you try to add a new entry after e4, say e5, you’ll receive an error.

enum E3{
   e0
   e1 = 5
   e2 = ([int]::MaxValue) - 1
   e4
   e5
   e3 = 1    # This shouldn't be an error even though previous member was max int
}

e5error

Have a look at the PizzaSize enumeration, what are the values actually?

[Enum]::GetValues('PizzaSize') | ForEach-Object {'{0}: {1}' -f $_, ([PizzaSize]$_).value__ }

PizzaSizeValues
Medium and Regular are incrementals of Small, Who knew! 😛

Want to make sure the enumeration has been defined?

[enum]::IsDefined([PizzaSize],'large') #Will be False case-sensitive
[enum]::IsDefined([PizzaSize],'Large') #True
[PizzaSize]10 #Returns Small
[PizzaSize]3  #Returns Large

#These will all return Large
[PizzaSize]::Large
[PizzaSize]'Large'
[PizzaSize]3

How about if I wanted the value of the day of the week, say Saturday?

([System.DayOfWeek]'Saturday').value__

[Enum]::GetValues('System.DayofWeek') | ForEach-Object {'{0}: {1}' -f $_, ([System.DayofWeek]$_).value__ }

Well, off to a good start when it comes to Enumerations!

Hope it’s worth something to you…

Ttyl,

Urv