├── LICENSE ├── ReadMe.md └── WatchDog3.ps1 /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 SadProcessor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | WatchDog is a BloodHound Data scanner [NodeWeight] 2 | 3 | More info: 4 | 5 | 6 | https://insinuator.net/2019/10/blue-hands-on-bloodhound/ 7 | -------------------------------------------------------------------------------- /WatchDog3.ps1: -------------------------------------------------------------------------------- 1 | ########################################################### 2 | <# 3 | ## Instructions ## 4 | 5 | # Setup 6 | - [stop neo4j service if running] 7 | - uncomment following in neo4j.conf and save change 8 | #dbms.security.auth_enabled=false 9 | [will disable auth for Localhost only] 10 | - start neo4j service 11 | - load watchdog.ps1 12 | 13 | # Basic usage 14 | -Single Group: 15 | $Data = Datadog 16 | 17 | -Default Group List: 18 | $Data = WatchDog 19 | 20 | -TotalImpact 21 | $Data | TotalImpact 22 | 23 | -Text Report: 24 | $Data | ReportDog 25 | 26 | ## 27 | #> 28 | 29 | #################################################### Vars 30 | 31 | ## Group List [Customize if needed] 32 | $GroupList = @( 33 | <###############| NAME |SID [regex] #> 34 | <######################################################################################> 35 | [PSCustomObject]@{Name='Account Operators' ;SID='S-1-5-32-548' } 36 | [PSCustomObject]@{Name='Administrators' ;SID='S-1-5-32-544' } 37 | [PSCustomObject]@{Name='Allowed RODC Password Replication' ;SID='^S-1-5-21-.*-571$' } 38 | [PSCustomObject]@{Name='Backup Operators' ;SID='S-1-5-32-551' } 39 | [PSCustomObject]@{Name='Certificate Service DCOM Access' ;SID='S-1-5-32-574' } 40 | [PSCustomObject]@{Name='Cert Publishers' ;SID='^S-1-5-21-.*-517$' } 41 | [PSCustomObject]@{Name='Distributed DCOM Users' ;SID='S-1-5-32-562' } 42 | [PSCustomObject]@{Name='Domain Admins' ;SID='^S-1-5-21-.*-512$' } 43 | [PSCustomObject]@{Name='Domain Controllers' ;SID='^S-1-5-21-.*-516$' } 44 | [PSCustomObject]@{Name='Enterprise Admins' ;SID='S-1-5-21-.*-519' }#HeadOnly 45 | [PSCustomObject]@{Name='Event Log Readers' ;SID='S-1-5-32-573' } 46 | [PSCustomObject]@{Name='Group Policy Creators Owners' ;SID='^S-1-5-21-.*-520$' } 47 | [PSCustomObject]@{Name='Hyper-V Admistrators' ;SID='S-1-5-32-578' } 48 | [PSCustomObject]@{Name='Pre-Windows 2000 compatible Access' ;SID='S-1-5-32-554' } 49 | [PSCustomObject]@{Name='Print Operators' ;SID='S-1-5-32-550' } 50 | [PSCustomObject]@{Name='Protected Users' ;SID='^S-1-5-21-.*-525$' } 51 | [PSCustomObject]@{Name='Remote Desktop Users' ;SID='S-1-5-32-555' } 52 | [PSCustomObject]@{Name='Schema Admins' ;SID='S-1-5-21-.*-518' }#HeadOnly 53 | [PSCustomObject]@{Name='Server Operators' ;SID='S-1-5-32-549' } 54 | [PSCustomObject]@{Name='Incoming Forest Trust Builders' ;SID='S-1-5-32-557' }#HeadOnly 55 | [PSCustomObject]@{Name='Cryptographic Operators' ;SID='S-1-5-32-569' } 56 | [PSCustomObject]@{Name='Key Admins' ;SID='^S-1-5-21-.*-526$' }#HeadOnly 57 | [PSCustomObject]@{Name='Enterprise Key Admins' ;SID='^S-1-5-21-.*-527$' }#HeadOnly 58 | )###################### Add more SIDS if needed... #################################### 59 | 60 | 61 | 62 | Enum ScanType{ 63 | Mini 64 | MiniX 65 | Mimi 66 | MimiX 67 | Standard 68 | StandardX 69 | Advanced 70 | AdvancedX 71 | Extreme 72 | ExtremeX 73 | Custom 74 | } 75 | 76 | ####################################################### DataDog Obj 77 | 78 | # DataDog Object format 79 | Class DataDog{ 80 | [String]$Group 81 | [String]$SID 82 | [String]$Description 83 | [int]$DirectMbrCount 84 | [int]$NestedMbrCount 85 | [int]$PathCount 86 | [int]$UserPathCount 87 | [Array]$NodeWeight 88 | [String[]]$Cypher 89 | } 90 | 91 | ########################################################### 92 | 93 | <# 94 | .Synopsis 95 | Time 96 | .DESCRIPTION 97 | Time 98 | .EXAMPLE 99 | Time 100 | #> 101 | function Time{Get-Date -F hh:mm:ss} 102 | 103 | ########################################################### 104 | 105 | <# 106 | .Synopsis 107 | Invoke Cypher 108 | .DESCRIPTION 109 | Post Cypher Query to REST API 110 | Cypher $Query [$Params] [-expand ] 111 | Post Cypher Query to BH 112 | .EXAMPLE 113 | $query="MATCH (n:User) RETURN n" 114 | Cypher $Query -Expand $Null 115 | #> 116 | function Invoke-Cypher{ 117 | [CmdletBinding()] 118 | [Alias('Cypher')] 119 | Param( 120 | # Cypher Query 121 | [Parameter(Mandatory=1)][string]$Query, 122 | # Query Params [optional] 123 | [Parameter(Mandatory=0)][Hashtable]$Params, 124 | # Expand Props [Default to .data.data / Use -Expand $Null for raw objects] 125 | [Parameter(Mandatory=0)][Alias('x')][String[]]$Expand=@('data','data') 126 | ) 127 | # Uri 128 | $Uri = "http://localhost:7474/db/data/cypher" 129 | # Header 130 | $Header=@{'Accept'='application/json; charset=UTF-8';'Content-Type'='application/json'} 131 | # Query [spec chars to unicode] 132 | $Query=$($Query.ToCharArray()|%{$x=[Byte][Char]"$_";if($x-gt191-AND$x-le255){'\u{0:X4}'-f$x}else{$_}})-join'' 133 | # Body 134 | if($Params){$Body = @{params=$Params; query=$Query}|Convertto-Json} 135 | else{$Body = @{query=$Query}|Convertto-Json} 136 | # Call 137 | #Write-Verbose "[+][$(Time)] Querying Database..." 138 | Write-Verbose "[+][$(Time)] $Query" <#Ckeck $Body if strange chars#> 139 | $Reply = Try{Invoke-RestMethod -Uri $Uri -Method Post -Headers $Header -Body $Body -verbose:$false}Catch{$Oops = $Error[0].ErrorDetails.Message} 140 | # Format obj 141 | if($Oops){Write-Warning "$((ConvertFrom-Json $Oops).message)";Return} 142 | if($Expand){$Expand | %{$Reply = $Reply.$_}} 143 | # Output Reply 144 | if($Reply){Return $Reply} 145 | } 146 | #End 147 | 148 | ########################################################### 149 | 150 | <# 151 | .Synopsis 152 | BloodHound DB Info 153 | .DESCRIPTION 154 | Get BloodHound DB node and edge count 155 | .EXAMPLE 156 | DBInfo 157 | #> 158 | function Get-BloodHoundDBInfo{ 159 | [Alias('DBInfo')] 160 | Param() 161 | Write-Verbose "[+][$(Time)] Fetching DB Info..." 162 | [PSCustomObject]@{ 163 | Domains = (Cypher 'MATCH (x:Domain) RETURN COUNT(x)' -expand Data)[0] 164 | Nodes = (Cypher 'MATCH (x) RETURN COUNT(x)' -expand Data)[0] 165 | Users = (Cypher 'MATCH (x:User) WHERE EXISTS(x.domain) RETURN COUNT(x)' -expand Data)[0] 166 | Computers = (Cypher 'MATCH (x:Computer) RETURN COUNT(x)' -expand Data)[0] 167 | Groups = (Cypher 'MATCH (x:Group) RETURN COUNT(x)' -expand Data)[0] 168 | OUs = (Cypher 'MATCH (x:OU) RETURN COUNT(x)' -expand Data)[0] 169 | GPOs = (Cypher 'MATCH (x:GPO) RETURN COUNT(x)' -expand Data)[0] 170 | Edges = (Cypher 'MATCH (x)-[r]->() RETURN COUNT(r)' -expand Data)[0] 171 | ACLs = (Cypher "MATCH x=(s)-[r]->(t) WHERE r.isacl=True RETURN COUNT(x)" -Expand Data)[0] 172 | Sessions = (Cypher "MATCH p=(s:Computer)-[r:HasSession]->(t:User) RETURN COUNT(r)" -expand Data)[0] 173 | }} 174 | #####End 175 | 176 | ########################################################### 177 | 178 | <# 179 | .Synopsis 180 | BloodHound DataDog 181 | .DESCRIPTION 182 | BloodHound node metrics on user shortest path to specified target group 183 | .EXAMPLE 184 | DataDog 'DOMAIN ADMINS@DOMAIN.LOCAL','BACKUP OPERATORS@DOMAIN.LOCAL' 185 | #> 186 | Function Invoke-DataDog{ 187 | [Alias('DataDog')] 188 | [OutputType([Datadog])] 189 | Param( 190 | # Name of the Group to Scan 191 | [Parameter(Mandatory=1,ValueFromPipeline=$true)][Alias('Group')][String[]]$Name, 192 | # Limit number of returned path 193 | [Parameter(Mandatory=0)][Int]$Limit=1000, 194 | # Scan Type 195 | [Parameter(Mandatory=0)][ScanType]$ScanType="Advanced", 196 | # Switch to All Shortest Paths 197 | [Parameter(Mandatory=0)][Switch]$AllShortest, 198 | # Switch Quick [less accurate] 199 | [Parameter(Mandatory=0)][Switch]$Quick, 200 | # Specify user origin 201 | [Parameter(Mandatory=0)][String]$UserDomain 202 | ) 203 | Begin{ 204 | $EdgeList = Switch("$ScanType".ToUpper()){ 205 | MINI {":MemberOf|AdminTo|HasSIDHistory"} 206 | MINIX {":MemberOf|AdminTo|HasSIDHistory|CanRDP|CanPSRemote|ExecuteDCOM"} 207 | MIMI {":MemberOf|HasSession|AdminTo|HasSIDHistory"} 208 | MIMIX {":MemberOf|HasSession|AdminTo|HasSIDHistory|CanRDP|CanPSRemote|ExecuteDCOM"} 209 | STANDARD {":MemberOf|HasSession|AdminTo|AllExtendedRights|AddMember|ForceChangePassword|GenericAll|GenericWrite|Owns|WriteDacl|WriteOwner|ReadLAPSPassword|ReadGMSAPassword"} 210 | STANTARDX{":MemberOf|HasSession|AdminTo|AllExtendedRights|AddMember|ForceChangePassword|GenericAll|GenericWrite|Owns|WriteDacl|WriteOwner|ReadLAPSPassword|ReadGMSAPassword|CanRDP|CanPSRemote|ExecuteDCOM"} 211 | ADVANCED {":MemberOf|HasSession|AdminTo|AllExtendedRights|AddMember|ForceChangePassword|GenericAll|GenericWrite|Owns|WriteDacl|WriteOwner|ReadLAPSPassword|ReadGMSAPassword|AllowedToDelegate|AddAllowedToAct|AllowedToAct|SQLAdmin|HasSIDHistory"} 212 | ADVANCEDX{":MemberOf|HasSession|AdminTo|AllExtendedRights|AddMember|ForceChangePassword|GenericAll|GenericWrite|Owns|WriteDacl|WriteOwner|ReadLAPSPassword|ReadGMSAPassword|AllowedToDelegate|AddAllowedToAct|AllowedToAct|SQLAdmin|HasSIDHistory|CanRDP|CanPSRemote|ExecuteDCOM"} 213 | EXTREME {":MemberOf|HasSession|AdminTo|AllExtendedRights|AddMember|ForceChangePassword|GenericAll|GenericWrite|Owns|WriteDacl|WriteOwner|ReadLAPSPassword|ReadGMSAPassword|AllowedToDelegate|AddAllowedToAct|AllowedToAct|SQLAdmin|HasSIDHistory|Contains|GpLink"} 214 | EXTREMEX {""} 215 | CUSTOM {":MemberOf|HasSession|AdminTo|AllExtendedRights|AddMember|ForceChangePassword|GenericAll|GenericWrite|Owns|WriteDacl|WriteOwner|ReadLAPSPassword|ExecuteDCOM|AllowedToDelegate|AddAllowedToAct|AllowedToAct"} 216 | } 217 | # if All Shortest Path 218 | if($AllShortest){$q='allShortestPaths'}Else{$q='shortestPath'} 219 | # If Quick [No Order] 220 | if($Quick){$order=$Null}Else{$order='ORDER BY LENGTH(p) '} 221 | } 222 | Process{ 223 | Foreach($Obj in $Name){ 224 | # Get Group 225 | Write-Verbose "[?][$(Time)] Querying Group by Name" 226 | $Grp = Cypher "MATCH (g:Group {name:'$Obj'}) RETURN g" | select Name,objectid,description 227 | # If Group not found 228 | if(-NOT $Grp.name){ 229 | Write-Warning "[!][$(Time)] OBJECT NOT FOUND: $Obj`r`n" 230 | } 231 | # If Group found 232 | else{ 233 | # Name & stuff 234 | $SD = $Grp.objectid 235 | $Nme = $Grp.Name 236 | $Desc = $Grp.Description 237 | #if($UserDomain){$WhereDom=" WHERE m.name=~'@$($Grp.Name.split('@')[1])'"} 238 | if($UserDomain){$WhereDom=" WHERE m.name ENDS WITH '@$($UserDomain.ToUpper())'"} 239 | Write-Verbose "[*][$(Time)] $Nme" 240 | # Direct Members 241 | Write-Verbose "[.][$(Time)] Querying Direct Member Count" 242 | $Cypher1 = "MATCH (m:User)$WhereDom MATCH p=shortestPath((m)-[r:MemberOf*1]->(n:Group {name:'$NmE'})) RETURN COUNT(m)" 243 | $DirectMbr = (Cypher $Cypher1 -expand data)|Select -first 1 244 | Write-Verbose "[.][$(Time)] > Direct Member: $DirectMbr" 245 | # Unrolled Members 246 | Write-Verbose "[.][$(Time)] Querying Nested Member Count" 247 | $cypher2 = "MATCH (m:User)$WhereDom MATCH p=shortestPath((m)-[r:MemberOf*1..]->(n:Group {name:'$NmE'})) RETURN COUNT(m)" 248 | $UnrollMbr =(Cypher $Cypher2 -expand data)|Select -first 1 249 | Write-Verbose "[.][$(Time)] > Nested Member: $($UnrollMbr-$DirectMbr)" 250 | # Shortest Path 251 | $Cypher3 = "MATCH (m:User)$WhereDom MATCH p=$q((m)-[r$EdgeList*1..]->(n:Group {name:'$NmE'})) RETURN p ${Order}LIMIT $Limit" 252 | Write-Verbose "[.][$(Time)] Querying User Shortest Paths" 253 | $RawData = Cypher $Cypher3 -expand data 254 | # User Path Count 255 | $PathCount = $RawData.count 256 | $UserCount = ($RawData.start|sort -unique).count 257 | Write-Verbose "[.][$(Time)] > UserPathCount: $UserCount" 258 | # Node Weight 259 | Write-Verbose "[.][$(Time)] Grouping Nodes" 260 | $AllNodeU = $RawData.nodes | Group | Select name,count 261 | Write-Verbose "[.][$(Time)] Mesuring Weight" 262 | $NodeWeight = @(Foreach($x in $AllNodeU){ 263 | # Name 264 | $Obj=irm $x.Name -Verbose:$false 265 | # Dist 266 | $Path = $RawData | ? {$_.nodes -match $x.name} | select -first 1 267 | $Step = $Path.Nodes.Count-1 268 | if($Path){while($Path.Nodes[$Step] -ne $x.name -AND $Step -gt 1){$Step-=1}} 269 | # Calc Weight 270 | $W=$X|select -expand Count 271 | # Out 272 | [PScustomObject]@{ 273 | Type = $Obj.metadata.labels[1] 274 | Name = $Obj.data.name 275 | Distance = ($Path.Nodes.Count)-$Step-1 276 | Weight = $W 277 | Impact = [Math]::Round($W/$RawData.Count*100,1) 278 | } 279 | }) 280 | 281 | # Cypher 282 | Write-Verbose "[.][$(Time)] Storing Cypher" 283 | $Cypher = @( 284 | $Cypher1.Replace('COUNT(m)','p') 285 | $Cypher1.Replace('COUNT(m)','{Type: "Direct", Name: m.name, SID: m.objectid} as obj') 286 | $Cypher2.Replace('COUNT(m)','p') 287 | $Cypher2.Replace('COUNT(m)','{Type: "Nested", Name: m.name, SID: m.objectid} as obj') 288 | $Cypher3 289 | $Cypher3.Replace("RETURN p ${Order}LIMIT $limit",'RETURN {Type: "Path", Name: m.name, SID: m.objectid} as obj') 290 | ) 291 | # Output DataDog Obj 292 | Write-Verbose "[+][$(Time)] Returning Object...`r`n" 293 | [DataDog]@{ 294 | Group = $Nme 295 | SID = $SD 296 | Description = $Desc 297 | DirectMbrCount= $DirectMbr 298 | NestedMbrCount= $UnrollMbr - $DirectMbr 299 | PathCount = $PathCount 300 | UserPathCount = $UserCount 301 | NodeWeight = $NodeWeight 302 | Cypher = $Cypher 303 | }}}} 304 | End{}########### 305 | } 306 | #End 307 | 308 | ########################################################### 309 | 310 | <# 311 | .Synopsis 312 | BloodHound Watchdog 313 | .DESCRIPTION 314 | Collect Path Data from default group for specified domain 315 | .EXAMPLE 316 | WatchDog domain.local 317 | #> 318 | Function Invoke-WatchDog{ 319 | [Alias('WatchDog')] 320 | [OutputType([Datadog])] 321 | Param( 322 | # Name of the domain to scan 323 | [Parameter()][String]$Domain, 324 | # Add extra Group Names 325 | [Parameter()][String[]]$ExtraGroup, 326 | # Limit Number of returned paths 327 | [Parameter(Mandatory=0)][Int]$Limit=1000, 328 | # Scan Type 329 | [Parameter()][ScanType]$ScanType='Advanced', 330 | # Switch to All Shortest Paths 331 | [Parameter(Mandatory=0)][Switch]$AllShortest, 332 | # Switch Quick [less accurate] 333 | [Parameter(Mandatory=0)][Switch]$Quick, 334 | # Specify user origin 335 | [Parameter(Mandatory=0)][String]$UserDomain 336 | ) 337 | # Domain to upper 338 | $Domain = $Domain.ToUpper() 339 | ## foreach in list ## 340 | foreach($Obj in $GroupList){ 341 | # Get Group 342 | $ObjID = if($Obj.SID -match '^S-1-5-32-'){"$Domain"+"-$($Obj.SID)"}else{"$($Obj.SID)"} 343 | Write-Verbose "[?][$(Time)] Searching Name by SID" 344 | $Grp = Cypher "MATCH (g:Group {domain:'$Domain'}) WHERE g.objectid =~ '(?i)$ObjID' RETURN g" | select Name,objectid,description 345 | # If Group not found 346 | if(-NOT $Grp.objectid){ 347 | Write-Warning "[!][$(Time)] OBJECT NOT FOUND: $($Obj.Name)`r`n" 348 | } 349 | # If Group found 350 | else{DataDog $Grp.name -ScanType $ScanType -AllShortest:$AllShortest -Quick:$Quick -Limit $Limit -UserDomain $UserDomain} 351 | } 352 | ## If Extra ## 353 | if($ExtraGroup){$ExtraGroup|DataDog -ScanType $ScanType -AllShortest:$AllShortest -Quick:$Quick -Limit $Limit -UserDomain $UserDomain} 354 | } 355 | #End 356 | 357 | ########################################################### 358 | 359 | <# 360 | .Synopsis 361 | Calc Ttl Impact - INTERNAL 362 | .DESCRIPTION 363 | Calculate Total Impact from Datadog Object Collection 364 | .EXAMPLE 365 | $Data | TotalImpact 366 | #> 367 | function Measure-TotalImpact{ 368 | [Alias('TotalImpact')] 369 | Param( 370 | # Datadog Objects [Piped from DataDog/WatchDog] 371 | [Parameter(Mandatory=1,ValueFromPipeline=1)][Datadog[]]$Data, 372 | # Filter on Node Type [optional] 373 | [ValidateSet('User','Group','Computer','GPO','OU')] 374 | [Parameter(Mandatory=0)][Alias('Filter')][String]$Type, 375 | # Limit to Top X [optional] 376 | [Parameter(Mandatory=0)][Alias('Limit')][Int]$Top 377 | ) 378 | Begin{[Collections.ArrayList]$Collect=@()} 379 | Process{foreach($Obj in ($data)){$Null=$Collect.add($Obj)}} 380 | End{ 381 | # Total Path Count 382 | $TtlPC=($Collect|measure -prop PathCount -sum).sum 383 | # Total Unique User Count 384 | $TtlUC= (($Collect.NodeWeight|? Type -eq User).name| Sort -Unique ).count 385 | # Total Object 386 | $Res = $Collect.NodeWeight | ? Distance -ne 0 | Group Name |%{ 387 | $TtlW = ($_.Group|Measure-object -Property Weight -sum).sum 388 | [PSCustomObject]@{ 389 | Type= $_.Group[0].type 390 | Name= $_.Name 391 | Hit=$_|Select -expand Count 392 | Weight=$TtlW 393 | Impact=[Math]::Round($TtlW/$TtlPC*100,1) 394 | } 395 | } 396 | $res = $res | Sort Impact -Descending 397 | if($Type){$Res = $Res | ? type -eq $Type} 398 | if($Top){$res = $res | select -first $top} 399 | $res 400 | } 401 | } 402 | #End 403 | 404 | ########################################################### 405 | 406 | <# 407 | .Synopsis 408 | WatchDog Report 409 | .DESCRIPTION 410 | DataDog/WatchDog to readable text report 411 | .EXAMPLE 412 | $Data | ReportDog 413 | Will generate report out of Datadog objects 414 | $Data holds result of WatchDog/DataDog Command 415 | #> 416 | Function Invoke-ReportDog{ 417 | [Alias('ReportDog')] 418 | Param( 419 | [Parameter(ValueFromPipeline=1)][DataDog[]]$Data, 420 | [Parameter()][String]$File, 421 | [Parameter()][Switch]$NoDBInfo, 422 | [Parameter()][Switch]$NoTotal 423 | ) 424 | Begin{ 425 | # Empty Collector 426 | [Collections.ArrayList]$Total=@() 427 | # If DB Info [Default] 428 | if(-Not$NoDBInfo){ 429 | # DB Info 430 | "############################## 431 | 432 | ------------------------------ 433 | # DB Info # 434 | ------------------------------ 435 | $((Get-BloodHoundDBInfo|Out-String).trim()) 436 | 437 | ##############################" 438 | }} 439 | Process{ 440 | Foreach($Obj in $Data){ 441 | # Add to Total 442 | $Null=$Total.Add($Obj) 443 | # Output 444 | " 445 | ## $($Obj.group) ## 446 | 447 | SID: $($Obj.SID) 448 | Description: 449 | $($Obj.description) 450 | 451 | User Count 452 | ---------- 453 | Direct Members : $($Obj.DirectMbrCount) 454 | Nested Members : $($Obj.NestedMbrCount) 455 | Users w. Paths : $($Obj.UserPathCount) 456 | 457 | 458 | Top10 - Impact 459 | -------------- 460 | 461 | $(($Obj.NodeWeight|Sort Impact -Descending |Where distance -ne 0 |Select -first 10|ft|Out-String).trim()) 462 | 463 | 464 | Top5 User - Impact 465 | ------------------ 466 | 467 | $(($Obj.NodeWeight|? type -eq user|Sort Impact -Descending |Select -first 5|ft|Out-String).trim()) 468 | 469 | 470 | Top5 Computer - Impact 471 | ---------------------- 472 | 473 | $(($Obj.NodeWeight|? type -eq Computer|Sort Impact -Descending |Select -first 5|ft|Out-String).trim()) 474 | 475 | 476 | Top5 Group - Impact 477 | ------------------- 478 | 479 | $(($Obj.NodeWeight|? type -eq Group|Sort Impact -Descending|Where impact -ne 100|Select -first 5|ft|Out-String).trim()) 480 | 481 | 482 | # Cypher - Query 483 | ---------------- 484 | 485 | $($Obj.Cypher[4]) 486 | 487 | 488 | ##############################" 489 | }} 490 | End{# If Total 491 | if(-Not$NoTotal){ 492 | # Target Count 493 | $TC = $Total.Count 494 | # Total Path Count 495 | $PC = ($Total|measure -prop PathCount -sum).sum 496 | $TI = $Total|TotalImpact 497 | 498 | 499 | " 500 | ## TOTAL IMPACT ## 501 | ------------------ 502 | 503 | 504 | Top10 User - TotalImpact [ $TC : $PC : 100 ] 505 | ------------------------ 506 | 507 | $(($TI|Where Type -eq User|Sort Impact -Descending | Select -First 10 | FT | Out-String).trim()) 508 | 509 | 510 | Top10 Computer - TotalImpact [ $TC : $PC : 100 ] 511 | ---------------------------- 512 | 513 | $(($TI|Where Type -eq Computer |Sort Impact -Descending | Select -First 10 | FT | Out-String).trim()) 514 | 515 | 516 | Top10 Group - TotalImpact [ $TC : $PC : 100 ] 517 | ------------------------- 518 | 519 | $(($TI|Where Type -eq Group|Sort Impact -Descending | Select -First 10 | FT | Out-String).trim()) 520 | 521 | 522 | Top20 Overall - TotalImpact [ $TC : $PC : 100 ] 523 | --------------------------- 524 | 525 | $(($TI|Sort Impact -Descending | Select -First 20 | FT -AutoSize | Out-String).trim()) 526 | 527 | 528 | ############################## 529 | 530 | ## GROUP COUNT OVERVIEW ## 531 | -------------------------- 532 | 533 | $($Total| select group,directmbrcount,nestedmbrcount,userpathcount | Out-String) 534 | " 535 | } 536 | } 537 | } 538 | #####End 539 | 540 | ########################################################### --------------------------------------------------------------------------------