DependencySearch.psm1
function Get-AddPSSnapinFromAST { <# .SYNOPSIS Function finds calls of Add-PSSnapin command (including its alias asnp) in given AST and returns objects with used parameters and their values for each call. .DESCRIPTION Function finds calls of Add-PSSnapin command (including its alias asnp) in given AST and returns objects with used parameters and their values for each call. .PARAMETER AST AST object which will be searched. Can be retrieved like: $AST = [System.Management.Automation.Language.Parser]::ParseFile("C:\script.ps1", [ref] $null, [ref] $null) .PARAMETER source For internal use only. .EXAMPLE $AST = [System.Management.Automation.Language.Parser]::ParseFile("C:\script.ps1", [ref] $null, [ref] $null) Get-AddPSSnapinFromAST -AST $AST #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.Management.Automation.Language.Ast] $AST, [array] $source ) $usedCommand = $AST.FindAll( { $args[0] -is [System.Management.Automation.Language.CommandAst ] }, $true) if (!$usedCommand) { Write-Verbose "No command detected in given AST" return } $addPSSnapinCommandList = $usedCommand | ? { $_.CommandElements[0].Value -in "Add-PSSnapin", "asnp" } if (!$addPSSnapinCommandList) { Write-Verbose "No 'Add-PSSnapin' or its alias 'asnp' detected" return } foreach ($addPSSnapinCommand in $addPSSnapinCommandList) { $addPSSnapinCommandElement = $addPSSnapinCommand.CommandElements $addPSSnapinCommandElement = $addPSSnapinCommandElement | select -Skip 1 # skip Add-PSSnapin command itself Write-Verbose "Getting Add-PSSnapin parameters from: '$($addPSSnapinCommand.extent.text)' (file: $($addPSSnapinCommand.extent.File))" #region get parameter name and value for NAMED parameters $param = @{} $paramName = '' foreach ($element in $addPSSnapinCommandElement) { if ($paramName) { # variable is set true when parameter was found # this foreach cycle therefore contains parameter value if ($element.StaticType.Name -eq "String") { # one module was specified $param.$paramName = $element.Extent.Text -replace "`"|'" } elseif ($element.Elements) { # multiple modules were specified $param.$paramName = ($element.Elements | ? { $_.StaticType.Name -eq "String" } | select -ExpandProperty Value) -replace "`"|'" } else { # value passed from pipeline etc probably Write-Verbose "Unknown Add-PSSnapin '$paramName' parameter value" $param.$paramName = '<unknown>' } $paramName = '' continue } if ($element.ParameterName) { $paramName = $element.ParameterName # transform param. name shortcuts to their full name if necessary switch ($paramName) { { $_ -match "^n" } { $paramName = "Name" } } } } #endregion get parameter name and value for NAMED parameters if (!$param.Name) { Write-Verbose "PSSnapins are imported using positional parameter" # 'Name' parameter wasn't specified by name, but by position, search for entered values # 'Name' parameter is on first position $firstAddPSSnapinCommandElement = $addPSSnapinCommandElement | select -First 1 if ($firstAddPSSnapinCommandElement.Elements) { # multiple PSSnapin values were specified $param.Name = ($firstAddPSSnapinCommandElement.Elements | ? { $_.StaticType.Name -eq "String" } | select -ExpandProperty Value) -replace "`"|'" } elseif ($firstAddPSSnapinCommandElement.StaticType.Name -eq "String") { # one PSSnapin value was specified $param.Name = ($firstAddPSSnapinCommandElement | ? { $_.StaticType.Name -eq "String" } | select -ExpandProperty Value) -replace "`"|'" } else { Write-Verbose "Unknown Add-PSSnapin 'Name' parameter value" } } if (!$param.Name -or $param.Name -eq '<unknown>') { if ($source) { $sourceTxt = "source: " + @($source)[-1] } else { $sourceTxt = "file: " + $addPSSnapinCommand.extent.File } Write-Warning "Unable to detect PSSnapins added through Add-PSSnapin command: '$($addPSSnapinCommand.extent.text)' ($sourceTxt)" continue } # output object for each added PSSnapin $param.Name | % { [PSCustomObject]@{ Command = $addPSSnapinCommand.extent.text File = $addPSSnapinCommand.extent.File AddedPSSnapin = $_ } } } } function Get-CodeDependency { <# .SYNOPSIS Function finds dependencies for given PSH code/script/module. By dependencies, different kind of requirements are meant like: - PSH modules such code explicitly imports OR requires (using statement) OR requires, because uses commands defined in such module - PSSnapins - admin rights (using statement) .DESCRIPTION Function finds dependencies/requirements for given PSH code/script/module. a) When code/script is given: - code '#requires' statement is searched for required modules and their dependencies are gathered using option b) - code explicit module imports (calls of Import-Module) are searched and their dependencies are gathered using option b) - all commands used in the code are searched and: - if command is private, ignored or already processed it is skipped - if command is "known" (Get-Command founds it OR it's noun begin with 'Mg' and 'Find-MgGraphCommand' command founds it in Microsoft Graph commands OR Find-Command founds it in PSGallery): - dependencies for command "source" module are searched using option b) - if command text definition exists, it is searched too using option a) recursively b) When module is given: - module is searched in locally available modules ($env:PSModulePath) using name and optionally version - if not found, it is searched again online in PowerShell Gallery - 'dependencies' in module manifest is checked and option b) is called upon for every required module recursively - (if 'checkModuleFunctionsDependencies' switch is used) text definition of every command in module is searched for dependencies using option a) recursively (and module is downloaded from PSGallery if not found locally) TIP: Built-in modules and corresponding commands are skipped during search (because everyone have them). By default only given code dependencies are returned and no recursion to get also dependencies of dependencies is made :) .PARAMETER scriptPath Path to PSH script whose dependencies should be searched. .PARAMETER scriptContent PSH code whose dependencies should be searched. .PARAMETER moduleName PSH module name whose dependencies should be searched. .PARAMETER moduleVersion (optional) PSH module version whose dependencies should be searched. .PARAMETER moduleBasePath Base path of the module that should be checked. .PARAMETER checkModuleFunctionsDependencies Switch for searching dependencies also for all commands defined in processed modules. Such command cannot be binary a.k.a. plaintext definition has to be available so it is possible to process it using AST. This can significantly increase searching time! If command is not found in locally available modules, but in PSGallery, its source module will be downloaded from the PSGallery, to get the command text definition. By default just required modules defined in module manifest are used for getting module dependencies. But this information doesn't have to be 100% correct. .PARAMETER availableModules To speed up repeated function invocations, save all available modules into variable and use it as value for this parameter. By default this function caches all locally available modules before each run which can take several seconds. .PARAMETER goDeep Switch to check for dependencies not just in the given code, but even in its dependencies (recursively). A.k.a. get the whole dependency tree. To really get all dependencies and not just the ones for running analyzed code, use parameter 'getDependencyOfRequiredModule'. .PARAMETER dontSearchCommandInPSGallery Switch to skip searching unknown commands in PowerShell Gallery. Drawback of using PowerShell Gallery is that it is just guessing. Even though some module defines our command doesn't mean it is real source of it. Moreover command with the same name can be defined in multiple modules. .PARAMETER getDependencyOfRequiredModule By default modules that are required (because in code #requires statement or explicitly imported using 'Import-Module') are outputted, but not searched for their dependencies. This parameter can change that. Possible values: - scriptRequires - search dependencies of the module(s) from #requires statement - scriptImportedModules - search dependencies of explicitly imported modules - scriptSourceModule - search dependencies of command's source module (module where processed command is hosted) Using this parameter you can get all dependencies, even the ones that are not necessarily needed to run the analyzed code .PARAMETER allOccurrences Switch to output dependant module each time it was found to be required in the code. Useful if you want to get all commands that require some module and not just the first one found. .PARAMETER nonInteractive Switch to run the function without any user interruptions like if: - function finds unknown command in multiple PSGallery modules, it won't asks which one to search and uses all of them - function finds command definition in multiple local modules, it won't asks which one to search and uses all of them .PARAMETER unknownDependencyAsObject Switch to return dependency object with empty module 'name' property for commands whose dependencies cannot be retrieved (because command is unknown etc). Instead of just outputting the warning message. .PARAMETER processJustMSGraphSDK Switch for skipping all non-MSGraphSDK modules/commands. Used internally when called by Get-CodeGraphPermissionRequirement to speed up the processing. Works only if 'goDeep' parameter is not used, because you cannot skip any module/command, because it might use some Graph commands inside. .PARAMETER installNuget Switch for installing NuGet package provider in case it is missing. NuGet is required to be able to search for missing modules/commands in PSGallery (enables use of Find-Module and Find-Command) .EXAMPLE # cache locally available modules to make following calls faster $availableModules = @(Get-Module -ListAvailable) Get-CodeDependency -scriptPath "C:\scripts\Get-AzureServicePrincipalOverview.ps1" -availableModules $availableModules -Verbose Get dependencies of given script. .EXAMPLE $code = @' Import-Module AzureAD Connect-MsolService ... '@ Get-CodeDependency -scriptContent $code -Verbose Get dependencies of given code. .EXAMPLE Get-CodeDependency -moduleName MyModule Get dependencies of module MyModule. Such module has to be placed in any folder mentioned in $env:PSModulePath or must exist in PowerShell Gallery. Only dependencies defined in module manifest will be processed. .EXAMPLE Get-CodeDependency -moduleName MyModule -checkModuleFunctionsDependencies Get dependencies of module MyModule. Such module has to be placed in any folder mentioned in $env:PSModulePath or must exist in PowerShell Gallery. Dependencies defined in module manifest AND all commands such module defines will be processed. Therefore you will get all really required dependencies. because module official manifest doesn't have to exist, or have required modules defined at all (or just partially correct)! .EXAMPLE Get-CodeDependency -moduleBasePath 'C:\modules\AWS.Tools.Common\4.1.233' -Verbose Get dependencies of module AWS.Tools.Common version 4.1.233 (such module does NOT have to be placed in folder from $env:PSModulePath). Only dependencies defined in module manifest will be processed. .EXAMPLE #save current variable content, so it can be restored later $PSModulePathBkp = $env:PSModulePath # path to the folder where all my custom made modules are stored (and that is outside module auto-discovery paths) # this folder contains module 'MyCustomModule' and some other dependant modules which I want to use when searching for code dependencies (to avoid unnecessary error that such modules doesn't exist) $myPrivateModules = "C:\useful_powershell_modules" # add modules path if necessary if ($myPrivateModules -notin ($env:PSModulePath -split ";")) { $env:PSModulePath = $env:PSModulePath + ";$myPrivateModules" } # cache available modules including the extra ones $availableModules = Get-Module -ListAvailable Get-CodeDependency -moduleName MyCustomModule -availableModules $availableModules # restore previous version of $env:PSModulePath $env:PSModulePath = $PSModulePathBkp Get dependencies of MyCustomModule module that is placed in folder not listed in $env:PSModulePath that uses some other modules from such folder. Only dependencies defined in module manifest will be processed. .EXAMPLE Get-CodeDependency -scriptPath "C:\scripts\Get-AzureServicePrincipalOverview.ps1" -allOccurrences -nonInteractive -installNuget -unknownDependencyAsObject Get dependencies of given script. Redundancy dependencies will be outputted (for example in case multiple used commands are defined in the same module). In case command/module isn't found locally but is found in PSGallery, all such findings will be searched without asking the user to choose the "right one". In case NuGet is not installed, it will be, so the PSGallery can be searched. In case unknown command is found it will be outputted instead of just warning message. #> [CmdletBinding()] [Alias("Get-Dependency", "Get-PSHCodeDependency")] param ( [Parameter(Mandatory = $true, ParameterSetName = "scriptPath")] [ValidateScript( { if ((Test-Path -Path $_ -PathType leaf) -and $_ -match "\.ps1$") { $true } else { throw "$_ is not a ps1 file or it doesn't exist" } })] [string] $scriptPath, [Parameter(Mandatory = $true, ParameterSetName = "scriptContent")] [string] $scriptContent, [Parameter(Mandatory = $true, ParameterSetName = "moduleName")] [string] $moduleName, [Parameter(Mandatory = $false, ParameterSetName = "moduleName")] [string] $moduleVersion, [Parameter(Mandatory = $true, ParameterSetName = "moduleBasePath")] [ValidateScript( { if (Test-Path -Path $_ -PathType Container) { $true } else { throw "$_ is not a path to folder where module is stored. For example 'C:\modules\AWS.Tools.Common' or 'C:\modules\AWS.Tools.Common\4.1.233'" } })] [string] $moduleBasePath, [switch] $checkModuleFunctionsDependencies, [System.Collections.ArrayList] $availableModules = @(), [Alias("recurse")] [switch] $goDeep, [switch] $dontSearchCommandInPSGallery, [ValidateSet('scriptRequires', 'scriptImportedModules', 'scriptSourceModule')] [string[]] $getDependencyOfRequiredModule, [switch] $allOccurrences, [switch] $nonInteractive, [switch] $unknownDependencyAsObject, [switch] $processJustMSGraphSDK, [switch] $installNuget ) # check whether PSGallery can be used to retrieve commands/modules information if (!(Get-PackageProvider | ? Name -EQ "NuGet") -and $installNuget) { Write-Warning "Installing NuGet package manager" $null = Install-PackageProvider -Name "NuGet" -Force -ForceBootstrap } # modules available by default, will be therefore skipped $ignoredModule = 'AppBackgroundTask', 'AppLocker', 'AppvClient', 'Appx', 'AssignedAccess', 'BitLocker', 'BitsTransfer', 'BranchCache', 'CimCmdlets', 'ConfigCI', 'Defender', 'DeliveryOptimization', 'DirectAccessClientComponents', 'Dism', 'DnsClient', 'EventTracingManagement', 'International', 'iSCSI', 'ISE', 'Kds', 'LanguagePackManagement', 'Microsoft.PowerShell.Archive', 'Microsoft.PowerShell.Diagnostics', 'Microsoft.PowerShell.Host', 'Microsoft.PowerShell.LocalAccounts', 'Microsoft.PowerShell.Management', 'Microsoft.PowerShell.ODataUtils', 'Microsoft.PowerShell.Security', 'Microsoft.PowerShell.Utility', 'Microsoft.WSMan.Management', 'MMAgent', 'MsDtc', 'NetAdapter', 'NetConnection', 'NetEventPacketCapture', 'NetLbfo', 'NetNat', 'NetQos', 'NetSecurity', 'NetSwitchTeam', 'NetTCPIP', 'NetworkConnectivityStatus', 'NetworkSwitchManager', 'NetworkTransition', 'PcsvDevice', 'PersistentMemory', 'PKI', 'PnpDevice', 'PrintManagement', 'ProcessMitigations', 'Provisioning', 'PSDesiredStateConfiguration', 'PSDiagnostics', 'PSScheduledJob', 'PSWorkflow', 'PSWorkflowUtility', 'ScheduledTasks', 'SecureBoot', 'SmbShare', 'SmbWitness', 'StartLayout', 'Storage', 'StorageBusCache', 'TLS', 'TroubleshootingPack', 'TrustedPlatformModule', 'UEV', 'VpnClient', 'Wdac', 'Whea', 'WindowsDeveloperLicense', 'WindowsErrorReporting', 'WindowsSearch', 'WindowsUpdate', 'Microsoft.PowerShell.Operation.Validation', 'PackageManagement', 'Pester', 'PowerShellGet', 'PSReadline' # here will be saved downloaded modules from PowerShell Gallery $moduleTmpPath = "$env:TEMP\PSHModules" #region set functions default parameters $PSDefaultParameterValuesBkp = $PSDefaultParameterValues.Clone() if (!$PSDefaultParameterValues) { $PSDefaultParameterValues = @{} } # to minimize clutter in verbose output $PSDefaultParameterValues.'Import-Module:Verbose' = $false $PSDefaultParameterValues.'Get-Module:Verbose' = $false #endregion set functions default parameters #region create cache variables if ($availableModules) { Write-Verbose "Using given 'availableModules' as list of available modules" [System.Collections.ArrayList] $global:availableModules = $availableModules # } elseif ($PSBoundParameters.ContainsKey("availableModules")) { #TODO prikazy se stejne budou hledat lokalne prec get-command, tzn dava tohle vubec smysl? # Write-Warning "You choose to not provide 'availableModules'. All modules will be searched directly in the PSGallery instead of searching locally first" # [System.Collections.ArrayList] $global:availableModules = @() } else { Write-Warning "Caching locally available modules. To skip this step, use parameter 'availableModules'" [System.Collections.ArrayList] $global:availableModules = @(Get-Module -ListAvailable) } # array of already processed modules saved as psobjects where each object contains module name and (optionally) its version $global:processedModules = @() # array of already outputted modules saved as psobjects where each object contains module name and (optionally) its version $global:outputtedModules = @() # array of already processed commands $global:processedCommands = @() # array of already processed PSSnapins saved as psobjects where each object contains snapin name and (optionally) its version $global:processedPSSnapins = @() # if the code or some of its dependencies requires elevation $global:isElevationRequired = $false # hash where key is module BasePath and value is module private functions $global:modulePrivateFunction = @{} # hash where key is module BasePath and value is list of all module function definitions $global:moduleFunctionDefinition = @{} #endregion create cache variables #region helper functions function ConvertTo-FlatArray { # flattens input in case, that string and arrays are entered at the same time param ( [array] $inputArray ) foreach ($item in $inputArray) { if ($item -ne $null) { # recurse for arrays if ($item.gettype().BaseType -eq [System.Array]) { ConvertTo-FlatArray $item } else { # output non-arrays $item } } } } function Get-FunctionDefinitionFromAST { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] $AST, [switch] $recurse ) $AST.FindAll( { param([System.Management.Automation.Language.Ast] $AST) $AST -is [System.Management.Automation.Language.FunctionDefinitionAst] -and # Class methods have a FunctionDefinitionAst under them as well, but we don't want them. ($PSVersionTable.PSVersion.Major -lt 5 -or $AST.Parent -isnot [System.Management.Automation.Language.FunctionMemberAst]) }, [bool]$recurse) } function _getModulePrivateFunction { # get & cache module private functions [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $moduleBasePath ) if ($global:modulePrivateFunction.keys -contains $moduleBasePath) { return $global:modulePrivateFunction.$moduleBasePath } $result = Get-ModulePrivateFunction -moduleBasePath $moduleBasePath $global:modulePrivateFunction.$moduleBasePath = $result return $result } function Get-ModulePrivateFunction { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $moduleBasePath ) $moduleObj = Get-Module -FullyQualifiedName $moduleBasePath -ListAvailable -Verbose:$false if (!$moduleObj) { Write-Error "Module in path '$moduleBasePath' doesn't exist" return } $exportedCommand = $moduleObj.ExportedCommands.keys $scriptFile = Get-ChildItem (Join-Path $moduleBasePath "*") -Include "*.psm1", "*.ps1" -Recurse | select -ExpandProperty FullName foreach ($script in $scriptFile) { # get AST $errors = [System.Management.Automation.Language.ParseError[]]@() $tokens = [System.Management.Automation.Language.Token[]]@() $AST = [System.Management.Automation.Language.Parser]::ParseFile($script, [ref] $tokens, [ref] $errors) # get functions defined in the code, so I can ignore them when searching for dependencies (their content is checked though) $definedFunction = Get-FunctionDefinitionFromAST $AST $definedFunction.name | ? { $_ -notin $exportedCommand } } } function _getModuleFunctionDefinition { # get & cache module function definitions [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $moduleBasePath, [Parameter(Mandatory = $true)] [string] $commandName ) if ($global:moduleFunctionDefinition.keys -contains $moduleBasePath) { return ($global:moduleFunctionDefinition.$moduleBasePath | ? Name -EQ $commandName | select -First 1 -ExpandProperty Body) } $result = Get-ModuleFunctionDefinition -moduleBasePath $moduleBasePath $global:moduleFunctionDefinition.$moduleBasePath = $result return ($result | ? Name -EQ $commandName | select -First 1 -ExpandProperty Body) } function Get-ModuleFunctionDefinition { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateScript( { if (Test-Path -Path $_ -PathType Container) { $true } else { throw "$_ is not a folder" } })] [string] $moduleBasePath, [string] $commandName ) $moduleObj = Get-Module -FullyQualifiedName $moduleBasePath -ListAvailable -Verbose:$false if (!$moduleObj) { Write-Error "Module in path '$moduleBasePath' doesn't exist" return } $scriptFile = Get-ChildItem (Join-Path $moduleBasePath "*") -Include "*.psm1", "*.ps1" -Recurse | select -ExpandProperty FullName foreach ($script in $scriptFile) { # get AST $errors = [System.Management.Automation.Language.ParseError[]]@() $tokens = [System.Management.Automation.Language.Token[]]@() $AST = [System.Management.Automation.Language.Parser]::ParseFile($script, [ref] $tokens, [ref] $errors) # get functions defined in the code, so I can ignore them when searching for dependencies (their content is checked though) $definedFunction = Get-FunctionDefinitionFromAST $AST if ($commandName) { $definedFunction | ? { $_.Name -eq $commandName } } else { $definedFunction } } } function _getModuleDependency { [CmdletBinding()] param ( [Parameter(Mandatory = $true, ParameterSetName = "moduleObj")] [System.Management.Automation.PSModuleInfo] $module, [Parameter(Mandatory = $true, ParameterSetName = "moduleName")] [string] $moduleName, [Parameter(ParameterSetName = "moduleName")] [version] $moduleVersion, [switch] $processBuiltinModule, [int] $indent = 1, [switch] $dontOutputTheModuleItself, [array] $source, [string] $command, [switch] $dontSearchForDependencies ) #region helper functions function _getModule { [CmdletBinding()] param ([string] $moduleName, $moduleVersion, [int] $indent) Write-Verbose ("`t`t`t`t" * $indent + "- Searching for '$moduleName' (ver. $moduleVersion) in available modules") $module = $global:availableModules | ? Name -EQ $moduleName if ($moduleVersion) { $module = $module | ? Version -EQ $moduleVersion } #TODO muze byt vic se stejnym jmenem $module | select -First 1 } function _moduleIsProcessed { param ($moduleName, $moduleVersion) if (($moduleVersion -and ($global:processedModules | ? { $_.ModuleName -eq $moduleName -and $_.ModuleVersion -eq $moduleVersion })) -or (!$moduleVersion -and ($moduleName -in $global:processedModules.ModuleName))) { $true } else { $false } } function _moduleIsOutputted { param ($moduleName, $moduleVersion) if (($moduleVersion -and ($global:outputtedModules | ? { $_.ModuleName -eq $moduleName -and $_.ModuleVersion -eq $moduleVersion })) -or (!$moduleVersion -and ($moduleName -in $global:outputtedModules.ModuleName))) { $true } else { $false } } #endregion helper functions #region checks & output before start of the module processing if ($module) { $mName = $module.name $mVersion = $module.Version } else { $mName = $moduleName $mVersion = $moduleVersion } Write-Verbose ("`t`t`t" * $indent + "- Processing module '$mName' (ver. $mVersion)") if ($processJustMSGraphSDK -and !$goDeep -and $mName -notlike "Microsoft.Graph.*" ) { Write-Verbose ("`t`t`t`t" * $indent + "- Module '$mName' (ver. $mVersion) isn't Graph SDK module. Skipping") return } if ($mName -in $ignoredModule -and !$processBuiltinModule) { Write-Verbose ("`t`t`t`t" * $indent + "- Module '$mName' (ver. $mVersion) is built-in. Skipping") return } $moduleWasProcessed = _moduleIsProcessed -moduleName $mName -moduleVersion $mVersion $moduleWasOutputted = _moduleIsOutputted -moduleName $mName -moduleVersion $mVersion if (!$dontOutputTheModuleItself -and ($allOccurrences -or (!$moduleWasProcessed -and !$moduleWasOutputted))) { # OUTPUT module that is being processed [PSCustomObject]@{ Type = 'Module' Name = $mName Version = $mVersion RequiredBy = $command DependencyPath = ConvertTo-FlatArray $source } # make a note that module was outputted $global:outputtedModules += [PSCustomObject]@{ ModuleName = $mName ModuleVersion = $mVersion } } if ($dontSearchForDependencies) { Write-Verbose ("`t`t`t`t" * $indent + "- Searching for module '$mName' dependencies is skipped") return } # make a note that module was processed $global:processedModules += [PSCustomObject]@{ ModuleName = $mName ModuleVersion = $mVersion } if (!$dontOutputTheModuleItself -and !$goDeep) { return } if ($moduleWasProcessed) { Write-Verbose ("`t`t`t`t" * $indent + "- Module '$mName' (ver. $mVersion) was already processed. Skipping") return } #endregion checks & output before start of the module processing #region get module object if necessary if ($moduleName) { # module is defined by its name (and optionally version) # search must be made to get an actual module object with all relevant data $module = _getModule -moduleName $moduleName -moduleVersion $moduleVersion -indent $indent #region get module data from PSH Gallery if not found locally if (!$module) { if ($moduleVersion) { $moduleVersionTxt = "(ver. $moduleVersion) " } else { $moduleVersionTxt = $null } Write-Warning "- Module '$moduleName' $moduleVersionTxt`isn't present on this machine. Trying to find it in online PowerShell Gallery" # if ('Trusted' -ne ($Policy = (Get-PSRepository PSGallery).InstallationPolicy)) { # Set-PSRepository PSGallery -InstallationPolicy Trusted # } # get dependencies for every command this module defines # officially defined requirements don't have to be 100% correct if ($checkModuleFunctionsDependencies) { # module commands should be processed, therefore I try to download the module locally # if successful I will process the module as any other local module # define module path $modulePath = Join-Path $moduleTmpPath $moduleName # C:\modules\AWS.Tools.Common if ($moduleVersion) { $modulePath = Join-Path $modulePath $moduleVersion # C:\modules\AWS.Tools.Common\4.1.233 } $module = Get-Module -FullyQualifiedName $modulePath -ListAvailable -ErrorAction SilentlyContinue if ($module) { # module is already downloaded } else { # download missing module from PowerShell Gallery $param = @{ Name = $moduleName Path = $moduleTmpPath ErrorAction = 'Stop' Verbose = $false } if ($moduleVersion) { $param.RequiredVersion = $moduleVersion } try { [Void][System.IO.Directory]::CreateDirectory($moduleTmpPath) Write-Verbose ("`t`t`t`t" * $indent + "- Downloading module from the PowerShell Gallery to the '$moduleTmpPath'") Save-Module @param $module = Get-Module -FullyQualifiedName $modulePath -ListAvailable -ErrorAction SilentlyContinue # cache the result $null = $global:availableModules.add($module) } catch { if ($_ -like "*No match was found for the specified search criteria*") { Write-Warning "- Module isn't available in the PowerShell Gallery either" } else { Write-Error $_ } return } } } else { # commands defined in the module shouldn't be processed, just officially defined dependencies # therefore module won't be downloaded locally, information will be gathered from Gallery instead $param = @{ Name = $moduleName ErrorAction = 'Stop' Verbose = $false } if ($moduleVersion) { $param.RequiredVersion = $moduleVersion } try { Write-Verbose ("`t`t`t`t" * $indent + "- Searching for module in the PowerShell Gallery") if (Get-PackageProvider | ? Name -EQ "NuGet") { $pshgModule = Find-Module @param } else { Write-Warning ("`t`t`t`t`t" * $indent + "- PSGallery cannot be used to search for '$moduleName' module. NuGet is missing. Use 'installNuget' parameter to solve this.") return } } catch { if ($_ -like "*No match was found for the specified search criteria*") { Write-Warning "- Module isn't available in the PowerShell Gallery either" } else { Write-Error $_ } return } #region get dependencies for every required module (specified in the manifest) $moduleDependency = $pshgModule.Dependencies if ($moduleDependency) { $moduleDependency | % { $dependency = $_.getenumerator() if ($dependency.gettype().name -eq 'SZArrayEnumerator') { # multiple dependencies defined, expand once more $dependency = $dependency.getenumerator() } foreach ($moduleUrl in ($dependency | ? key -EQ 'CanonicalId' | select -exp Value)) { # CanonicalId looks like: powershellget:Microsoft.Graph.Authentication/[1.19.0]#https://www.powershellgallery.com/api/v2 $reqModuleName = ($moduleUrl -split "/")[0] -replace "powershellget:" $reqModuleVersion = ([regex]"\d+\.\d+\.\d+").Match($moduleUrl).value Write-Verbose ("`t`t`t`t" * $indent + "- Module '$moduleName' (ver. $moduleVersion) requires module $reqModuleName (ver. $reqModuleVersion)") # get dependencies of dependency :) $param = @{ moduleName = $reqModuleName indent = $indent + 1 Source = $moduleName Command = "<module manifest>" } if ($reqModuleVersion) { $param.moduleVersion = $reqModuleVersion } _getModuleDependency @param } } } else { Write-Verbose "`t- Didn't find any dependency" } #endregion get dependencies for every required module (specified in the manifest) return } } # module was searched in PowerShell Gallery #endregion get module data from PSH Gallery if not found locally } else { # module was passed as an object, no search is necessary } #endregion get module object if necessary #region get dependencies for every module, module in question requires through manifest $requiredModules = $module.RequiredModules if ($requiredModules) { $requiredModules | % { Write-Verbose ("`t`t`t`t" * $indent + "- Module '$($module.name)' (ver. $($module.version)) requires module $($_.name) (ver. $($_.version))") # required modules definition doesn't contain requirements for required modules :) # get dependencies of dependency :) _getModuleDependency -moduleName $_.name -moduleVersion $_.version -indent ($indent + 1) -source ($source, $module.name) -command "<module manifest>" } } else { Write-Verbose ("`t`t`t`t" * $indent + "- Module $($module.name) (ver. $($module.version)) doesn't require any modules") } #TODO vytahnout i dalsi DotNetFrameworkVersion, PowerShellVersion, RequiredAssemblies #endregion get dependencies for every module, module in question requires through manifest #region get dependencies for every command, module in question defines # officially defined requirements don't have to be 100% correct if ($checkModuleFunctionsDependencies) { # get private functions so I can ignore them later Write-Verbose ("`t`t`t`t" * $indent + "- Getting private functions defined in module '$mName'") $modulePrivateFunction = _getModulePrivateFunction -moduleBasePath $module.ModuleBase Write-Verbose ("`t`t`t`t" * $indent + "- Getting commands defined in module '$mName'") # cmdlets are binary, hence no text definition will be available $module.ExportedCommands.keys | ? { $_ -notin $module.ExportedAliases.keys -and $_ -notin $module.ExportedCmdlets.Keys } | % { $cmdName = $_ Write-Verbose ("`t`t`t`t`t" * $indent + "- Processing command '$cmdName'") # skip errors, because some module exports commands that doesn't exist $cmdData = Get-Command $cmdName -Module $module -Verbose:$false -ErrorAction SilentlyContinue | ? Name -EQ $cmdName # just exact matches (name can contain wildcard) $cmdDefinition = $cmdData.ScriptBlock # command body if (!$cmdDefinition) { # sometimes module details doesn't contain commands definition (even though its not binary module) Write-Verbose ("`t`t`t`t`t`t" * $indent + "- Unable to get command definition using Get-Command, trying to get it from AST of the '$mName' module") $cmdDefinition = _getModuleFunctionDefinition -moduleBasePath $module.ModuleBase -commandName $cmdName } if ($cmdDefinition) { Write-Verbose ("`t`t`t`t`t" * $indent + "- Getting command '$cmdName' dependencies from its definition") #TODO tim ze taham dependency z definition, tak nikdy neuvidim obsah #requires! (neni soucast tela funkce v modulech), proto muze byt kontrola per ps1 file lepsi _getScriptDependency -scriptContent $cmdDefinition -indent ($indent + 1) -source ($source, $mName, $cmdName) -ignoreCommand $modulePrivateFunction } else { Write-Warning ("`t`t`t`t`t" * $indent + "- Unable to get command '$cmdName' definition") if ($unknownDependencyAsObject) { [PSCustomObject]@{ Type = 'Module' Name = '' Version = '' RequiredBy = $cmdName DependencyPath = ConvertTo-FlatArray $source } } } } } #endregion get dependencies for every command, module in question defines } # end of _getModuleDependency function function _getScriptDependency { [CmdletBinding()] param ( [Parameter(Mandatory = $true, ParameterSetName = "scriptPath")] [ValidateScript( { if ((Test-Path -Path $_ -PathType leaf) -and $_ -match "\.ps1$") { $true } else { throw "$_ is not a ps1 file or it doesn't exist" } })] $scriptPath, [Parameter(Mandatory = $true, ParameterSetName = "scriptContent")] $scriptContent, [int] $indent = 1, [array] $source, [string[]] $ignoreCommand ) #region get commands used in the given code # get AST $errors = [System.Management.Automation.Language.ParseError[]]@() $tokens = [System.Management.Automation.Language.Token[]]@() if ($scriptPath) { $AST = [System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path $scriptPath), [ref] $tokens, [ref] $errors) } else { $AST = [System.Management.Automation.Language.Parser]::ParseInput($scriptContent, [ref] $tokens, [ref] $errors) } # get functions defined inside the code, so I can ignore them when searching for dependencies (their content is checked though) $definedFunction = Get-FunctionDefinitionFromAST $AST -recurse $usedCommand = $AST.FindAll( { $args[0] -is [System.Management.Automation.Language.CommandAst ] }, $true) # filter out items that are with 99% probability not a real commands $usedCommand = $usedCommand | ? { $_.CommandElements[0].Value -notmatch "\\|/|\.ps1$|\.exe$" } #endregion get commands used in the given code #region get&add used PSSnapins #region added using requires statement $requiresPSSnapIns = $AST.ScriptRequirements.RequiresPSSnapIns foreach ($PSSnapin in $requiresPSSnapIns) { Write-Verbose "PSSnapin '$($PSSnapin.Name)' is required" if ($PSSnapin.Name -in $global:processedPSSnapins.Name) { Write-Verbose "PSSnapin was already processed. Skipping" } else { # take a note $global:processedPSSnapins += [PSCustomObject]@{ Name = $PSSnapin.Name Version = $PSSnapin.Version } # OUTPUT processing pssnapin [PSCustomObject]@{ Type = 'PSSnapin' Name = $PSSnapin.Name Version = $PSSnapin.Version RequiredBy = "<requires statement>" DependencyPath = ConvertTo-FlatArray $source } # add pssnapin Write-Verbose "Importing used PSSnapin '$($PSSnapin.Name)' (to be able to get details using Get-Command later)" try { Add-PSSnapin -Name $($PSSnapin.Name) -ErrorAction Stop } catch { Write-Warning "Unable to add PSSnapin '$($PSSnapin.Name)'. Some used commands won't be processed probably. Error was $_" } } } #endregion added using requires statement #region added using Add-PSSnapin $addedPSSnapin = Get-AddPSSnapinFromAST $AST $source foreach ($PSSnapin in $addedPSSnapin) { $PSSnapinName = $PSSnapin.AddedPSSnapin Write-Verbose "PSSnapin '$PSSnapinName' is required" if ($PSSnapinName -in $global:processedPSSnapins.Name) { Write-Verbose "PSSnapin was already processed. Skipping" } else { # take a note $global:processedPSSnapins += [PSCustomObject]@{ Name = $PSSnapinName Version = $null } # OUTPUT processing pssnapin [PSCustomObject]@{ Type = 'PSSnapin' Name = $PSSnapinName Version = $null RequiredBy = $PSSnapin.Command DependencyPath = ConvertTo-FlatArray $source } # add pssnapin Write-Verbose "Importing used PSSnapin '$PSSnapinName' (to be able to get details using Get-Command later)" try { Add-PSSnapin -Name $PSSnapinName -ErrorAction Stop } catch { Write-Warning "Unable to add PSSnapin '$PSSnapinName'. Some used commands won't be processed probably. Error was $_" } } } #endregion added using Add-PSSnapin #endregion get&add used PSSnapins #region get dependencies for every module, command in question requires #region get dependencies for every module, command in question has in requires statement #TODO detekovat pouziti using Write-Verbose ("`t`t`t`t" * $indent + "- Getting code dependencies for every required MODULE") # get all required modules defined in requires statement $requiresModuleList = $AST.ScriptRequirements.RequiredModules if ($requiresModuleList) { Write-Verbose ("`t`t`t`t`t" * $indent + "- Processing modules from #requires statement") $requiresModuleList | ? { $_ } | % { $minimumVersion = $_.version $maximumVersion = $_.MaximumVersion $requiredVersion = $_.RequiredVersion $param = @{ moduleName = $_.Name moduleVersion = $requiredVersion indent = ($indent + 1) source = $source command = "<requires statement>" dontSearchForDependencies = $true } if ($getDependencyOfRequiredModule -contains "scriptRequires") { $param.dontSearchForDependencies = $false } _getModuleDependency @param } } #endregion get dependencies for every module, command in question has in requires statement #region get dependencies for every module imported using Import-Module (or ipmo alias) # ma smysl jen kvuli modulum ktere definuji promenne, typy atp a zjisteni konkretni verze modulu..jinak najdu moduly pres pouzite prikazy v kodu $importModuleCommandList = Get-ImportModuleFromAST $AST $source if ($importModuleCommandList) { Write-Verbose ("`t`t`t`t`t" * $indent + "- Processing modules from Import-Module command calls") $importModuleCommandList | % { # Write-Verbose "Module '$($_.ImportedModule)' is imported via command: $($_.Command)" #TODO resit i minimum/maximum verzi? $param = @{ moduleName = $_.ImportedModule moduleVersion = $_.RequiredVersion indent = ($indent + 1) source = $source command = $_.Command dontSearchForDependencies = $true } if ($getDependencyOfRequiredModule -contains "scriptImportedModules") { $param.dontSearchForDependencies = $false } _getModuleDependency @param } } #endregion get dependencies for every module imported using Import-Module (or ipmo alias) #endregion get dependencies for every module, command in question requires #TODO hledat i pres promenne ( i v param bloku!)? pokud pouziva takove ktere jsou nekde exportovane... #region get dependencies for every used function/cmdlet/alias #TODO prikazy s prefixem z naimportovaneho modulu (ziskat explicitne importovane moduly a pouzity prefix) # skip internal functions of the module where command in question is defined a.k.a. omit unnecessary warnings about unknown (private) commands if ($source) { # WARNING: I cannot be sure if I select correct command/module if there are multiple matches! #TODO source[-2] by asi mel obsahovat modul, ze ktereho prikaz pochazi, tzn bych ho mohl rovnou pouzit? $gcmData = Get-Command $source[-1] -Verbose:$false -ErrorAction SilentlyContinue | select -First 1 if ($gcmData.Module.ModuleBase) { Write-Verbose ("`t`t`t`t" * $indent + "- Getting private functions defined in command's source module '$($gcmData.ModuleName)' to ignore them") $modulePrivateFunction = _getModulePrivateFunction -moduleBasePath $gcmData.Module.ModuleBase $ignoreCommand += @($modulePrivateFunction) } } Write-Verbose ("`t`t`t`t" * $indent + "- Getting code dependencies for every used COMMAND") # list of prefixes added to commands imported from modules $importModulePrefix = $importModuleCommandList.Prefix | ? { $_ } # get dependencies of every used command foreach ($cmd in $usedCommand) { $cmdName = $cmd.CommandElements[0].Value $cmdCommand = $cmd.Extent.Text # remove command prefix added when importing command source module if ($importModulePrefix) { foreach ($prefix in $importModulePrefix) { $regPrefix = [regex]::escape($prefix) if ($cmdName -match "-$regPrefix") { $cmdNewName = $cmdName -replace "-$regPrefix", "-" Write-Verbose ("`t`t`t`t`t" * $indent + "- Replacing command to process '$cmdName' for '$cmdNewName'. Because name matches module prefix '$prefix'") $cmdName = $cmdNewName break } } } Write-Verbose ("`t`t`t`t`t" * $indent + "- Processing command '$cmdName'") if ($processJustMSGraphSDK -and !$goDeep -and $cmdName -notlike "*-Mg*") { Write-Verbose ("`t`t`t`t`t`t" * $indent + "- Not a Graph SDK function. Skipping") } elseif ($cmdName -in $definedFunction.name) { Write-Verbose ("`t`t`t`t`t`t" * $indent + "- Locally defined function. Skipping") } elseif ($cmdName -in $ignoreCommand) { Write-Verbose ("`t`t`t`t`t`t" * $indent + "- Ignored function. Skipping") } elseif ($cmdName -in $global:processedCommands) { # ignore (but what about same named functions defined in different modules?!) Write-Verbose ("`t`t`t`t`t`t" * $indent + "- Already processed. Skipping") } else { # it is externally defined command, hence should be processed # make a note that it was processed $global:processedCommands += $cmdName # get the command noun $cmdNoun = "" if ($cmdName -match "-") { $cmdNoun = $cmdName.split("-", 2)[1] } # get command details $cmdData = Get-Command $cmdName -All -Verbose:$false -ErrorAction SilentlyContinue | ? { ($_.ModuleName -or $_.CommandType -eq "Alias") -and $_.Name -EQ $cmdName } # just exact matches (name can contain wildcard) and defined in module if ($cmdData.count -gt 1) { #region try to guess the command real source Write-Verbose ("`t`t`t`t`t`t" * $indent + "- Defined in multiple modules. Trying to guess the right one") # try to limit the data just to module of the "source" if ($source) { Write-Verbose ("`t`t`t`t`t`t`t" * $indent + "- Trying 'source' $($source[-1])") $sourceCmdData = $cmdData | ? ModuleName -EQ $source[-1] if ($sourceCmdData) { # limit the command to the source module, but its just guessing! $cmdData = $sourceCmdData } else { # source isn't module probably, try to search it as command instead $sourceCmdData = Get-Command $source[-1] -All -Verbose:$false -ErrorAction SilentlyContinue | ? { ($_.ModuleName -or $_.CommandType -eq "Alias") -and $_.Name -eq $cmdName } # just exact matches (name can contain wildcard) and defined in module if ($sourceCmdData) { $sourceCmdData = $cmdData | ? ModuleName -In $sourceCmdData.ModuleName if ($sourceCmdData) { # limit the command to the source module (where source command is defined), but its just guessing! $cmdData = $sourceCmdData } } } } # try to limit the data to the explicitly imported module, but its just guessing! if ($cmdData.count -gt 1 -and $importModuleCommandList) { # if one of the modules from explicitly imported modules matches the command source module, its probably the right one Write-Verbose ("`t`t`t`t`t`t`t" * $indent + "- Trying imported modules") $sourceCmdData = $cmdData | ? ModuleName -In $importModuleCommandList.ImportedModule if ($sourceCmdData) { $cmdData = $sourceCmdData } } # try to limit the data to the module from requires list, but its just guessing! if ($cmdData.count -gt 1 -and $requiresModuleList) { # if one of the modules from requires list matches the command source module, its probably the right one Write-Verbose ("`t`t`t`t`t`t`t" * $indent + "- Trying required modules") $sourceCmdData = $cmdData | ? ModuleName -In $requiresModuleList.Name if ($sourceCmdData) { $cmdData = $sourceCmdData } } #endregion try to guess the command real source if ($cmdData.count -gt 1) { if ($nonInteractive) { Write-Warning "Command '$cmdName' is defined multiple times ($($cmdData.ModuleName -join ', ')). Searching for dependencies in all of them which is 100% not-correct :)" } else { # let the user pick which (if any) modules should be searched for dependencies $cmdData = $cmdData | Out-GridView -Title "Command '$cmdName' was found in multiple modules, select which to use for dependency retrieval" -OutputMode Multiple } } } #region get command dependencies if ($cmdData) { # Get-Command found the command Write-Verbose ("`t`t`t`t`t`t" * $indent + "- Command is available locally") foreach ($data in $cmdData) { # transform alias'es data to the original command's data if ($data.commandType -eq "alias") { Write-Verbose ("`t`t`t`t`t`t" * $indent + "- '$cmdName' is alias for '$($data.ResolvedCommandName)'") $data = Get-Command $data.ResolvedCommandName -Verbose:$false } $cmdSource = $data.source $cmdModule = $data.module # module that contains/defines this command $cmdDefinition = $data.ScriptBlock # command body if ($cmdSource -eq "Microsoft.PowerShell.Core" -or $cmdModule.Name -in $ignoredModule) { # built-in command, ignore Write-Verbose ("`t`t`t`t`t`t" * $indent + "- Skipping. Its built-in command.") continue } if ($cmdModule) { Write-Verbose ("`t`t`t`t`t`t" * $indent + "- Searching for dependencies in the command's source module '$($cmdModule.Name)'") # searching just using name, because I can't say for sure that specific version is needed # because it was found using Get-Command $param = @{ moduleName = $cmdModule.Name indent = ($indent + 1) source = ($source, $cmdName) command = $cmdCommand dontSearchForDependencies = $true } if ($getDependencyOfRequiredModule -contains "scriptSourceModule") { $param.dontSearchForDependencies = $false } _getModuleDependency @param } if ($cmdDefinition -and $goDeep) { Write-Verbose ("`t`t`t`t`t`t" * $indent + "- Searching for dependencies in the command's '$cmdName' body") _getScriptDependency -scriptContent $cmdDefinition.ToString() -indent ($indent + 1) -source ($source, $cmdName) } # if (!$cmdModule -and !$cmdDefinition) { # Write-Warning "Command $cmdName isn't defined in any module nor its definition was found. Skip getting its dependencies" # } if (!$cmdDefinition -and $goDeep) { Write-Warning "Command's $cmdName definition is missing. Skip getting its dependencies" } } } else { # Get-Command didn't find the command, try other options Write-Verbose ("`t`t`t`t`t`t" * $indent + "- Command isn't available locally") #region is it Microsoft Graph SDK command? if ($cmdNoun -cmatch "^Mg[A-Z]") { # it might be Microsoft Graph SDK command Write-Verbose ("`t`t`t`t`t`t`t" * $indent + "- Trying Microsoft Graph SDK") if (Get-Command "Find-MgGraphCommand" -ErrorAction SilentlyContinue) { $cmdData = @(Find-MgGraphCommand -Command $cmdName -ErrorAction SilentlyContinue) $data = $cmdData[0] if ($data) { $cmdModule = "Microsoft.Graph." + $data.module # module that contains/defines this command # Write-Verbose ("`t`t`t`t`t`t" * $indent + "- Searching for dependencies in the command's source module '$cmdModule'") Write-Verbose ("`t`t`t`t`t`t" * $indent + "- Command was found in module '$cmdModule'") if ($goDeep) { Write-Warning "Command's $cmdName definition is missing. Skip getting its dependencies" } # searching just using name, because I can't say for sure that specific version is needed # because it was found using Find-MgGraphCommand $param = @{ moduleName = $cmdModule indent = ($indent + 1) source = ($source, $cmdName) command = $cmdCommand dontSearchForDependencies = $true } if ($getDependencyOfRequiredModule -contains "scriptSourceModule") { $param.dontSearchForDependencies = $false } _getModuleDependency @param } else { # Find-MgGraphCommand didn't find the command Write-Warning "Unable to find command '$cmdName' (source: $((ConvertTo-FlatArray $source) -join ' >> ')) details using Get-Command (locally) nor Find-MgGraphCommand (in PSGallery)" } } else { Write-Warning "Unable to find command '$cmdName' (source: $((ConvertTo-FlatArray $source) -join ' >> ')) details using Get-Command. Because of 'Mg' prefix, it might be some Microsoft Graph SDK command, but Find-MgGraphCommand is missing to test it (get it by installing 'Microsoft.Graph.Authentication' module)" } } #endregion is it Microsoft Graph SDK command? #region is it in PSGallery? if (!$cmdData) { # it wasn't Graph SDK command either, trying to find it in registered repositories (PSGallery) if ($dontSearchCommandInPSGallery) { Write-Warning "Unable to find command '$cmdName' (source: $((ConvertTo-FlatArray $source) -join ' >> ')) details using Get-Command. Skip getting its dependencies" if ($unknownDependencyAsObject) { [PSCustomObject]@{ Type = 'Module' Name = '' Version = '' RequiredBy = $cmdName DependencyPath = ConvertTo-FlatArray $source } } } else { Write-Verbose ("`t`t`t`t`t`t`t" * $indent + "- Trying PSGallery") if (Get-PackageProvider | ? Name -EQ "NuGet") { $cmdData = Find-Command -Name $cmdName } else { Write-Warning ("`t`t`t`t`t" * $indent + "- PSGallery cannot be used to search for '$cmdName' command. NuGet is missing. Use 'installNuget' parameter to solve this.") } if ($cmdData) { if ($cmdData.count -gt 1) { #region try to guess the command real source Write-Verbose ("`t`t`t`t`t`t`t" * $indent + "- Defined in multiple modules. Trying to guess the right one") # try to limit the data to the explicitly imported module, but its just guessing! if ($cmdData.count -gt 1 -and $importModuleCommandList) { # if one of the modules from explicitly imported modules matches the command source module, its probably the right one Write-Verbose ("`t`t`t`t`t`t`t" * $indent + "- Trying imported modules") $sourceCmdData = $cmdData | ? ModuleName -In $importModuleCommandList.ImportedModule if ($sourceCmdData) { $cmdData = $sourceCmdData } } # try to limit the data to the module from requires list, but its just guessing! if ($cmdData.count -gt 1 -and $requiresModuleList) { # if one of the modules from requires list matches the command source module, its probably the right one Write-Verbose ("`t`t`t`t`t`t`t" * $indent + "- Trying required modules") $sourceCmdData = $cmdData | ? ModuleName -In $requiresModuleList.Name if ($sourceCmdData) { $cmdData = $sourceCmdData } } #endregion try to guess the command real source if ($cmdData.count -gt 1) { if ($nonInteractive) { Write-Warning "Found multiple modules ($($cmdData.ModuleName -join ', ')) in PSGallery that contains command '$cmdName'. Searching for dependencies in all of them which is 100% not-correct :)" } else { # let the user pick which (if any) modules should be searched for dependencies $cmdData = $cmdData | Out-GridView -Title "Command '$cmdName' was found in multiple modules in PSGallery, select which to use for dependency retrieval" -OutputMode Multiple } } } foreach ($data in $cmdData) { $cmdModule = $data.ModuleName # Write-Verbose ("`t`t`t`t`t`t" * $indent + "- Searching for dependencies in the command's PSGallery source module '$cmdModule'") Write-Verbose ("`t`t`t`t`t`t" * $indent + "- Command was found in PSGallery module '$cmdModule'") if ($goDeep) { Write-Warning "Command's $cmdName definition is missing. Skip getting its dependencies" } # searching just using name, because I can't say for sure that specific version is needed # because it was found using Find-Command #TODO novy atribut, ktery rekne, ze jen hadam? $param = @{ moduleName = $cmdModule indent = ($indent + 1) source = ($source, $cmdName) command = $cmdCommand dontSearchForDependencies = $true } if ($getDependencyOfRequiredModule -contains "scriptSourceModule") { $param.dontSearchForDependencies = $false } _getModuleDependency @param } } else { Write-Warning "Unable to find command '$cmdName' (source: $((ConvertTo-FlatArray $source) -join ' >> ')) details using Get-Command (locally) nor Find-Command (in PSGallery). Skip getting its dependencies" if ($unknownDependencyAsObject) { [PSCustomObject]@{ Type = 'Module' Name = '' Version = '' RequiredBy = $cmdName DependencyPath = ConvertTo-FlatArray $source } } } } } #endregion is it in PSGallery? } #endregion get command dependencies } } #endregion get dependencies for every used function/cmdlet/alias #region output various requirements from requires statement #TODO output other requirements too if ($AST.ScriptRequirements.IsElevationRequired) { # code requires elevation Write-Verbose ("`t`t`t`t`t" * $indent + "- Code requires elevation through #requires statement") if ($global:isElevationRequired) { Write-Verbose ("`t`t`t`t`t`t" * $indent + "- Elevation is already required. Skipping") } else { # take a note $global:isElevationRequired = $true # OUTPUT requirement [PSCustomObject]@{ Type = 'Requirement' Name = 'ElevationIsRequired' Version = $null # jen abych vracel stejny objekt jako u ostatnich requirementu RequiredBy = "<requires statement>" DependencyPath = ConvertTo-FlatArray $source } } } #endregion output various requirements from requires statement } # end of _getScriptDependency function #endregion helper functions #region start searching for dependencies if ($scriptPath -or $scriptContent) { # get code dependencies $param = @{} if ($scriptPath) { $param.source = $scriptPath $param.scriptPath = $scriptPath } if ($scriptContent) { $param.source = "*scriptContent*" $param.scriptContent = $scriptContent } _getScriptDependency @param } elseif ($moduleName) { # get module dependencies $param = @{ dontOutputTheModuleItself = $true moduleName = $moduleName source = $moduleName } if ($moduleVersion) { $param.moduleVersion = $moduleVersion } _getModuleDependency @param } elseif ($moduleBasePath) { # get module dependencies (by module path) $param = @{ dontOutputTheModuleItself = $true module = (Get-Module -FullyQualifiedName $moduleBasePath -ListAvailable -ErrorAction Stop) source = $moduleBasePath } _getModuleDependency @param } else { throw "undefined option" } #endregion start searching for dependencies # restore previous default parameter values $PSDefaultParameterValues = $PSDefaultParameterValuesBkp # cleanup downloaded modules Remove-Item $moduleTmpPath -Recurse -Force -ErrorAction SilentlyContinue } function Get-CodeDependencyStatus { <# .SYNOPSIS Function gets (module) dependencies of given script/module and warns you about possible problems. .DESCRIPTION Function gets (module) dependencies of given script/module and warns you about possible problems. What problems are checked: - given code uses command from module that is not explicitly required or imported - there is version mismatch between used and required modules - there is explicit requirement for module that is not being used - there is explicit import of a module that is not being used Beware that dependencies are not (on purpose) searched recursively. A.k.a dependencies of found dependencies are not checked :). .PARAMETER scriptPath Path to the ps1 script that should be checked. .PARAMETER moduleName Name of the module that should be checked. .PARAMETER moduleVersion (optional) version of the module that should be checked. .PARAMETER moduleBasePath Base path of the module that should be checked. .PARAMETER availableModules To speed up repeated function runs, save all available modules into variable and use it as value for this parameter. By default this function caches all available modules before each run which can take several seconds. .PARAMETER asObject Switch for returning psobjects instead of warning messages. .PARAMETER allOccurrences Switch to output even all problems including duplicities. For example one missing module can be used by several commands, so with this switch used, warning about such module will be outputted for each command. .EXAMPLE $availableModules = Get-Module -ListAvailable Get-CodeDependencyStatus -scriptPath C:\scripts\myScript.ps1 -availableModules $availableModules Get dependencies of given script and warns about possible problems. .EXAMPLE $availableModules = Get-Module -ListAvailable Get-CodeDependencyStatus -scriptPath C:\scripts\myScript.ps1 -availableModules $availableModules -resultAsObject Get dependencies of given script and warns about possible problems. Instead of warning messages, objects will be returned. .EXAMPLE $availableModules = Get-Module -ListAvailable Get-CodeDependencyStatus -moduleName MyModule -availableModules $availableModules Get dependencies of given module and warns about possible problems. Such module has to be available in $env:PSModulePath or in PowerShell Gallery. .EXAMPLE $availableModules = Get-Module -ListAvailable Get-CodeDependencyStatus -moduleBasePath 'C:\modules\AWS.Tools.Common\4.1.233' -availableModules $availableModules Get dependencies of given module and warns about possible problems. .NOTES Requires function Get-CodeDependency. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ParameterSetName = "scriptPath")] [ValidateScript( { if ((Test-Path -Path $_ -PathType leaf) -and $_ -match "\.ps1$") { $true } else { throw "$_ is not a ps1 file or it doesn't exist" } })] [string] $scriptPath, [Parameter(Mandatory = $true, ParameterSetName = "moduleName")] [string] $moduleName, [Parameter(Mandatory = $false, ParameterSetName = "moduleName")] [string] $moduleVersion, [Parameter(Mandatory = $true, ParameterSetName = "moduleBasePath")] [ValidateScript( { if (Test-Path -Path $_ -PathType Container) { $true } else { throw "$_ is not a path to folder where module is stored. For example 'C:\modules\AWS.Tools.Common' or 'C:\modules\AWS.Tools.Common\4.1.233'" } })] [string] $moduleBasePath, [System.Collections.ArrayList] $availableModules = @(), [switch] $asObject, [switch] $allOccurrences ) #region get dependencies $param = @{ noRecursion = $true checkModuleFunctionsDependencies = $true } if ($availableModules) { $param.availableModules = $availableModules } if ($scriptPath) { $param.scriptPath = $scriptPath } elseif ($moduleName) { $param.moduleName = $moduleName if ($moduleVersion) { $param.moduleVersion = $moduleVersion } } elseif ($moduleBasePath) { $param.moduleBasePath = $moduleBasePath } else { throw "undefined option" } $dependency = Get-CodeDependency @param #endregion get dependencies $usedModule = $dependency | ? { $_.Type -EQ "Module" -and $_.DependencyPath -NE $scriptPath } $explicitlyImportedModule = $dependency | ? { $_.Type -EQ "Module" -and $_.RequiredBy -Match "^(Import-Module|ipmo) " } if ($scriptPath) { $explicitlyRequiredModule = $dependency | ? { $_.Type -EQ "Module" -and $_.RequiredBy -EQ "<requires statement>" } } else { $explicitlyRequiredModule = $dependency | ? { $_.Type -EQ "Module" -and $_.RequiredBy -EQ "<module manifest>" } } if ($scriptPath) { # script check $suffixTxt = "(through #requires statement)" } else { # module check $suffixTxt = "(through module manifest)" } #region missing explicit module requirement if ($usedModule) { $processedModule = @() foreach ($module in $usedModule) { $mName = $module.Name if (!$allOccurrences -and $mName -in $processedModule) { continue } if ($mName -in $explicitlyImportedModule.Name -and $mName -notin $explicitlyRequiredModule.Name) { $msg = "Module '$mName' is explicitly imported, but not required $suffixTxt. Reason: '$($module.RequiredBy)'" $problem = "ModuleImportedButMissingRequirement" } elseif ($mName -notin $explicitlyImportedModule.Name -and $mName -notin $explicitlyRequiredModule.Name) { $msg = "Module '$mName' is used, but not required $suffixTxt. Reason: '$($module.RequiredBy)'" $problem = "ModuleUsedButMissingRequirement" } else { # module is required, no action needed continue } if ($asObject) { [PSCustomObject]@{ Module = $mName Problem = $problem Message = $msg } } else { Write-Warning $msg } $processedModule += $mName } } #endregion missing explicit module requirement #region version mismatch if ($usedModule) { $processedModule = @{} foreach ($module in $usedModule) { $mName = $module.Name $mVersion = $module.Version $explicitlyRequiredModuleVersion = ($explicitlyRequiredModule | ? name -EQ $mName).version if ($mVersion -and $mVersion -notin $explicitlyRequiredModuleVersion) { if (!$allOccurrences -and $mVersion -eq $processedModule.$mName) { continue } $msg = "Module '$mName' (thanks to command: '$($module.RequiredBy)') that is used, has different version ($mVersion) then explicitly required one ($($explicitlyRequiredModuleVersion -join ', ')) $suffixTxt" if ($asObject) { [PSCustomObject]@{ Module = $mName Problem = "ModuleVersionConflict" Message = $msg } } else { Write-Warning $msg } $processedModule.$mName = $mVersion } } } if ($explicitlyImportedModule) { $processedModule = @{} foreach ($module in $explicitlyImportedModule) { $mName = $module.Name $mVersion = $module.Version $explicitlyRequiredModuleVersion = ($explicitlyRequiredModule | ? name -EQ $mName).version if ($mVersion -and $mVersion -notin $explicitlyRequiredModuleVersion) { if (!$allOccurrences -and $mVersion -eq $processedModule.$mName) { continue } $msg = "Module '$mName' that is explicitly imported, has different version ($mVersion) then explicitly required one ($($explicitlyRequiredModuleVersion -join ', ')) $suffixTxt" if ($asObject) { [PSCustomObject]@{ Module = $mName Problem = "ModuleVersionConflict" Message = $msg } } else { Write-Warning $msg } $processedModule.$mName = $mVersion } } } #endregion version mismatch #region unnecessary module requirement if ($explicitlyImportedModule) { $processedModule = @() foreach ($module in $explicitlyImportedModule) { $mName = $module.Name if (!$allOccurrences -and $mName -in $processedModule) { continue } if ($mName -notin $usedModule.Name) { $msg = "Module '$mName' is explicitly imported, but not used" if ($asObject) { [PSCustomObject]@{ Module = $mName Problem = "ModuleImportedNotUsed" Message = $msg } } else { Write-Warning $msg } $processedModule += $mName } } } if ($explicitlyRequiredModule) { $processedModule = @() foreach ($module in $explicitlyRequiredModule) { $mName = $module.Name if (!$allOccurrences -and $mName -in $processedModule) { continue } if ($mName -notin $usedModule.Name) { $msg = "Module '$mName' is explicitly required, but not used" if ($asObject) { [PSCustomObject]@{ Module = $mName Problem = "ModuleRequiredNotUsed" Message = $msg } } else { Write-Warning $msg } $processedModule += $mName } } } #endregion unnecessary module requirement } function Get-CorrespondingGraphCommand { <# .SYNOPSIS Function finds corresponding Graph command for MSOnline and AzureAD commands. .DESCRIPTION Function finds corresponding Graph command for MSOnline and AzureAD commands. .PARAMETER commandName MSOnline or AzureAD command name. .EXAMPLE Get-CorrespondingGraphCommand Get-MsolUser Finds corresponding Graph command for Get-MsolUser command. A.k.a. Get-MgUser. .EXAMPLE $scripts = Get-ChildItem C:\scripts -Recurse -Filter "*.ps1" -file | ? name -Match "\.ps1$" | select -exp FullName $moduleList = @() "AzureAD", "AzureADPreview", "MSOnline", "AzureRM" | % { $module = Get-Module $_ -ListAvailable if ($module) { $moduleList += $module } else { Write-Warning "Module $_ isn't available on you system. Add it to `$env:PSModulePath or install using Install-Module?" } } $scripts | % { Get-ModuleCommandUsedInCode -scriptPath $_ -module $moduleList | Select-Object *, @{n = 'GraphCommand'; e = { (Get-CorrespondingGraphCommand $_.command).GraphCommand } } | Format-Table -AutoSize } Search all ps1 scripts in C:\scripts folder for commands defined in modules "AzureAD", "AzureADPreview", "MSOnline", "AzureRM". Show where they are used and if possible also equivalent Graph command. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $commandName ) $cacheFile = "$env:TEMP\graphcommandmap.xml" if ((Test-Path $cacheFile -ea SilentlyContinue) -and ((Get-Item $cacheFile).LastWriteTime -gt [datetime]::Today.AddDays(-30))) { Write-Verbose "Using $cacheFile" $table = Import-Clixml $cacheFile } else { Write-Verbose "Getting command map" $uri = "https://learn.microsoft.com/en-au/powershell/microsoftgraph/azuread-msoline-cmdlet-map?view=graph-powershell-beta" $pageContent = (Invoke-WebRequest -Method GET -Uri $uri -UseBasicParsing).content $table = ConvertFrom-HTMLTable $pageContent -useHTMLAgilityPack -asArrayOfTables -all $table | Export-Clixml $cacheFile -Force } $table | % { $_ | select @{n = "Command"; e = { if ($_."Azure AD cmdlet") { $_."Azure AD cmdlet" } elseif ($_."MSOnline cmdlet") { $_."MSOnline cmdlet" } else { $_."Azure AD Preview cmdlet" } } }, @{n = "GraphCommand"; e = { $_."Microsoft Graph PowerShell cmdlet" } } } | select *, @{n = 'Note'; e = { if ($_.Command -like "* 1") { "This cmdlet has more than one cmdlet mapping in Microsoft Graph PowerShell" } elseif ($_.Command -like "* 2") { "Privileged Identity Management (PIM) iteration 3 APIs (https://learn.microsoft.com/en-us/graph/api/resources/privilegedidentitymanagementv3-overview?view=graph-rest-1.0) should be used. Check this guidance (https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-apis) for more details." } } } | select @{n = "Command"; e = { $_.Command -replace " \d+$" } }, GraphCommand, Note | ? Command -EQ $commandName } function Get-ImportModuleFromAST { <# .SYNOPSIS Function finds calls of Import-Module command (including its alias ipmo) in given AST and returns objects with used parameters and their values for each call. .DESCRIPTION Function finds calls of Import-Module command (including its alias ipmo) in given AST and returns objects with used parameters and their values for each call. .PARAMETER AST AST object which will be searched. Can be retrieved like: $AST = [System.Management.Automation.Language.Parser]::ParseFile("C:\script.ps1", [ref] $null, [ref] $null) .PARAMETER source "Path" to provided AST that will be shown as a source instead of AST file path property. Used by Get-CodeDependency. .EXAMPLE $AST = [System.Management.Automation.Language.Parser]::ParseFile("C:\script.ps1", [ref] $null, [ref] $null) Get-ImportModuleFromAST -AST $AST #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.Management.Automation.Language.Ast] $AST, [array] $source ) $usedCommand = $AST.FindAll( { $args[0] -is [System.Management.Automation.Language.CommandAst ] }, $true) if (!$usedCommand) { Write-Verbose "No command detected in given AST" return } #region functions function ConvertTo-FlatArray { # flattens input in case, that string and arrays are entered at the same time param ( [array] $inputArray ) foreach ($item in $inputArray) { if ($item -ne $null) { # recurse for arrays if ($item.gettype().BaseType -eq [System.Array]) { ConvertTo-FlatArray $item } else { # output non-arrays $item } } } } function _source { if ($source) { "(source: $((ConvertTo-FlatArray $source) -join ' >> '))" } else { if ($importModuleCommand.extent.File) { "(source: $($importModuleCommand.extent.File))" } } } #endregion functions $importModuleCommandList = $usedCommand | ? { $_.CommandElements[0].Value -in "Import-Module", "ipmo" } if (!$importModuleCommandList) { Write-Verbose "No 'Import-Module' or its alias 'ipmo' detected" return } foreach ($importModuleCommand in $importModuleCommandList) { $importModuleCommandElement = $importModuleCommand.CommandElements $importModuleCommandElement = $importModuleCommandElement | select -Skip 1 # skip Import-Module command itself Write-Verbose "Getting Import-Module parameters from: '$($importModuleCommand.extent.text)' $(_source)" #region get parameter name and value for NAMED parameters $param = @{} $paramName = '' foreach ($element in $importModuleCommandElement) { if ($paramName) { # variable is set true when parameter was found # this foreach cycle therefore contains parameter value if ($paramName -eq "FullyQualifiedName" -and $element.StaticType.Name -eq "String") { # module name or path was specified $param.Name = $element.Extent.Text -replace "`"|'" } elseif ($paramName -eq "FullyQualifiedName" -and $element.StaticType.Name -eq "Hashtable") { # hashtable for 'FullyQualifiedName' parameter was specified $param.Name = ($element.KeyValuePairs | ? { $_.Item1.Value -eq "ModuleName" }).Item2.Extent.Text -replace "`"|'" $param.MinimumVersion = ($element.KeyValuePairs | ? { $_.Item1.Value -eq "ModuleVersion" }).Item2.Extent.Text -replace "`"|'" $param.MaximumVersion = ($element.KeyValuePairs | ? { $_.Item1.Value -eq "MaximumVersion" }).Item2.Extent.Text -replace "`"|'" $param.RequiredVersion = ($element.KeyValuePairs | ? { $_.Item1.Value -eq "RequiredVersion" }).Item2.Extent.Text -replace "`"|'" } elseif ($element.StaticType.Name -eq "String") { # one module was specified $param.$paramName = $element.Extent.Text -replace "`"|'" } elseif ($element.Elements) { # multiple modules were specified $param.$paramName = ($element.Elements | ? { $_.StaticType.Name -eq "String" } | select -ExpandProperty Value) -replace "`"|'" } else { # value passed from pipeline etc probably Write-Verbose "Unknown Import-Module '$paramName' parameter value" $param.$paramName = '<unknown>' } $paramName = '' continue } if ($element.ParameterName) { $paramName = $element.ParameterName # transform param. name shortcuts to their full name if necessary switch ($paramName) { { $_ -match "^f" } { $paramName = "FullyQualifiedName" } { $_ -match "^n" } { $paramName = "Name" } { $_ -match "^ma" } { $paramName = "MaximumVersion" } { $_ -match "^mi" } { $paramName = "MinimumVersion" } { $_ -match "^p" } { $paramName = "Prefix" } { $_ -match "^r" } { $paramName = "RequiredVersion" } } } } #endregion get parameter name and value for NAMED parameters if (!$param.Name) { Write-Verbose "Modules are imported using positional parameter" # 'Name' parameter wasn't specified by name, but by position, search for entered values # Name parameter is on first position $firstImportModuleCommandElement = $importModuleCommandElement | select -First 1 if ($firstImportModuleCommandElement.Elements) { # multiple module values were specified $param.Name = ($firstImportModuleCommandElement.Elements | ? { $_.StaticType.Name -eq "String" } | select -ExpandProperty Value) -replace "`"|'" } elseif ($firstImportModuleCommandElement.StaticType.Name -eq "String") { # one module value was specified $param.Name = ($firstImportModuleCommandElement | ? { $_.StaticType.Name -eq "String" } | select -ExpandProperty Value) -replace "`"|'" } else { Write-Verbose "Unknown Import-Module 'Name' parameter value" } } if (!$param.Name -or $param.Name -eq '<unknown>') { Write-Warning "Unable to detect module imported through command: '$($importModuleCommand.extent.text)' $(_source)" continue } # output object for each added module $param.Name | % { # I output separate object for each imported module, because in case path is used instead of name, different version for different modules can be specified # Import-Module "C:\DATA\repo\Pilot_PowerShell\modules\AutoItX\AutoItX.psd1", "C:\DATA\repo\Pilot_PowerShell\modules\AWS.Tools.Common\4.1.233\AWS.Tools.Common.psd1" $name = $_ Write-Verbose "Processing module '$name'" $itIsPath = $name -like "*\*" # get module version # INFO version can be from two part to four part a.k.a. from 0.0 to 0.0.0.0 $reqModuleVersion = $null $nameContainsVersion = $name -match "\\\d+(\.\d+){0,2}\.\d+" if ($param.RequiredVersion) { # RequiredVersion parameter overrides version specified in the module path Write-Verbose "Getting module version from RequiredVersion parameter" $reqModuleVersion = $param.RequiredVersion } elseif ($nameContainsVersion) { # path contain module version Write-Verbose "Getting module version from its path" $reqModuleVersion = ([regex]"\d+(\.\d+){0,2}\.\d+").Match($name).value } if ($itIsPath) { # replace path for name only if (Test-Path $name -PathType Leaf) { # path looks like C:\modules\AWS.Tools.Common\AWS.Tools.Common.psd1 $moduleName = [System.IO.Path]::GetFileNameWithoutExtension($name) } else { if ($nameContainsVersion) { # path looks like C:\modules\AWS.Tools.Common\4.1.233\... $moduleName = Split-Path ($name -replace "\\\d+(\.\d+){0,2}\.\d+\\.*") -Leaf } else { # path looks like C:\modules\AWS.Tools.Common $moduleName = ($name -split "\\")[-1] } } } else { $moduleName = $name } [PSCustomObject]@{ Command = $importModuleCommand.extent.text File = $importModuleCommand.extent.File ImportedModule = $moduleName MaximumVersion = $param.MaximumVersion MinimumVersion = $param.MinimumVersion Prefix = $param.Prefix RequiredVersion = $reqModuleVersion } } } } function Get-ModuleCommandUsedInCode { <# .SYNOPSIS Function for getting commands (defined in given module) that are used in given script. .DESCRIPTION Function for getting commands (defined in given module) that are used in given script. .PARAMETER scriptPath Path to the ps1 script that should be searched for used commands. .PARAMETER module Module(s) object whose commands/aliases will be searched in given script. Should be retrieved using Get-Module command a.k.a. has to exist on local system! .EXAMPLE $module = Get-Module MSOnline -ListAvailable | select -last 1 Get-ModuleCommandUsedInCode -scriptPath "C:\repo\AzureAD_monitoring\AzureAD_user_APS.ps1" -module $module Get all commands used in "AzureAD_user_APS.ps1" script that are defined in the module MSOnline. .EXAMPLE $scripts = Get-ChildItem C:\scripts -Recurse -Filter "*.ps1" -file | ? name -Match "\.ps1$" | select -exp FullName $moduleList = @() "AzureAD", "AzureADPreview", "MSOnline", "AzureRM" | % { $module = Get-Module $_ -ListAvailable if ($module) { $moduleList += $module } else { Write-Warning "Module $_ isn't available on you system. Add it to `$env:PSModulePath or install using Install-Module?" } } $scripts | % { Get-ModuleCommandUsedInCode -scriptPath $_ -module $moduleList } Search all ps1 scripts in C:\scripts folder for commands defined in modules "AzureAD", "AzureADPreview", "MSOnline", "AzureRM". #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateScript( { if ((Test-Path -Path $_ -PathType leaf) -and $_ -match "\.ps1$") { $true } else { throw "$_ is not a ps1 file or it doesn't exist" } })] [string] $scriptPath, [Parameter(Mandatory = $true)] [PSModuleInfo[]] $module ) #TODO if module has default prefix, add it to each exported commands # $modulePrefix = $module.Prefix #region get commands & aliases defined in the module # hash with where keys are function names defined in given module(s) and value is name of module where it is defined $definedCommand = @{} # fill the $definedCommand hash $module | % { $moduleObj = $_ $moduleObj.ExportedCommands.keys | % { $definedCommand.$_ = $moduleObj.Name } $moduleObj.ExportedAliases.keys | % { $definedCommand.$_ = $moduleObj.Name } if (($moduleObj.ExportedCommands.keys).count -eq 0 -and ($moduleObj.ExportedAliases.keys).count -eq 0) { Write-Warning "Module $($_.Name) doesn't contain any commands" } } #endregion get commands & aliases defined in the module #region get commands & aliases used in the script $AST = [System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path $scriptPath), [ref] $null, [ref] $null) $usedCommand = $AST.FindAll( { $args[0] -is [System.Management.Automation.Language.CommandAst ] }, $true) if (!$usedCommand) { Write-Warning "Script '$scriptPath' doesn't contain any commands" return } #endregion get commands & aliases used in the script #region output the results [System.Collections.ArrayList] $result = @() $usedCommand | % { $commandName = $_.CommandElements[0].Value $commandLine = $_.Extent.StartLineNumber if ($commandName -in $definedCommand.Keys) { $null = $result.add([PSCustomObject]@{ Command = $commandName Line = $commandLine Module = $definedCommand.$commandName Script = $scriptPath }) } } $result | Sort-Object -Property Command #endregion output the results } Export-ModuleMember -function Get-AddPSSnapinFromAST, Get-CodeDependency, Get-CodeDependencyStatus, Get-CorrespondingGraphCommand, Get-ImportModuleFromAST, Get-ModuleCommandUsedInCode Export-ModuleMember -alias Get-Dependency, Get-PSHCodeDependency |