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: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
    Confirms the XML dataset file is available and not beyond the expiration time.
.DESCRIPTION
    Confirms the XML dataset file is present on the file system for use. Determines the age of the XML dataset file. Returns true if present and 9 days older or more.
.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...'

    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-ChildItem -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.'
                $result = $false
            }
            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 XML Data set to device.
.DESCRIPTION
    Retrieves XML Data zip file from web and downloads to device.
.EXAMPLE
    Get-XMLDataSet
 
    Downloads XML data set to data path.
.OUTPUTS
    System.Boolean
.NOTES
    Author: Jake Morrison - @jakemorrison - https://www.techthoughts.info/
    Overwrites existing zip file.
.COMPONENT
    PSGalleryExplorer
#>

function Get-XMLDataSet {
    [CmdletBinding()]
    param (
    )
    $result = $true #assume the best

    Write-Verbose -Message 'Downloading XML data set...'
    try {
        $invokeWebRequestSplat = @{
            OutFile     = '{0}/{1}' -f $script:dataPath, $script:dataFileZip
            Uri         = 'https://{0}/{1}' -f $script:dlURI, $script:dataFileZip
            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-XMLDataSet


<#
.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-XMLDataSet
    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-XMLDataSet
                    if ($retrieve -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-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')]
        [string]
        [ValidateSet(
            'GalleryUpdate',
            'RepoUpdate'
        )]
        $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')]
        [string]
        [ValidatePattern('^[A-Za-z]+')]
        $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
    )
    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
        $item.PSObject.TypeNames.Insert(0, 'PSGEFormat')
    } #foreach_find
    Write-Verbose -Message 'Properties addition completed.'

    return $find
} #Find-PSGModule