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

14 thoughts on “AD Security Group matrix

  1. sjonker73@gmail.com

    Hey Man, nice bit of coding. This script is pretty similar. I admit your script has more style, but also a lot more lines of code. Input is a CV file (ordered) with a list of users to group relationships. Left column user, right column group. The CSV can easily be swapped for a get-adgroupmembers command dumped into an array and then sorted. Output to a CSV, but can also be piped to FT right at the end of the loop.

    $oList = ipcsv e:\temp\usergroups.csv -Delimiter “;”
    $sTemp = $null
    cls
    $sLinks = ‘User ID’
    $sRechts = ‘Group Name’

    foreach ($oEntry in $oList){

    if ($oEntry.$sLinks -ne $sTemp){
    if ($sTemp -ne $Null){$oColumns|Export-Csv [some_file] -Delimiter “;” -NoTypeInformation -Append}

    $oColumns = New-Object PSObject
    $i = 1
    $sTemp = $null

    $oColumns | Add-Member -type NoteProperty -Name ‘User’ -Value $oEntry.$sLinks
    $oColumns | Add-Member -type NoteProperty -Name $i -Value $oEntry.$sRechts
    $i++
    }elseif ($oEntry.$sLinks -eq $sTemp){
    $oColumns | Add-Member -type NoteProperty -Name $i -Value $oEntry.$sRechts
    $i++
    }

    $sTemp = $oEntry.$sLinks

    }

    Happy coding.

    Liked by 1 person

    Reply
  2. Dirk

    Hi Irwin,
    I really like your ideas. You could use hashtables instead of an array of PSObjects instead, making use of the fact that hashtables only permit unique keys, to build your matrix. This should work faster too.


    $htUsers = @{}
    $htProps = @{}
    $addADGroupMembers.Keys | foreach {$htProps.$_=$null}
    foreach ($group in $addADGroupMembers.GetEnumerator()){
    foreach ($user in $group.Value){
    if (!$htUsers.ContainsKey($user)){
    $htProps.UserID = $user
    $htUsers.$user = $htProps.Clone()
    }
    ($htUsers.$user).$($group.Name) = 1
    }
    }
    $htUsers.GetEnumerator() | foreach{
    [PSCustomObject]$_.Value
    } | sort UserID | ft

    view raw

    test.ps1

    hosted with ❤ by GitHub

    Liked by 1 person

    Reply
    1. Chris Morley

      Hi i am new to powershell and trying to wrap my head around the code (nice and compact btw!), I can see the code as-is gives a matrix of the groups, but please advise how I can get the users listed down the left colum, and how to get their UPN (as opposed to the ObjectGUID). Thanks in advance!

      $groups = Get-ADGroup -Filter {GroupCategory -eq ‘security’} -SearchBase ‘OU=Groups,DC=corp,DC=test,DC=local’
      $users = Get-ADUser -Filter * -SearchBase “dc=corp,dc=test,dc=local”

      $addADGroupMembers = @{}

      foreach($group in $groups) {
      $addADGroupMembers.Add($group.Name, $group.ObjectGUID)
      }

      $htUsers = @{}
      $htProps = @{}
      $addADGroupMembers.Keys | foreach {$htProps.$_=$null}
      foreach ($group in $addADGroupMembers.GetEnumerator()){
      foreach ($user in $group.Value){
      if (!$htUsers.ContainsKey($user)){
      $htProps.UserID = $user
      $htUsers.$user = $htProps.Clone()
      }
      ($htUsers.$user).$($group.Name) = 1
      }
      }

      $htUsers.GetEnumerator() | foreach{
      [PSCustomObject]$_.Value
      } | sort UserID | ft

      Like

      Reply
      1. Irwin Strachan Post author

        Hi Chris,

        So instead of the user’s SamAccountName, you’d like the user’s UPN, right? That could be tricky as the UPN isn’t mandatory/or always filled in…

        Also, a group member could be a security group (no UPN available then)…

        Here’s something you could try:


        $paramADGroup = @{
        Filter = {GroupCategory -eq 'security'}
        SearchBase = 'OU=Groups,DC=corp,DC=test,DC=local'
        Properties = @('Members')
        }
        $groups = Get-ADGroup @paramADGroup
        $addADGroupMembers = @{}
        $groups |
        ForEach-Object {
        $addADGroupMembers.$($_.Name) = Get-ADGroupMember -Identity $($_.Name)
        }
        $htUsers = @{}
        $htProps = @{}
        $addADGroupMembers.Keys |
        ForEach-Object {$htProps.$_ = $null}
        foreach ($group in $addADGroupMembers.GetEnumerator()){
        foreach ($member in $group.Value){
        if (!$htUsers.ContainsKey($member.SamAccountName)){
        $htProps.UserID = $member.SamAccountName
        $htProps.ObjectClass = $member.objectClass
        $htProps.UPN = if($member.objectClass -eq 'user'){(Get-ADUser $member.SamAccountName).UserPrincipalName}
        $htUsers.$($member.SamAccountName) = $htProps.Clone()
        }
        ($htUsers.$($member.SamAccountName)).$($group.Name) = 1
        }
        }
        $htUsers.GetEnumerator() | ForEach-Object{
        [PSCustomObject]$_.Value
        } |
        Sort-Object UserID

        You might have to select at the end depending on if you have any prefixes.

        Rg./Irwin

        Like

  3. Pingback: Protecting output content from prying eyes | pshirwin

  4. Tim

    Please please please I’m trying to do this for all users in all groups for my company to know who’s in what groups at what level. Are you able to write up a script for me in matrix format.

    Like

    Reply
    1. Irwin Strachan Post author

      Hi Tim,

      I have no idea the size of your AD. That could be a tall order. Here’s something to get you started. First look into how many groups you have. I did this by scope. Next look into the group count. I did the security matrix for just the global but you get the idea…

      HTH


      $ADGroups = @{
      DomainLocal = @{}
      Global = @{}
      Universal = @{}
      }
      #region Get Members security Groups in AD
      Get-ADGroup -Filter * |
      ForEach-Object{
      $SamAccountName = $_.SamAccountName
      switch ($_.GroupScope) {
      'DomainLocal' {
      $ADGroups.DomainLocal.$SamAccountName = Get-ADGroupMember -Identity $SamAccountName | Select-Object -ExpandProperty SamAccountName
      }
      'Global'{
      $ADGroups.Global.$SamAccountName = Get-ADGroupMember -Identity $SamAccountName | Select-Object -ExpandProperty SamAccountName
      }
      'Universal' {
      $ADGroups.Universal.$SamAccountName = Get-ADGroupMember -Identity $SamAccountName | Select-Object -ExpandProperty SamAccountName
      }
      }
      }
      #endregion
      #region Get Member Count AD Groups
      $ADGroups.Keys |
      ForEach-Object{
      $GroupScope = $_
      $ADGroups.$GroupScope.Keys |
      ForEach-Object{
      [PSCustomObject]@{
      Group = $_
      GroupScope = $GroupScope
      Count = @($ADGroups.$GroupScope.$_).Count
      }
      }
      }
      #endregion
      #region Get Security Matrix for Global AD Groups
      $htUsers = @{}
      $htProps = @{}
      $ADGroups.Global.Keys | ForEach-Object {$htProps.$_ = $null}
      foreach ($group in $ADGroups.Global.keys){
      foreach ($user in $ADGroups.Global.$($group)){
      if (!$htUsers.ContainsKey($user)){
      $htProps.SamAccountName = $user
      $htUsers.$user = $htProps.Clone()
      }
      ($htUsers.$user).$($group) = 'x'
      }
      }
      $htUsers.GetEnumerator() |
      ForEach-Object{
      [PSCustomObject]$($_.Value)
      } |
      Out-GridView
      #endregion

      Rg./Irwin

      Like

      Reply
  5. Tim

    amazing, script! however i need to show the members in each group in matrix format, i’m trying to change it so it doesnt show only the count, but the members – if that makes sense.

    Like

    Reply

Leave a comment