PSGraphPlus.psm1
Write-Verbose 'Importing from [C:\projects\psgraphplus\PSGraphPlus\private]' # .\PSGraphPlus\private\Add-DebugNote.ps1 function Add-DebugNote { param($Id, $Message) if ($script:DebugAST) { $debugID = New-Guid node -Name $debugID @{label = $Message; shape = 'plaintext'} edge -From $debugID -To $Id @{style = 'dotted'; arrowhead = 'none'} } } # .\PSGraphPlus\private\Get-AstMap.ps1 function Get-AstMap { [cmdletbinding()] param ( [Parameter( ValueFromPipeline, Position = 0 )] [AllowNull()] $Ast, $ParentID = $null, [ref]$ChildId = [ref]$null ) process { $lastId = $null $rank = [System.Collections.Generic.List[System.Object]]::new() :node foreach ($node in $Ast) { $type = $node.GetType().Name $id = $node.gethashcode() $rule = Get-AstRule -Name $type if ($rule) { if ($rule.Visible -or $Script:DebugAST -eq $true) { Add-DebugNote -Id $id -Message $type $ChildId.Value = $id if ( $rule.Container ) { throw 'not supported, subgraphs are buggy with these types of graphs' subgraph -Name $id -Attributes @{label = $rule.Label; labeljust = 'l'} -scriptblock { Get-AstMap -AST $node.$($rule.Container) } } else { $node | node -NodeScript {$id} -Attributes @{label = $rule.Label} } foreach ( $property in $rule.ChildProperty ) { Get-AstMap -AST $node.$property -ParentID $id } } else { $ChildId.Value = $id foreach ( $property in $rule.ChildProperty ) { Get-AstMap -AST $node.$property -ParentID $ParentID -ChildId $ChildId } continue node } } else { # hand crafted rules switch ( $type ) { 'ForStatementAst' { $node } 'IfStatementAST' { Add-DebugNote -Id $id -Message $type $ChildId.Value = $id node -Name $id @{label = "IF (...)"; color = 'blue'} $conditionID = New-Guid $conditionTrueID = new-guid node -Name $conditionID @{label = '( CONDITION )'; color = 'blue'; } Edge $id -To $conditionID Get-AstMap -AST $node.Clauses[0].Item1 -ParentID $conditionID node -Name $conditionTrueID @{label = 'IF TRUE'; color = 'blue'; shape = 'diamond'} Edge $id -To $conditionTrueID Get-AstMap -AST $node.Clauses[0].Item2 -ParentID $conditionTrueID $NextParent = $id $list = $node.Clauses for ($index = 1; $index -lt $list.count; $index++ ) { $child = $node.Clauses[$index] $ifElseID = New-Guid $conditionID = New-Guid $conditionTrueID = new-guid node -Name $ifElseID @{label = "IFELSE (...)"; color = 'blue'} edge $NextParent -To $ifElseID node -Name $conditionID @{label = '( CONDITION )'; color = 'blue'; } Edge $ifElseID -To $conditionID Get-AstMap -AST $child.Item1 -ParentID $conditionID node -Name $conditionTrueID @{label = 'IF TRUE'; color = 'blue'; shape = 'diamond'} Edge $ifElseID -To $conditionTrueID Get-AstMap -AST $child.Item2 -ParentID $conditionTrueID $NextParent = $ifElseID } $child = $node.ElseClause if ( $child ) { $elseId = New-Guid $conditionID = New-Guid $conditionTrueID = new-guid node -Name $elseId @{label = "ELSE"; color = 'blue'; shape = 'diamond'} edge $NextParent -To $elseId Get-AstMap -AST $child -ParentID $elseId } #continue node } 'HashtableAst' { Add-DebugNote -Id $id -Message $type $ChildId.Value = $id node -Name $id @{label = '@{...}'} foreach ($child in $node.KeyValuePairs) { $NextParent = 0 Get-AstMap -AST $child.Item1 -ParentID $id -ChildId ([ref]$NextParent) Get-AstMap -AST $child.Item2 -ParentID $NextParent } #$node } 'CommandAst' { #CommandElements $property = 'CommandElements' if ($node.$($property).count ) { $ChildId.Value = $id $child = $node.$($property)[0] Get-AstMap -AST $child -ParentID $ParentId -ChildId $ChildId Add-DebugNote -Id $ChildId.Value -Message $type } $command = Get-Command -Name $child.Value -ErrorAction Ignore $PrimaryParent = $ChildId.Value $NewParent = $ChildId.Value $NextParent = $ChildId.Value $list = $node.$($property) for ($index = 1; $index -lt $list.count; $index++ ) { $child = $node.$($property)[$index] Get-AstMap -AST $child -ParentID $NewParent -ChildId ([ref]$NextParent) $NewParent = $PrimaryParent if ( $child.GetType().name -eq 'CommandParameterAst' -and -not $command.Parameters.$($child.ParameterName).SwitchParameter ) { $NewParent = $NextParent } } continue node } 'PipelineAst' { $NextParent = $ParentID if ($Script:DebugAST) { $ChildId.Value = $id node -Name $id @{label = 'PipelineAst[]'} edge $ParentID -To $id $NextParent = $id } $ChildId.Value = $id Get-AstMap -AST $node.PipelineElements[0] -ParentID $NextParent -ChildId $ChildId Add-DebugNote -Id $ChildId.Value -Message $type $NewParent = $ChildId.Value $NextParent = $ChildId.Value $list = $node.PipelineElements for ($index = 1; $index -lt $list.count; $index++ ) { if ( $index -lt $list.count ) { $guid = New-Guid node -Name $guid @{label = "|"} edge $NewParent -To $guid $NewParent = $guid } Get-AstMap -AST $node.PipelineElements[$index] -ParentID $NewParent -ChildId ([ref]$NextParent) Add-DebugNote -Id $NextParent -Message $type $NewParent = $NextParent } continue node } default { $ChildId.Value = $id node -Name $id @{label = $type; color = 'red'} $guid = New-Guid node -Name $guid @{label = $node.extent.tostring()} edge $id -to $guid #Write-Host "Skipping type [$PSItem]" #continue node } } } if ($null -ne $lastId) { #edge $lastId -to $id } $rank.Add($id) $lastId = $id if ( $null -ne $ParentId ) { edge $ParentId -to $id } } if ($rank.Count -gt 1) { #edge -From $rank -LiteralAttribute '[style="invis"]' rank -Nodes $rank } } } # .\PSGraphPlus\private\Get-AstRule.ps1 function Get-AstRule { <# .Synopsis Gets a rendering rule for a specific AST object .Example Get-AstRule -Name 'IfStatementAst' .Notes Most AST items have very generic rules. #> [cmdletbinding()] param( # AST object type name [Parameter( Mandatory, Position = 0, ValueFromPipelineByPropertyName )] [ValidateNotNullOrEmpty()] [String] $Name ) begin { $astRules = @{ ScriptBlockExpressionAst = @{ ChildProperty = 'ScriptBlock' Label = {'ScriptBlock'} Visible = $false } CommandExpressionAst = @{ ChildProperty = 'Expression' Label = {'CommandExpression'} Visible = $false } ConstantExpressionAst = @{ Visible = $true Label = {$_.Value} } NamedBlockAst = @{ Visible = $true ChildProperty = 'Statements' Label = {'{0} Block' -f $_.BlockKind} } VariableExpressionAst = @{ Visible = $true Label = {$_.extent.tostring()} } AssignmentStatementAst = @{ Visible = $true Label = {("{0} {1}" -f $_.left.ToString(), $_.Operator)} ChildProperty = 'Right' } UnaryExpressionAst = @{ Visible = $true Label = {$_.extent.tostring()} } CommandParameterAst = @{ Visible = $true Label = {"-$($_.ParameterName)"} } ScriptBlockAst = @{ Visible = $false ChildProperty = 'ParamBlock', 'BeginBlock', 'ProcessBlock', 'EndBlock' Label = {'ScriptBlock'} } MemberExpressionAst = @{ Visible = $true Label = {$_.extent.tostring()} } InvokeMemberExpressionAst = @{ Visible = $true Label = {$_.extent.tostring()} } ArrayExpressionAst = @{ Visible = $false Label = '[System.Object[]]::New()@{' ChildProperty = 'SubExpression' } StringConstantExpressionAst = @{ Visible = $true Label = { "{0}" -f $_.extent.tostring() } } ExpandableStringExpressionAst = @{ Visible = $true Label = {$_.extent.tostring()} ChildProperty = 'NestedExpressions' } IndexExpressionAst = @{ Visible = $true Label = {$_.extent.tostring()} } ThrowStatementAst = @{ Visible = $true Label = {$_.extent.tostring()} } CmdletInfo = { Visible = $true Label = {$_.Name} } _HashtableAst = @{ Visible = $true Label = {'Hashtable'} ChildProperty = 'KeyValuePairs' } 'Tuple`2' = @{ Visible = $true Label = {$_.Item1} ChildProperty = 'Item1', 'Item2' } ParenExpressionAst = @{ Visible = $false Label = {'ParenExpression'} ChildProperty = 'Pipeline' } _IfStatementAST = @{ Visible = $true Label = 'IF' ChildProperty = 'Clauses', 'ElseClause' } StatementBlockAst = @{ Visible = $false Label = 'StatementBlock' ChildProperty = 'Statements' } BinaryExpressionAST = @{ Visible = $true Label = {$_.Operator} ChildProperty = 'Left', 'Right' } ForEachStatementAst = @{ Visible = $true Label = {'{0} foreach ( {1} in {2} )' -f $_.Label, $_.Variable, $_.Condition} ChildProperty = 'Condition', 'Body' #Container = 'Condition' } FunctionDefinitionAST = @{ Visible = $true Label = {$_.Name} ChildProperty = 'Parameters', 'Body' } SwitchStatementAST = @{ Visible = $true Label = {'{0} Switch ( {1} )' -f $_.Label, $_.Condition} ChildProperty = 'Condition', 'Clauses', 'Default' } TryStatementAST = @{ Visible = $true Label = 'TRY' ChildProperty = 'Body', 'CatchClauses', 'Finally' } CatchClauseAst = @{ Visible = $true Label = 'CATCH' ChildProperty = 'CatchTypes', 'Body' } DoUntilStatementAst = @{ Visible = $true Label = {'{0} DO UNTIL ( {1} )' -f $_.label, $_.Condition} ChildProperty = 'Condition', 'Body' } ReturnStatementAst = @{ Visible = $true Label = 'RETURN' ChildProperty = 'Pipeline' } SubExpressionAst = @{ Visible = $false Label = 'SubExpression' ChildProperty = 'SubExpression' } ArrayLiteralAst = @{ Visible = $true Label = {$_.extent.tostring()} #ChildProperty = 'Elements' } ContinueStatementAst = @{ Visible = $true Label = {$_.extent.tostring()} } BreakStatementAst = @{ Visible = $true Label = {$_.extent.tostring()} } ConvertExpressionAst = @{ Visible = $true Label = {$_.type.extent.tostring()} ChildProperty = 'Child' } WhileStatementAst = @{ Visible = $true Label = {'{0} WHILE ( {1} )' -f $_.Label, $_.Condition} ChildProperty = 'Condition', 'Body' } TypeDefinitionAst = @{ Visible = $true Label = {'{0} {1}' -f $_.TypeAttributes, $_.Name} ChildProperty = 'Attributes', 'Members' } PropertyMemberAst = @{ Visible = $true Label = {$_.extent.tostring()} ChildProperty = 'InitialValue' } TypeConstraintAst = @{ Visible = $true Label = {$_.extent.tostring()} ChildProperty = 'InitialValue' } ParameterAst = @{ Visible = $true Label = {$_.Name} ChildProperty = 'Attributes', 'DefaultValue' } AttributeAst = @{ Visible = $true Label = {'[{0}( ... )]' -f $_.TypeName} ChildProperty = 'NamedArguments', 'PositionalArguments' } NamedAttributeArgumentAst = @{ Visible = $true Label = {$_.ArgumentName} ChildProperty = 'Argument' } FunctionMemberAst = @{ Visible = $true Label = {'{0} {1}' -f $_.ReturnType, $_.Name} ChildProperty = 'Attributes', 'Parameters', 'Body' } ParamBlockAst = @{ Visible = $true Label = 'Param (...)' ChildProperty = 'Parameters' } ForStatementAst = @{ Visible = $true Label = {'{0} FOR ( {1}; {2}; {3})' -f $_.Label, $_.Initializer, $_.Condition, $_.Iterator} ChildProperty = 'Condition', 'Body' } } } process { try { $astRules[$Name] } catch { $PSCmdlet.ThrowTerminatingError( $PSItem ) } } } Write-Verbose 'Importing from [C:\projects\psgraphplus\PSGraphPlus\public]' # .\PSGraphPlus\public\Show-AstCommandGraph.ps1 Function Show-AstCommandGraph { <# .SYNOPSIS Generates a graph of the commands called in a script .DESCRIPTION Generates a graph of the commands called in a script .PARAMETER ScriptBlock a scriptblock to process .PARAMETER ScriptText The raw text of a script to process .PARAMETER Path The path to a script to process .PARAMETER AllCommands Show commands called that are not part of the script or module .PARAMETER AllCalls Will show a line for each time a function is called .PARAMETER Raw Produces a raw dot file. .EXAMPLE $script = { function test-function () { Write-Output 'test' } function other-function () { test-function } } $script | Show-AstCommandGraph .NOTES The core powershell cmdlets are filtered out of the graph. commands like foreach-object, write-verbose, ect just add noise #> [cmdletbinding(DefaultParameterSetName = 'ScriptBlock')] param( [parameter( ParameterSetName = 'ScriptBlock', ValueFromPipeline )] [ValidateNotNullOrEmpty()] [scriptblock] $ScriptBlock, [parameter( ParameterSetName = 'ScriptText' )] [ValidateNotNullOrEmpty()] [string] $ScriptText, [parameter( ParameterSetName = 'Path', ValueFromPipeline, Position = 0 )] [ValidateNotNullOrEmpty()] [string] $Path, [switch] $AllCommands, [switch] $AllCalls, [switch] $Raw ) process { if (![string]::IsNullOrWhiteSpace($Path)) { $ScriptText = Get-Content $Path -Raw -ErrorAction Stop } if (![string]::IsNullOrWhiteSpace($ScriptText)) { $ScriptBlock = [scriptblock]::Create($ScriptText) } $functions = $ScriptBlock.Ast | Select-AST -Type FunctionDefinitionAst $names = $functions.Name $commands = $names if ($AllCommands) { $commands = (Get-Command | Where-Object source -notlike Microsoft.PowerShell.* | Select-Object -ExpandProperty Name) + $names $commands = $commands | Select-Object -Unique } if ($null -ne $names) { $graph = Graph { node @{shape = 'box'} node $names foreach ($function in $functions) { $calls = $function.Body | Select-Ast -Type CommandAst $uniquecalls = $calls.commandelements | Where-Object StringConstantType -eq 'BareWord' | Select-Object -ExpandProperty Value -Unique:(-Not $AllCalls) | ForEach-Object {$commands -eq $_ } if ($uniquecalls) { edge $function.Name -To $uniquecalls } } } } if ($Raw) { $graph } else { $graph | Export-PSGraph -ShowGraph } } } # .\PSGraphPlus\public\Show-AstGraph.ps1 function Show-AstGraph { <# .Synopsis Creates a full AST diagram .Description Parses a script for all the AST elements and builds a graph out of them. .PARAMETER ScriptBlock a scriptblock to process .PARAMETER ScriptText The raw text of a script to process .PARAMETER Path The path to a script to process .PARAMETER Annotate Expand the graph and show AST object types .PARAMETER Raw Produces a raw dot file. .EXAMPLE $script = { function test-function () { Write-Output 'test' } function other-function () { test-function } } $script | Show-AstGraph .NOTES #> [cmdletbinding(DefaultParameterSetName = 'ScriptBlock')] param( [parameter( ParameterSetName = 'ScriptBlock', ValueFromPipeline )] [ValidateNotNullOrEmpty()] [scriptblock] $ScriptBlock, [parameter( ParameterSetName = 'ScriptText' )] [ValidateNotNullOrEmpty()] [string] $ScriptText, [parameter( ParameterSetName = 'Path', ValueFromPipeline, Position = 0 )] [ValidateNotNullOrEmpty()] [string] $Path, [switch] $Annotate, [switch] $Raw ) process { if (![string]::IsNullOrWhiteSpace($Path)) { $ScriptText = Get-Content $Path -Raw -ErrorAction Stop } if (![string]::IsNullOrWhiteSpace($ScriptText)) { $ScriptBlock = [scriptblock]::Create($ScriptText) } $script:DebugAst = $Annotate $ast = $ScriptBlock.Ast $nodesAndEdges = Get-AstMap -Ast $ast $options = @{ rankdir = 'LR' splines = 'true' nodesep = '0.6' } $graph = graph $options { node @{shape = 'box'} $nodesAndEdges } if ($Raw) { $graph } else { $graph | Export-PSGraph -ShowGraph } } } # .\PSGraphPlus\public\Show-GitGraph.ps1 enum Direction { BottomToTop TopToBottom RightToLeft LeftToRight } function Show-GitGraph { <# .SYNOPSIS Gets a graph of the git history .DESCRIPTION This will generate a graph showing the recent histroy of a project with the branches. .PARAMETER Path Local location of the Git repository .PARAMETER HistoryDepth How far back into history to show .PARAMETER Uri Allows the injection of a base URL for github projects .PARAMETER ShowCommitMessage This will show the git commit instead of the hash .PARAMETER Raw Output the raw graph without generating the image or showing it. Useful for testing. .PARAMETER Direction This sets the direction of the chart. .EXAMPLE Show-GitGraph .EXAMPLE Show-GitGraph -HistoryDepth 30 .EXAMPLE Show-GitGraph -Path c:\workspace\project -ShowCommitMessage .NOTES #> [CmdletBinding()] param( $Path = $PWD, [alias('Depth')] $HistoryDepth = 15, $Uri = 'https://github.com/KevinMarquette/PSGraph', [switch] $ShowCommitMessage, [switch] $Raw, [Direction] $Direction = [Direction]::LeftToRight ) begin { $directionMap = @{ [Direction]::TopToBottom = 'TB' [Direction]::BottomToTop = 'BT' [Direction]::LeftToRight = 'LR' [Direction]::RightToLeft = 'RL' } } process { Push-Location $Path # Git history with branch details $git = git log --format="%h|%p|%s" -n $HistoryDepth --branches=* | Select-Object -SkipLast 1 $HASH = 0 $PARENT = 1 $SUBJECT = 2 $branches = git branch -a -v $tagList = git show-ref --abbrev=7 --tags $current = git log -1 --pretty=format:"%h" $tagLookup = @{} foreach ($tag in $tagList) { $tagHash, $tagName = $tag -split ' ' if (-not $tagLookup.ContainsKey($tagHash)) { $tagLookup[$tagHash] = @() } $tagLookup[$tagHash] += $tagName.replace('refs/tags/', '') } $commits = @() $graph = graph git @{ rankdir = $directionMap[$Direction]; label = [regex]::Escape( $PWD); pack = 'true' } { Node @{shape = 'box'} foreach ($line in $git) { $data = $line.split('|') #$label = $data[$HASH] if ($ShowCommitMessage) { #$label = '{0}\n{1}' -f $data[$SUBJECT], $data[$HASH] $commitID = 'commit' + $data[$HASH] Node $commitID @{label = $data[$SUBJECT]; shape = 'plaintext'} Rank $commitID, $data[$HASH] Edge -From $commitID -To $data[$HASH] @{style = 'dotted'; arrowhead = 'none'} $commits = @($commitID) + @($commits) } Node -Name $data[$HASH] @{ URL = "{0}/commit/{1}" -f $Uri, $data[$HASH] } Edge -From $data[$PARENT].split(' ') -To $data[$HASH] #add tags if ($tagLookup.ContainsKey($data[$HASH])) { Node $tagLookup[$data[$HASH]] @{fillcolor = 'yellow'; style = 'filled'} Edge -From $tagLookup[$data[$HASH]] -To $data[$HASH] } } if ($commits.Count) { Edge $commits @{style = 'invis'} } # branches Node @{shape = 'box'; fillcolor = 'green'; style = 'filled'} foreach ($line in $branches) { if ($line -match '(?<branch>[\w/-]+)\s+(?<hash>\w+) (.+)') { Node $Matches.branch Edge $Matches.branch -To $Matches.hash } } # current commit Node $current @{fillcolor = 'gray'; style = 'filled'} } if ($Raw) { $graph } else { $graph | Export-PSGraph -ShowGraph } Pop-Location } } # .\PSGraphPlus\public\Show-NetworkConnectionGraph.ps1 function Show-NetworkConnectionGraph { <# .SYNOPSIS Generates a map of network connections .Description This graph will show the source and target IP addresses with each edge showing the ports .EXAMPLE Show-NetworkConnectionGraph .Example Show-NetworkConnectionGraph -ComputerName $server -Credential $Credential .NOTES #> [CmdletBinding( DefaultParameterSetName = 'Default' )] param( # Remote computer name [Parameter( ParameterSetName = 'Default' )] [string[]] $ComputerName, # Credential for authorization [Parameter( ParameterSetName = 'Default' )] [pscredential] $Credential, # Outputs the raw dot graph (for testing [switch] $Raw ) process { $session = @{} if ( $null -ne $ComputerName ) { $PSBoundParameters.Remove('Raw') $session = @{ CimSession = New-CimSession @PSBoundParameters } } elseif ( $CimSession ) { $session = @{ CimSession = $CimSession } } $netstat = Get-NetTCPConnection -State Established, TimeWait -ErrorAction SilentlyContinue @session $netstat = $netstat | Where-Object LocalAddress -NotMatch ':' $dns = Get-DnsClientCache @session | Where-Object data -in $netstat.RemoteAddress $graph = graph network @{rankdir = 'LR'; label = 'Network Connections'} { Node @{shape = 'rect'} $EdgeParam = @{ Node = $netstat FromScript = {$_.LocalAddress} ToScript = {$_.RemoteAddress} Attributes = @{label = {'{0}:{1}' -f $_.LocalPort, $_.RemotePort}} } Edge @EdgeParam Node $dns -NodeScript {$_.data} @{label = {'{0}\n{1}' -f $_.entry, $_.data}} } if ($Raw) { $graph } else { $graph | Export-PSGraph -ShowGraph } } } # .\PSGraphPlus\public\Show-ProcessConnectionGraph.ps1 function Show-ProcessConnectionGraph { <# .SYNOPSIS Generates a map of network connections .Description This graph will show the source and target IP addresses with each edge showing the ports .EXAMPLE Show-NetworkConnectionGraph .Example Show-NetworkConnectionGraph -ComputerName $server -Credential $Credential .NOTES #> [CmdletBinding( DefaultParameterSetName = 'Default' )] param( # Remote computer name [Parameter( ParameterSetName = 'Default' )] [string[]] $ComputerName, # Credential for authorization [Parameter( ParameterSetName = 'Default' )] [pscredential] $Credential, # Outputs the raw dot graph (for testing) [switch] $Raw ) process { $session = @{} if ( $null -ne $ComputerName ) { $PSBoundParameters.Remove('Raw') $session = @{ CimSession = New-CimSession @PSBoundParameters } } $netstat = Get-NetTCPConnection -State Established, TimeWait -ErrorAction SilentlyContinue @session $netstat = $netstat | Where-Object LocalAddress -NotMatch ':' $dns = Get-DnsClientCache @session | Where-Object data -in $netstat.RemoteAddress $process = Get-CIMInstance -ClassName CIM_Process @session | Where-Object ProcessId -in $netstat.OwningProcess $graph = graph network @{rankdir = 'LR'; label = 'Process Network Connections'} { Node @{shape = 'rect'} Node $process -NodeScript {$_.ProcessID} @{label = {'{0}\n{1}' -f $_.ProcessName, $_.ProcessID}} $EdgeParam = @{ Node = $netstat FromScript = {$_.OwningProcess} ToScript = {$_.RemoteAddress} Attributes = @{label = {'{0}:{1}' -f $_.LocalPort, $_.RemotePort}} } Edge @EdgeParam Node $dns -NodeScript {$_.data} @{label = {'{0}\n{1}' -f $_.entry, $_.data}} } if ($Raw) { $graph } else { $graph | Export-PSGraph -ShowGraph } } } # .\PSGraphPlus\public\Show-ServiceDependencyGraph.ps1 function Show-ServiceDependencyGraph { <# .SYNOPSIS Show the process dependency graph .DESCRIPTION Loads all processes and maps out the dependencies .EXAMPLE Show-ProcessDependencyGraph .NOTES General notes #> [CmdletBinding()] param( # Service Name [Parameter()] [string[]] $Name, # Remote computer name [Parameter()] [string[]] $ComputerName, # Credential for authorization [Parameter()] [pscredential] $Credential, # Outputs the raw dot graph (for testing) [switch] $Raw ) process { if ( $null -ne $ComputerName ) { Write-Verbose 'Connecting to remote system' $PSBoundParameters.Remove('Raw') $services = Invoke-Command @PSBoundParameters -ScriptBlock {Get-Service -Include *} } else { $services = Get-Service -Include * } if ($null -ne $Name) { Write-Verbose ( 'Filtering on name [{0}]' -f ( $Name -join ',' ) ) $services = foreach ($node in $services) { if ($node.Name -in $Name) { $node continue } foreach ($dependency in $node.ServicesDependedOn.Name) { if ( $dependency -in $Name) { $node continue } } } } if ( $null -eq $services ) { return } Set-NodeFormatScript {$_.tolower()} $graph = graph services @{rankdir = 'LR'; pack = 'true'} { Node @{shape = 'box'} Node $services -NodeScript {$_.name} @{ label = {'{0}\n{1}' -f $_.DisplayName, $_.Name} color = {If ($_.Status -eq 'Running') {'blue'}else {'red'}} } $linkedServices = $services | Where-Object {$_.ServicesDependedOn} Edge $linkedServices -FromScript {$_.Name} -ToScript {$_.ServicesDependedOn.Name} } Set-NodeFormatScript if ($Raw) { $graph } else { $graph | Export-PSGraph -ShowGraph } } } Write-Verbose 'Importing from [C:\projects\psgraphplus\PSGraphPlus\classes]' |