PSGalleryExplorer.psm1
# This is a locally sourced Imports file for local development. # It can be imported by the psm1 in local development to add script level variables. # It will merged in the build process. This is for local development only. #region script variables function Get-DataLocation { $folderName = "PSGalleryExplorer" if ($PROFILE) { $script:dataPath = (Join-Path (Split-Path -Parent $PROFILE) $folderName) } else { $script:dataPath = "~\${$folderName}" } } $script:corps = @( 'AWS' 'Amazon' 'Amazon.com, Inc' 'Amazon Web Services' 'AtlassianPS' 'BAMCIS' 'Bentley Systems, Incorporated' 'BitTitan' 'BitTitan, Inc.' '(c) 2014 Microsoft Corporation. All rights reserved.' 'CData Software, Inc.' 'Chocolatey Software' 'Cisco Systems' 'Cisco' 'Cisco Systems, Inc.' 'DSC Community' 'Dell Inc.' 'Dell Technologies' 'Dell' 'DELL|EMC' 'DELL||EMC' 'Dell EMC' 'DevScope' 'Docker Inc.' 'Docker' 'Evotec' 'Google' 'Google Inc' 'Google Inc.' 'Hewlett Packard Enterprise Co.' 'Hewlett Packard Enterprise' 'Hewlett-Packard Enterprise' 'Hewlett Packard Enterprise Development LP' 'HP Development Company L.P.' 'HP Inc' 'HPE Storage, A Hewlett Packard Enterprise Company' 'https://github.com/ebekker/ACMESharp' 'Ironman Software, LLC' 'Ironman Software' 'JDH Information Technology Solutions, Inc.' 'JumpCloud' 'Kelverion' 'Kelverion Automation Limited' 'Lockstep Technology Group' 'Microsoft 365 Patterns and Practices' 'Microsoft (Xbox)' 'Microsoft Corp' 'Microsoft Inc.' 'Microsoft Corporation' 'Microsoft Corportation' 'Microsoft Corpration' 'Microsoft' 'Microsoft | Services' 'Microsoft CSS' 'MicrosoftCorporation' 'Microsoft Corp.' 'Microsoft Germany GmbH' 'Microsoft Support' 'MosaicMK Software LLC' 'MosaicMKSoftwareLLC' 'Mozilla Corporation' 'Nimble Storage, A Hewlett Packard Enterprise Company' 'Noveris Pty Ltd' 'Octopus Deploy Pty. Ltd' 'Octopus Deploy' 'Oracle Cloud Infrastructure' 'Oracle Corporation' 'Pentia A/S' 'Pentia' 'PowerShell.org' 'Pure Storage, Inc.' 'Red Gate Software Ltd.' 'SecureMFA' 'SecureMFA.com' 'SolarNet' 'SolarWinds Worldwide, LLC.' 'SolarWinds' 'SynEdgy Limited' 'Synergex International Corporation' 'Transitional Data Services, Inc.' 'VMware' 'VMware, Inc.' 'VMware Inc.' 'Virtual Engine' 'waldo.be' 'Worxspace' 'XtremIO Dell EMC' 'Yevrag35, LLC.' 'Zerto Ltd.' ) $script:regulars = @( 'BuildHelpers' 'BurntToast' 'Carbon' 'ChocolateyGet' 'CredentialManager' 'dbatools' 'Foil' 'ImportExcel' 'Invokebuild' 'Invoke-CommandAs' 'oh-my-posh' 'PendingReboot' 'PSDepend' 'PSDeploy' 'PSKoans' 'PSLogging' 'PSSlack' 'PSWindowsUpdate' 'Pester' 'PoshBot' 'posh-git' 'Posh-SSH' 'powershell-yaml' 'psake' 'RunAsUser' 'Selenium' 'SnipeitPS' 'SNMP' 'TeamViewerPS' 'Write-ObjectToSQL' ) $domain = 'cloudfront.net' $target = 'dfuu1myynofuh' Get-DataLocation $script:dataFileZip = 'PSGalleryExplorer.zip' $script:metadataFile = 'PSGalleryExplorer.json' $script:dataFile = 'PSGalleryExplorer.xml' $script:dlURI = '{0}.{1}' -f $target, $domain $script:glData = $null #endregion <# .SYNOPSIS Confirm data output location. Creates output dir if not present. .DESCRIPTION Evaluates presence of data output location for xml dataset. If the directory is not found, it will be created. .EXAMPLE Confirm-DataLocation Confirms presence of data output location. Creates if not found. .OUTPUTS System.Boolean .NOTES Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/ .COMPONENT PSGalleryExplorer #> function Confirm-DataLocation { [CmdletBinding()] param ( ) $result = $true #assume the best Write-Verbose -Message 'Verifying data set output location...' try { $pathEval = Test-Path -Path $script:dataPath -ErrorAction Stop } catch { $result = $false Write-Error $_ return $result } if (-not ($pathEval)) { Write-Verbose -Message 'Creating output directory...' try { $newItemSplat = @{ ItemType = 'Directory' Path = $script:dataPath ErrorAction = 'Stop' } $null = New-Item @newItemSplat Write-Verbose -Message 'Created.' } catch { $result = $false Write-Error $_ return $result } } #if_TestPath else { Write-Verbose 'Data path confirmed.' } #else_TestPath return $result } #Confirm-DataLocation <# .SYNOPSIS Compares the local metadata file to the remote metadata file. .DESCRIPTION Evaluates the local metadata file and compares it to the remote metadata file. If the files are the same, returns true. If the files are different, returns false. .EXAMPLE Confirm-MetadataUpdate Compares the local metadata file to the remote metadata file. .OUTPUTS System.Boolean .NOTES Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/ .COMPONENT PSGalleryExplorer #> function Confirm-MetadataUpdate { [CmdletBinding()] param ( ) $result = $true #assume the best Write-Verbose -Message 'Checking for metadata file...' $localMetaDataFilePath = [System.IO.Path]::Combine($script:dataPath, $script:metadataFile) try { $pathEval = Test-Path -Path $localMetaDataFilePath -ErrorAction Stop } catch { $result = $false Write-Error $_ return $result } if (-not ($pathEval)) { $result = $false } #if_pathEval else { Write-Verbose 'Metadata file found. Performing metadata comparison...' try { $localMetadata = Get-Content $localMetaDataFilePath -ErrorAction 'Stop' | ConvertFrom-Json } catch { $result = $false Write-Error $_ return $result } $tempMetadataFile = '{0}_temp' -f $script:metadataFile $tempMetadataFilePath = [System.IO.Path]::Combine($script:dataPath, $tempMetadataFile) # if the temp metadata file exists, delete it if (Test-Path -Path $tempMetadataFile) { Remove-Item -Path $tempMetadataFilePath -Force } # download metadata file for comparison $fileFetchStatus = Get-RemoteFile -File $script:metadataFile -OutFile $tempMetadataFile if ($fileFetchStatus -eq $false) { Write-Error 'Unable to download metadata file.' $result = $false return $result } try { $remoteMetadata = Get-Content $tempMetadataFilePath -ErrorAction 'Stop' | ConvertFrom-Json } catch { $result = $false Write-Error $_ return $result } Write-Verbose -Message ('{0} vs {1}' -f $localMetadata.zipCreated, $remoteMetadata.zipCreated) if ($localMetadata.zipCreated -eq $remoteMetadata.zipCreated) { Write-Verbose 'Metadata file is current.' } else { Write-Verbose 'Metadata file requires refresh.' $result = $false } } return $result } #Confirm-MetadataUpdate <# .SYNOPSIS Confirms the XML dataset file is available and not beyond the expiration time. .DESCRIPTION Determines if the XML dataset file is stale or not available. If the file is not available, false will be returned so it can be downloaded. If the file is available, but over 9 days old, the metadata file will be checked to see if an update is available. If an update is available after the metadata file is checked, false will be returned so the data file can be refreshed. .EXAMPLE Confirm-XMLDataSet Checks for XML dataset and determines if it is 9 days older or more. .OUTPUTS System.Boolean .NOTES Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/ .COMPONENT PSGalleryExplorer #> function Confirm-XMLDataSet { [CmdletBinding()] param ( ) $result = $true #assume the best $dataFile = '{0}/{1}' -f $script:dataPath, $script:dataFile Write-Verbose -Message 'Confirming valid and current data set...' # if the file doesn't exist, we need to download it Write-Verbose -Message 'Checking for data file...' try { $pathEval = Test-Path -Path $dataFile -ErrorAction Stop } catch { $result = $false Write-Error $_ return $result } if (-not ($pathEval)) { $result = $false } #if_pathEval else { Write-Verbose 'Data file found. Checking date of file...' try { $fileData = Get-Item -Path $dataFile -ErrorAction Stop } catch { $result = $false Write-Error $_ return $result } if ($fileData) { $creationDate = $fileData.CreationTime $now = Get-Date if (($now - $creationDate).Days -ge 9) { # Write-Verbose 'Data file requires refresh.' Write-Verbose 'Data file is older than 9 days. Checking if an update is available...' $metadataStatus = Confirm-MetadataUpdate if ($metadataStatus -eq $false) { Write-Verbose 'Refreshing data file...' $result = $false } else { Write-Verbose 'No update available. Data file is current.' } } else { Write-Verbose 'Data file verified' } } #if_fileData else { Write-Warning 'Unable to retrieve file information for PSGalleryExplorer data set.' $result = $false return $result } #else_fileData } #else_pathEval return $result } #Confirm-XMLDataSet <# .SYNOPSIS Unzips the XML data set. .DESCRIPTION Evaluates for previous version of XML data set and removes if required. Expands the XML data set for use. .EXAMPLE Expand-XMLDataSet Unzips and expands the XML data set. .OUTPUTS System.Boolean .NOTES Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/ .COMPONENT PSGalleryExplorer #> function Expand-XMLDataSet { [CmdletBinding()] param ( ) $result = $true #assume the best $dataFile = '{0}/{1}' -f $script:dataPath, $script:dataFile Write-Verbose -Message 'Testing if data set file already exists...' try { $pathEval = Test-Path -Path $dataFile -ErrorAction Stop Write-Verbose -Message "EVAL: $true" } catch { $result = $false Write-Error $_ return $result } if ($pathEval) { Write-Verbose -Message 'Removing existing data set file...' try { $removeItemSplat = @{ Force = $true Path = $dataFile ErrorAction = 'Stop' } Remove-Item @removeItemSplat } #try catch { $result = $false Write-Error $_ return $result } #catch } #if_pathEval Write-Verbose -Message 'Expanding data set archive...' try { $expandArchiveSplat = @{ DestinationPath = $script:dataPath Force = $true ErrorAction = 'Stop' Path = '{0}/{1}' -f $script:dataPath, $script:dataFileZip } $null = Expand-Archive @expandArchiveSplat Write-Verbose -Message 'Expand completed.' } #try catch { $result = $false Write-Error $_ } #catch return $result } #Expand-XMLDataSet <# .SYNOPSIS Downloads file to device. .DESCRIPTION Retrieves file from web and downloads to device. .EXAMPLE Get-RemoteFile Downloads file to data path. .PARAMETER File File to download. .PARAMETER OutFileName Specify output file name. .OUTPUTS System.Boolean .NOTES Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/ Overwrites existing zip file. .COMPONENT PSGalleryExplorer #> function Get-RemoteFile { [CmdletBinding()] param ( [Parameter(Mandatory = $true, HelpMessage = 'File to download')] [string]$File, [Parameter(Mandatory = $false, HelpMessage = 'Specify output file name.')] [string]$OutFileName ) $result = $true #assume the best if ($OutFileName) { $OutFile = $OutFileName } else { $OutFile = $File } Write-Verbose -Message 'Downloading file...' try { $invokeWebRequestSplat = @{ OutFile = [System.IO.Path]::Combine($script:dataPath, $OutFile) Uri = 'https://{0}/{1}' -f $script:dlURI, $File ErrorAction = 'Stop' } $oldProgressPreference = $progressPreference $progressPreference = 'SilentlyContinue' if ($PSEdition -eq 'Desktop') { $null = Invoke-WebRequest @invokeWebRequestSplat -PassThru -UseBasicParsing } else { $null = Invoke-WebRequest @invokeWebRequestSplat -PassThru } } #try catch { $result = $false Write-Error $_ } #catch finally { $progressPreference = $oldProgressPreference } #finally return $result } #Get-RemoteFile <# .SYNOPSIS Evaluates if XML data set is in memory and kicks of child processes to obtain XML data set. .DESCRIPTION XML data set will be evaluated if already in memory. If not, a series of processes will be kicked off to load the XML data set for use. .EXAMPLE Import-XMLDataSet Loads the XML data set into memory. .OUTPUTS System.Boolean .NOTES Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/ Parent process for getting XML data. .COMPONENT PSGalleryExplorer #> function Import-XMLDataSet { [CmdletBinding()] param ( ) $result = $true #assume the best Write-Verbose -Message 'Verifying current state of XML data set...' if ($null -eq $script:glData) { $dataCheck = Invoke-XMLDataCheck if ($dataCheck) { try { $getContentSplat = @{ Path = "$script:dataPath\$script:dataFile" Raw = $true ErrorAction = 'Stop' } $fileData = Get-Content @getContentSplat $script:glData = $fileData | ConvertFrom-Clixml -ErrorAction Stop } #try catch { $result = $false Write-Error $_ } #catch } #if_dataCheck else { $result = $false } #else_dataCheck } #if_gldata return $result } #Import-XMLDataSet <# .SYNOPSIS Invokes all child functions required to process retrieving the XML data set file. .DESCRIPTION Runs all required child functions to successfully retrieve and process the XML data set file. .EXAMPLE Invoke-XMLDataCheck Downloads, expands, and verified the XML data set file. .OUTPUTS System.Boolean .NOTES Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/ Confirm-XMLDataSet Get-RemoteFile Expand-XMLDataSet .COMPONENT PSGalleryExplorer #> function Invoke-XMLDataCheck { [CmdletBinding(ConfirmImpact = 'Low', SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $false, HelpMessage = 'Skip confirmation')] [switch]$Force ) Begin { if (-not $PSBoundParameters.ContainsKey('Verbose')) { $VerbosePreference = $PSCmdlet.SessionState.PSVariable.GetValue('VerbosePreference') } if (-not $PSBoundParameters.ContainsKey('Confirm')) { $ConfirmPreference = $PSCmdlet.SessionState.PSVariable.GetValue('ConfirmPreference') } if (-not $PSBoundParameters.ContainsKey('WhatIf')) { $WhatIfPreference = $PSCmdlet.SessionState.PSVariable.GetValue('WhatIfPreference') } Write-Verbose -Message ('[{0}] Confirm={1} ConfirmPreference={2} WhatIf={3} WhatIfPreference={4}' -f $MyInvocation.MyCommand, $Confirm, $ConfirmPreference, $WhatIf, $WhatIfPreference) $results = $true #assume the best } #begin Process { # -Confirm --> $ConfirmPreference = 'Low' # ShouldProcess intercepts WhatIf* --> no need to pass it on if ($Force -or $PSCmdlet.ShouldProcess("ShouldProcess?")) { Write-Verbose -Message ('[{0}] Reached command' -f $MyInvocation.MyCommand) $ConfirmPreference = 'None' $dataOutputDir = Confirm-DataLocation if ($dataOutputDir -eq $true) { $confirm = Confirm-XMLDataSet if (-not $confirm -eq $true) { $retrieve = Get-RemoteFile -File $script:dataFileZip # remove metadata file if it exists $localMetaDataFilePath = [System.IO.Path]::Combine($script:dataPath, $script:metadataFile) if (Test-Path -Path $localMetaDataFilePath) { Remove-Item -Path $localMetaDataFilePath -Force } $retrieveMetadata = Get-RemoteFile -File $script:metadataFile if ($retrieve -eq $true -and $retrieveMetadata -eq $true) { $expand = Expand-XMLDataSet if (-not $expand -eq $true) { $results = $false } } else { $results = $false } } #if_Confirm } #if_data_output else { $results = $false } #else_data_output } #if_Should } #process End { return $results } #end } #Invoke-XMLDataCheck <# .EXTERNALHELP PSGalleryExplorer-help.xml #> function Find-ModuleByCommand { [CmdletBinding()] param ( [Parameter(Mandatory = $true, HelpMessage = 'Specifies the command name to search for')] [string]$CommandName, [Parameter(Mandatory = $false, HelpMessage = 'Output focus on community insights')] [switch]$InsightView ) Write-Verbose -Message 'Verifying XML Data Set Availability...' if (Import-XMLDataSet) { Write-Verbose -Message 'Verified.' $dataSet = $script:glData Write-Verbose -Message ('Searching for Modules that contain the command: {0}' -f $CommandName) $find = $dataSet | Where-Object { $_.Includes.Function -contains $CommandName -or $_.Includes.Command -contains $CommandName -or $_.Includes.Cmdlet -contains $CommandName } } #if_Import-XMLDataSet else { Write-Warning -Message 'PSGalleryExplorer was unable to source the required data set file.' Write-Warning -Message 'Ensure you have an active internet connection' return } #else_Import-XMLDataSet Write-Verbose -Message 'Adding output properties to objects...' foreach ($item in $find) { $metrics = $null $metrics = @{ Downloads = $item.AdditionalMetadata.downloadCount LastUpdate = $item.AdditionalMetadata.lastUpdated Star = $item.ProjectInfo.StarCount Sub = $item.ProjectInfo.Subscribers Watch = $item.ProjectInfo.Watchers Fork = $item.ProjectInfo.Forks Issues = $item.ProjectInfo.Issues RepoUpdate = $item.ProjectInfo.Updated } $item | Add-Member -NotePropertyMembers $metrics -TypeName Asset -Force if ($InsightView) { $item.PSObject.TypeNames.Insert(0, 'PSGEInsight') } else { $item.PSObject.TypeNames.Insert(0, 'PSGEFormat') } } #foreach_find Write-Verbose -Message 'Properties addition completed.' return $find } #Find-ModuleByCommand <# .EXTERNALHELP PSGalleryExplorer-help.xml #> function Find-PSGModule { [CmdletBinding(defaultparametersetname = 'none')] param ( [Parameter(ParameterSetName = 'GalleryDownloads', HelpMessage = 'Find by PowerShell Gallery Downloads')] [switch]$ByDownloads, [Parameter(ParameterSetName = 'Repo', HelpMessage = 'Find by Repository metrics')] [ValidateSet( 'StarCount', 'Forks', 'Issues', 'Subscribers' )] [string]$ByRepoInfo, [Parameter(ParameterSetName = 'Update', HelpMessage = 'Find by recently updated')] [ValidateSet( 'GalleryUpdate', 'RepoUpdate' )] [string]$ByRecentUpdate, [Parameter(ParameterSetName = 'GalleryDownloads', HelpMessage = 'Find random PowerShell Gallery modules')] [switch]$ByRandom, [Parameter(ParameterSetName = 'Names', HelpMessage = 'Find by name')] [string]$ByName, [Parameter(ParameterSetName = 'Tags', HelpMessage = 'Find by tag')] [ValidatePattern('^[A-Za-z]+')] [string]$ByTag, [Parameter(HelpMessage = 'Include corporation results')] [switch]$IncludeCorps, [Parameter(HelpMessage = 'Include more popular results')] [switch]$IncludeRegulars, [Parameter(Mandatory = $false, HelpMessage = 'Max number of modules to return')] [int]$NumberToReturn = 35, [Parameter(Mandatory = $false, HelpMessage = 'Output focus on community insights')] [switch]$InsightView ) Write-Verbose -Message 'Verifying XML Data Set Availability...' if (Import-XMLDataSet) { Write-Verbose -Message 'Verified.' #__________________________________________________________ Write-Verbose -Message 'Processing exclusions...' if ($IncludeCorps -and $IncludeRegulars) { $dataSet = $script:glData } elseif ($ByName) { $dataSet = $script:glData } elseif ($IncludeCorps) { $dataSet = $script:glData | Where-Object { $_.Name -notin $script:regulars } } elseif ($IncludeRegulars) { $dataSet = $script:glData | Where-Object { $_.AdditionalMetadata.CompanyName -notin $script:corps -and $_.Author -notin $script:corps -and $_.AdditionalMetadata.copyright -notin $script:corps } } else { $dataSet = $script:glData | Where-Object { $_.AdditionalMetadata.CompanyName -notin $script:corps -and $_.Author -notin $script:corps -and $_.AdditionalMetadata.copyright -notin $script:corps -and $_.Name -notin $script:regulars } } Write-Verbose -Message 'Exclusions completed.' #__________________________________________________________ if ($ByRepoInfo) { Write-Verbose -Message 'ByRepoInfo' $gitModules = $dataSet | Where-Object { $_.ProjectInfo.GitStatus -eq $true } $find = $gitModules | Sort-Object -Property { [int]$_.ProjectInfo.$ByRepoInfo } -Descending | Select-Object -First $NumberToReturn } #if_ByRepoInfo elseif ($ByDownloads) { Write-Verbose -Message 'ByDownloads' $find = $dataSet | Sort-Object -Property { [int]$_.AdditionalMetadata.downloadCount } -Descending | Select-Object -First $NumberToReturn } #elseif_ByDownloads elseif ($ByRecentUpdate) { Write-Verbose -Message 'ByRecentUpdate' switch ($ByRecentUpdate) { 'GalleryUpdate' { $find = $dataSet | Sort-Object -Property { [datetime]$_.AdditionalMetadata.updated } -Descending | Select-Object -First $NumberToReturn } 'RepoUpdate' { $gitModules = $dataSet | Where-Object { $_.ProjectInfo.GitStatus -eq $true } $find = $gitModules | Sort-Object -Property { [datetime]$_.ProjectInfo.Updated } -Descending | Select-Object -First $NumberToReturn } } } #elseif_ByRecentUpdate elseif ($ByRandom) { Write-Verbose -Message 'ByRandom' $captured = @() $randoms = @() for ($i = 0; $i -lt $NumberToReturn; $i++) { $thisRound = $null $thisRound = $dataSet | Where-Object { $captured -notcontains $_.Name } | Get-Random $captured += $thisRound.Name $randoms += $thisRound } $find = $randoms | Sort-Object -Property { [int]$_.AdditionalMetadata.downloadCount } -Descending } #elseif_ByRandom elseif ($ByName) { Write-Verbose -Message 'ByName' if ($ByName -like '*`**') { $find = $dataSet | Where-Object { $_.Name -like $ByName } } else { $find = $dataSet | Where-Object { $_.Name -eq $ByName } } } #ByName elseif ($ByTag) { Write-Verbose -Message 'ByTag' $tagModules = $dataSet | Where-Object { $ByTag -in $_.Tags } $find = $tagModules | Sort-Object -Property { [int]$_.AdditionalMetadata.downloadCount } -Descending | Select-Object -First $NumberToReturn } #ByTag else { $find = $dataSet | Sort-Object -Property { [int]$_.AdditionalMetadata.downloadCount } -Descending } #everything #__________________________________________________________ } #if_Import-XMLDataSet else { Write-Warning -Message 'PSGalleryExplorer was unable to source the required data set file.' Write-Warning -Message 'Ensure you have an active internet connection' return } #else_Import-XMLDataSet Write-Verbose -Message 'Adding output properties to objects...' foreach ($item in $find) { $metrics = $null $metrics = @{ Downloads = $item.AdditionalMetadata.downloadCount LastUpdate = $item.AdditionalMetadata.lastUpdated Star = $item.ProjectInfo.StarCount Sub = $item.ProjectInfo.Subscribers Watch = $item.ProjectInfo.Watchers Fork = $item.ProjectInfo.Forks Issues = $item.ProjectInfo.Issues RepoUpdate = $item.ProjectInfo.Updated } $item | Add-Member -NotePropertyMembers $metrics -TypeName Asset -Force if ($InsightView) { $item.PSObject.TypeNames.Insert(0, 'PSGEInsight') } else { $item.PSObject.TypeNames.Insert(0, 'PSGEFormat') } } #foreach_find Write-Verbose -Message 'Properties addition completed.' return $find } #Find-PSGModule |