PsImport.psm1
using namespace System.Management.Automation.Language #region Classes Class PsImport { static [System.Collections.Generic.List[string]] $ExcludedNames static [System.Collections.Generic.Dictionary[string, FunctionDetails]] $Functions # Dictionary of Functions that have already been parsed, so we won't have to do it over again (for performance reasons). static [FunctionDetails[]] GetFunctions([Query[]]$FnNames) { return [PsImport]::GetFunctions($FnNames, $false) } static [FunctionDetails[]] GetFunctions([Query[]]$FnNames, [bool]$throwOnFailure) { [ValidateNotNullOrEmpty()][Query[]]$FnNames = $FnNames; $res = @(); $_FnNames = @() foreach ($Fn in $FnNames) { $_FnNames += switch ($true) { $Fn.Text.Equals('*') { foreach ($Name in [PsImport]::GetFnNames()) { $res += [PsImport]::GetFunctions($Name) } ; break } $Fn.Text.Contains('*') { $AllNames = [PsImport]::GetFnNames(); $Fn_Names = @($AllNames | Where-Object { $_ -like $Fn.Text }); if (($Fn_Names | Where-Object { $_ -notin $AllNames }).Count -gt 0 -and $throwOnFailure) { throw [System.Management.Automation.ItemNotFoundException]::New($($Fn_Names -join ', ')) }; $Fn_Names; break } ([PsImport]::IsValidSource($FnNames, $false)) { $(Get-Command -CommandType Function | Where-Object { $_.Source -eq "$($Fn.Text)" } | Select-Object -ExpandProperty Name); break } Default { $Fn.Text } } } if ($res.Count -ne 0) { return $res } foreach ($Name in $_FnNames) { if ([bool]$(try { [PsImport]::Functions.Keys.Contains($Name) } catch { $false })) { $res += [PsImport]::Functions["$Name"]; continue } $c = Get-Command $Name -CommandType Function -ErrorAction Ignore if ($null -eq $c) { continue } [string]$fn = $("function script:$Name {`n" + $((((($c | Format-List) | Out-String) -Split ('Definition :')) -split ('CommandType : Function')) -split ("Name : $($Name)")).TrimEnd().Replace('# .EXTERNALHELP', '# EXTERNALHELP').Trim() + "`n}") $res += [FunctionDetails]::New($c.Module.Path, $Name, [scriptblock]::Create("$fn")) } if ($res.Count -eq 0) { $_Message = "Could not find function(s). Named: $($FnNames -join ', ')" if ($throwOnFailure) { throw [System.Management.Automation.ItemNotFoundException]::New($_Message) } $(Get-Variable -Name host).Value.UI.WriteWarningLine("$_Message") } return $res } static [FunctionDetails[]] GetFunctions([Query[]]$FnNames, [string[]]$FilePaths) { return [PsImport]::GetFunctions($FnNames, $FilePaths, $false) } static [FunctionDetails[]] GetFunctions([Query[]]$FnNames, [string[]]$FilePaths, [bool]$throwOnFailure) { [ValidateNotNullOrEmpty()][string[]]$FilePaths = $FilePaths; [ValidateNotNullOrEmpty()][Query[]]$FnNames = $FnNames $result = @(); $_Functions = @(); $_FilePaths = @(); [string[]]$PathsToSearch = @(); foreach ($line in $FilePaths) { if ([string]::IsNullOrWhiteSpace("$line")) { continue } if ([Regex]::IsMatch("$line", '^https?:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(:[0-9]+)?\/?.*$')) { $PathsToSearch += "$line"; continue } $PathsToSearch += Resolve-FilePath "$line" -Extensions '.ps1', '.psm1' } foreach ($path in $PathsToSearch) { if (![string]::IsNullOrWhiteSpace("$Path")) { $path = [PsImport]::ParseLink($path) if (!$path.Scheme.IsValid) { throw [System.IO.InvalidDataException]::New("'$($path.FullName)' is not a valid filePath or HTTPS URL.") } if ($path.Scheme.IsGistUrl) { throw "Get-GistContent is not implemented yet" } if ([Regex]::IsMatch($path.FullName, '^https:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(:[0-9]+)?\/?.*$')) { $outFile = [PsImport]::DownloadFile($path.FullName, $([IO.FileInfo]::New([IO.Path]::ChangeExtension([IO.Path]::Combine([IO.Path]::GetTempPath(), [IO.Path]::GetRandomFileName()), '.ps1'))).FullName); $_FilePaths += $outFile.FullName; Continue }; $_FilePaths += $path.FullName } } if ($_FilePaths.count -eq 0) { if ($throwOnFailure) { throw [System.IO.FileNotFoundException]::New("$FilePaths") } return $_Functions #still null } $_FilePaths = $_FilePaths | Sort-Object -Unique foreach ($file in $_FilePaths) { $_Functions += [PsImport]::ParseFile($file) } if (!$FnNames.Text.Contains('*')) { foreach ($q in $FnNames) { if ($q.Text.Contains('*')) { $result += $_Functions.Where({ $_.Name -like $q.Text }) Continue }; $result += $_Functions.Where({ $_.Name -eq $q.Text }) } } else { $result += $_Functions } $result = $result | Sort-Object -Property Name -Unique return $result } [System.Management.Automation.Language.FunctionDefinitionAST[]] static GetFncDefinition([string]$Path) { return [PsImport]::GetFncDefinition([System.Management.Automation.Language.Parser]::ParseFile($path, [ref]$null, [ref]$Null)) } [System.Management.Automation.Language.FunctionDefinitionAST[]] static GetFncDefinition([scriptBlock]$scriptBlock) { return [PsImport]::GetFncDefinition([System.Management.Automation.Language.Parser]::ParseInput($scriptBlock.Tostring(), [ref]$null, [ref]$Null)) } [System.Management.Automation.Language.FunctionDefinitionAST[]] static hidden GetFncDefinition([System.Management.Automation.Language.ScriptBlockAst]$ast) { $RawFunctions = $null $RawAstDocument = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.Ast] }, $true) if ($RawASTDocument.Count -gt 0 ) { # https://stackoverflow.com/questions/45929043/get-all-functions-in-a-powershell-script/45929412 $RawFunctions = $RawASTDocument.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $($args[0].parent) -isnot [System.Management.Automation.Language.FunctionMemberAst] }) } return $RawFunctions } static hidden [string[]] GetFnNames() { # Get all Names of loaded funtions whose source is known (loaded from modules) $sources = [PsImport]::GetCommandSources() return $(Get-Command -CommandType Function | Where-Object { $_.Source -in $sources }).Name -as [string[]] } static [FunctionDetails[]] ParseFile([string[]]$Path) { return [PsImport]::ParseFile($Path, $false, $false) } static [FunctionDetails[]] ParseFile([string[]]$Path, [bool]$ExcludePSCmdlets) { return [PsImport]::ParseFile($Path, $ExcludePSCmdlets, $false) } static [FunctionDetails[]] ParseFile([string[]]$Path, [bool]$ExcludePSCmdlets, [bool]$UseTitleCase) { if ([PsImport]::ExcludedNames.Count -eq 0 -and $ExcludePSCmdlets) { [PsImport]::ExcludedNames = [System.Collections.Generic.List[string]]::new() $((Get-Command -Module @( "Microsoft.PowerShell.Archive", "Microsoft.PowerShell.Utility", "Microsoft.PowerShell.ODataUtils", "Microsoft.PowerShell.Operation.Validation", "Microsoft.PowerShell.Management", "Microsoft.PowerShell.Core", "Microsoft.PowerShell.LocalAccounts", "Microsoft.WSMan.Management", "Microsoft.PowerShell.Security", "Microsoft.PowerShell.Diagnostics", "Microsoft.PowerShell.Host" ) ).Name + (Get-Alias).Name).Foreach({ [void][PsImport]::ExcludedNames.Add($_) } ) } $FnDetails = @(); $Paths = (Resolve-FilePath -Paths $Path -throwOnFailure:$false).Where({ $item = Get-Item -Path $_; $item -is [system.io.FileInfo] -and $item.Extension -in @('.ps1', '.psm1') } ) forEach ($p in $Paths) { $FncDef = [PsImport]::GetFncDefinition($p) foreach ($RawASTFunction in $FncDef) { $FnDetails += if ([PsImport]::ExcludedNames.Count -gt 0) { [FunctionDetails]::Create($p, $RawASTFunction, [PsImport]::ExcludedNames, $UseTitleCase) } else { [FunctionDetails]::Create($p, $RawASTFunction, $UseTitleCase) } } } $FnDetails | ForEach-Object { [void][PsImport]::Record($_) } return $FnDetails } static [psobject] ParseLink([string]$text) { [ValidateNotNullOrEmpty()][string]$text = $text $uri = $text -as 'Uri'; if ($uri -isnot [Uri]) { throw [System.InvalidOperationException]::New("Could not create uri from text '$text'.") }; $Scheme = $uri.Scheme if ([regex]::IsMatch($text, '^(\/[a-zA-Z0-9_-]+)+|([a-zA-Z]:\\(((?![<>:"\/\\|?*]).)+\\?)*((?![<>:"\/\\|?*]).)+)$')) { if ($text.ToCharArray().Where({ $_ -in [IO.Path]::InvalidPathChars }).Count -eq 0) { $Scheme = 'file' } else { Write-Debug "'$text' has invalidPathChars in it !" -Debug } } $IsValid = $Scheme -in @('file', 'https') $IsGistUrl = [Regex]::IsMatch($text, "^https://gist.github.com/[a-z0-9]+(?:/[a-z0-9]+)?$") $OutptObject = [pscustomobject]@{ FullName = $text Scheme = [PSCustomObject]@{ Name = $Scheme IsValid = $IsValid IsGistUrl = $IsGistUrl } } return $OutptObject } static [IO.FileInfo] DownloadFile([uri]$url) { # No $outFile so we create ones ourselves, and use suffix to prevent duplicaltes $randomSuffix = [Guid]::NewGuid().Guid.subString(15).replace('-', [string]::Join('', (0..9 | Get-Random -Count 1))) return [PsImport]::DownloadFile($url, "$(Split-Path $url.AbsolutePath -Leaf)_$randomSuffix"); } static [IO.FileInfo] DownloadFile([uri]$url, [string]$outFile) { return [PsImport]::DownloadFile($url, $outFile, $false) } static [IO.FileInfo] DownloadFile([uri]$url, [string]$outFile, [bool]$Force) { [ValidateNotNullOrEmpty()][uri]$url = [uri]$url; $outFile = [PsImport]::GetUnResolvedPath($outFile); if ([System.IO.Directory]::Exists($outFile)) { throw [InvalidOperationException]::new("outFile", "Please provide valid file path, not a directory.") } if ((Test-Path -Path $outFile -PathType Leaf -ErrorAction Ignore)) { if (!$Force) { throw "$outFile already exists" } Remove-Item $outFile -Force -ErrorAction Ignore | Out-Null } $stream = $null; $fileStream = $null; $name = Split-Path $url -Leaf; $request = [System.Net.HttpWebRequest]::Create($url) $request.UserAgent = "Mozilla/5.0" $response = $request.GetResponse() $contentLength = $response.ContentLength $stream = $response.GetResponseStream() $buffer = New-Object byte[] 1024 $fileStream = [System.IO.FileStream]::new($outFile, [System.IO.FileMode]::CreateNew) $totalBytesReceived = 0 $totalBytesToReceive = $contentLength while ($totalBytesToReceive -gt 0) { $bytesRead = $stream.Read($buffer, 0, 1024) $totalBytesReceived += $bytesRead $totalBytesToReceive -= $bytesRead $fileStream.Write($buffer, 0, $bytesRead) $percentComplete = [int]($totalBytesReceived / $contentLength * 100) Write-Progress -Activity "Downloading $name to $Outfile" -Status "Progress: $percentComplete%" -PercentComplete $percentComplete } try { Invoke-Command -ScriptBlock { $stream.Close(); $fileStream.Close() } -ErrorAction SilentlyContinue } catch { $null } return (Get-Item $outFile) } static hidden [string[]] GetCommandSources() { [string[]]$availableSources = @(Get-Command -CommandType Function | Select-Object Source -Unique).Source | Where-Object { $_.Length -gt 0 } return $availableSources } static [string] GetResolvedPath([string]$Path) { return [PsImport]::GetResolvedPath($((Get-Variable ExecutionContext).Value.SessionState), $Path) } static [string] GetResolvedPath([System.Management.Automation.SessionState]$session, [string]$Path) { $paths = $session.Path.GetResolvedPSPathFromPSPath($Path); if ($paths.Count -gt 1) { throw [System.IO.IOException]::new([string]::Format([cultureinfo]::InvariantCulture, "Path {0} is ambiguous", $Path)) } elseif ($paths.Count -lt 1) { throw [System.IO.IOException]::new([string]::Format([cultureinfo]::InvariantCulture, "Path {0} not Found", $Path)) } return $paths[0].Path } static [string] GetUnResolvedPath([string]$Path) { return [PsImport]::GetUnResolvedPath($((Get-Variable ExecutionContext).Value.SessionState), $Path) } static [string] GetUnResolvedPath([System.Management.Automation.SessionState]$session, [string]$Path) { return $session.Path.GetUnresolvedProviderPathFromPSPath($Path) } static hidden [bool] IsValidSource([String]$Source, [bool]$throwOnFailure) { $IsValid = $Source -in [PsImport]::GetCommandSources() if (!$IsValid -and $throwOnFailure) { throw $(New-Object System.Management.Automation.ErrorRecord $([System.Management.Automation.ItemNotFoundException]"Source named '$Source' was not found"), "ItemNotFoundException", $([System.Management.Automation.ErrorCategory]::ObjectNotFound), "PID: $((Get-Variable -Name PID).Value)") } return $IsValid } static [void] Record([FunctionDetails]$result) { $_nl = $null; $Should_Add = [bool]$(try { ![PsImport]::Functions.Keys.Contains($result.Name) } catch { $_nl = $_.Exception.Message.Equals('You cannot call a method on a null-valued expression.'); $_nl } ); if ($_nl) { [PsImport]::Functions = [System.Collections.Generic.Dictionary[string, FunctionDetails]]::New() } if ($Should_Add) { [PsImport]::Functions.Add($result.Name, $result) } # else { Write-Debug "[Recording] Skipped $($result.Name)" } } static [void] Record([FunctionDetails[]]$result) { foreach ($item in $result) { [PsImport]::Record($item) } } static [String] ToTitleCase ([string]$String) { return (Get-Culture).TextInfo.ToTitleCase($String.ToLower()) } static [hashtable] ReadPSDataFile([string]$FilePath) { return [scriptblock]::Create("$(Get-Content $FilePath | Out-String)").Invoke() } } class Query: Microsoft.PowerShell.Cmdletization.QueryBuilder { [ValidateNotNullOrEmpty()][string]$text Query() {} Query([string]$text) { $this.Text = $text } } class FunctionDetails { [string]$Name [string]$Path [string]$Source [System.Collections.ArrayList]$Commands = @() hidden [string]$DefaultParameterSet hidden [scriptblock]$ScriptBlock hidden [PsmoduleInfo]$Module hidden [string]$Description hidden [string]$ModuleName hidden [version]$Version hidden [string]$HelpUri hidden [string]$Noun hidden [string]$Verb hidden [ValidateNotNull()][System.Management.Automation.Language.FunctionDefinitionAST]$Definition FunctionDetails ([string]$Path, [string]$Name, [scriptblock]$ScriptBlock) { $FnDetails = @(); $FncDefinition = [PsImport]::GetFncDefinition($ScriptBlock) foreach ($FncAST in $FncDefinition) { $FnDetails += [FunctionDetails]::Create($path, $FncAST, $false) } $this.Definition = $FnDetails.Definition; $this.Path = Resolve-FilePath -Path $path -NoAmbiguous $this.Source = $this.Path.Split([IO.Path]::DirectorySeparatorChar)[-2] $this.SetName($Name) ; $this.SetCommands($false); $this.Module = Get-Module -Name $this.Source -ErrorAction Ignore $this.ScriptBlock = [scriptBlock]::Create("$($this.Definition.Extent.Text -replace '(?<=^function\s)(?!script:)', 'script:')") } FunctionDetails ([string]$Path, [System.Management.Automation.Language.FunctionDefinitionAST]$Raw, [Bool]$UseTitleCase) { $this.Definition = $Raw; $this.Path = Resolve-FilePath -Path $path -NoAmbiguous $this.Source = $this.Path.Split([IO.Path]::DirectorySeparatorChar)[-2] $this.SetCommands($UseTitleCase); $this.Module = Get-Module -Name $this.Source -ErrorAction Ignore $this.SetName($(if ($UseTitleCase) { [PsImport]::ToTitleCase($this.Definition.name) } else { $this.Definition.name })) $this.ScriptBlock = [scriptBlock]::Create("$($this.Definition.Extent.Text -replace '(?<=^function\s)(?!script:)', 'script:')") } FunctionDetails ([string]$Path, [System.Management.Automation.Language.FunctionDefinitionAST]$Raw, [string[]]$NamesToExculde, [Bool]$UseTitleCase) { $this.Definition = $Raw; $this.Path = Resolve-FilePath -Path $path -NoAmbiguous $this.Source = $this.Path.Split([IO.Path]::DirectorySeparatorChar)[-2] $this.SetCommands($NamesToExculde, $UseTitleCase); $this.Module = Get-Module -Name $this.Source -ErrorAction Ignore $this.SetName($(if ($UseTitleCase) { [PsImport]::ToTitleCase($this.Definition.name) } else { $this.Definition.name })) $this.ScriptBlock = [scriptBlock]::Create("$($this.Definition.Extent.Text -replace '(?<=^function\s)(?!script:)', 'script:')") } [FunctionDetails] Static Create([string]$path, [System.Management.Automation.Language.FunctionDefinitionAST]$RawAST, [bool]$UseTitleCase) { $res = [FunctionDetails]::New($path, $RawAST, $UseTitleCase) [void][PsImport]::Record($res); return $res } [FunctionDetails] Static Create([string]$path, [System.Management.Automation.Language.FunctionDefinitionAST]$RawAST, [string[]]$NamesToExculde, [bool]$UseTitleCase) { $res = [FunctionDetails]::New($path, $RawAST, $NamesToExculde, $UseTitleCase) [void][PsImport]::Record($res); return $res } hidden [void] SetName([string]$text) { [ValidateNotNullOrEmpty()]$text = $text $text = switch ($true) { $text.StartsWith('script:') { $text.Substring(7); break } $text.StartsWith('local:') { $text.Substring(6); break } Default { $text } } $this.Name = $text } hidden [void] SetCommands ([bool]$UseTitleCase) { $this.SetCommands(@(), $UseTitleCase) } hidden [void] SetCommands ([string[]]$ExclusionList, [Bool]$UseTitleCase) { $t = $this.Definition.findall({ $args[0] -is [System.Management.Automation.Language.CommandAst] }, $true) if ($t.Count -le 0 ) { return } ($t.GetCommandName() | Select-Object -Unique).Foreach({ $Command = if ($UseTitleCase ) { [PsImport]::ToTitleCase($_) } else { $_ }; if ($ExclusionList -contains $Command) { continue }; $this.Commands.Add($Command) } ) } } #endregion Classes #region functions function Get-GistContent { <# .SYNOPSIS A short one-line action-based description, e.g. 'Tests if a function is valid' .DESCRIPTION A longer description of the function, its purpose, common use cases, etc. .NOTES Information or caveats about the function e.g. 'This function is not supported in Linux' .LINK Specify a URI to a help page, this will show when Get-Help -Online is used. .EXAMPLE Get-GistContent https://gist.github.com/alainQtec/34c60b903f3c1c9d51ec5f0ee3a82adb Will get contents of the first file found in the gist #> [OutputType([string])] [CmdletBinding()] param ( # File name with extention [Parameter(Mandatory = $false, Position = 0)] [string]$FileName, # url string, can be a raw_url or just a normal gist url [Parameter(Mandatory = $false)] [string]$GistUrl ) begin { enum EncryptionScope { User # The encrypted data can be decrypted with the same user on any machine. Machine # The encrypted data can only be decrypted with the same user on the same machine it was encrypted on. } enum Compression { Gzip Deflate ZLib } class GitHub { static $webSession static [string] $UserName static hidden [bool] $IsInteractive = $false static hidden [string] $TokenFile = [GitHub]::GetTokenFile() static [PSObject] createSession() { return [Github]::createSession([Github]::UserName) } static [PSObject] createSession([string]$UserName) { [GitHub]::SetToken() return [GitHub]::createSession($UserName, [GitHub]::GetToken()) } static [Psobject] createSession([string]$GitHubUserName, [securestring]$clientSecret) { [ValidateNotNullOrEmpty()][string]$GitHubUserName = $GitHubUserName [ValidateNotNullOrEmpty()][string]$GithubToken = $GithubToken = [AesCrypt]::GetString([securestring]$clientSecret) $encodedAuth = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("$($GitHubUserName):$($GithubToken)")) $web_session = New-Object Microsoft.PowerShell.Commands.WebRequestSession [void]$web_session.Headers.Add('Authorization', "Basic $($encodedAuth)") [void]$web_session.Headers.Add('Accept', 'application/vnd.github.v3+json') [GitHub]::webSession = $web_session return $web_session } static [void] SetToken() { [GitHub]::SetToken([AesCrypt]::GetString((Read-Host -Prompt "[GitHub] Paste/write your api token" -AsSecureString)), $(Read-Host -Prompt "[GitHub] Paste/write a Password to encrypt the token" -AsSecureString)) } static [void] SetToken([string]$token, [securestring]$password) { if (![IO.File]::Exists([GitHub]::TokenFile)) { New-Item -Type File -Path ([GitHub]::TokenFile) -Force | Out-Null } [IO.File]::WriteAllText([GitHub]::TokenFile, [convert]::ToBase64String([AesCrypt]::Encrypt([system.Text.Encoding]::UTF8.GetBytes($token), $password)), [System.Text.Encoding]::UTF8); } static [securestring] GetToken() { $sectoken = $null; $session_pass = [AesCrypt]::GetSecureString('123'); try { if ([GitHub]::IsInteractive) { if ([string]::IsNullOrWhiteSpace((Get-Content ([GitHub]::TokenFile) -ErrorAction Ignore))) { Write-Host "[GitHub] You'll need to set your api token first. This is a One-Time Process :)" -ForegroundColor Green [GitHub]::SetToken() Write-Host "[GitHub] Good, now let's use the api token :)" -ForegroundColor DarkGreen } elseif ([GitHub]::ValidateBase64String([IO.File]::ReadAllText([GitHub]::TokenFile))) { Write-Host "[GitHub] Encrypted token found in file: $([GitHub]::TokenFile)" -ForegroundColor DarkGreen } else { throw [System.Exception]::New("Unable to read token file!") } $session_pass = Read-Host -Prompt "[GitHub] Input password to use your token" -AsSecureString } else { #Fix: Temporary Workaround: Thisz a pat from one of my GitHub a/cs.It Can only read/write gists. Will expire on 1/1/2025. DoNot Abuse this or I'll take it down!! $et = "OOLqqov4ugMQAtFcWqbzRwNBD65uf9JOZ+jzx1RtcHAZtnKaq1zkIpBcuv1MQfOkvIr/V066Zgsaq5Gka+VhlbqhV8apm8zcQomYjYqLaECKAonFeeo9MqvaP1F2VLgXokrxD1M6weLwS7KC+dyvAgv10IEvLzWFMw==" [GitHub]::SetToken([convert]::ToBase64String([aescrypt]::Decrypt([convert]::FromBase64String($et), $session_pass)), $session_pass) } $sectoken = [AesCrypt]::GetSecureString([system.Text.Encoding]::UTF8.GetString( [AesCrypt]::Decrypt([Convert]::FromBase64String([IO.File]::ReadAllText([GitHub]::GetTokenFile())), $session_pass) ) ) } catch { throw $_ } return $sectoken } static [PsObject] GetUserInfo([string]$UserName) { if ([string]::IsNullOrWhiteSpace([GitHub]::userName)) { [GitHub]::createSession() } $response = Invoke-RestMethod -Uri "https://api.github.com/user/$UserName" -WebSession ([GitHub]::webSession) -Method Get -Verbose:$false return $response } static [PsObject] GetGist([uri]$Uri) { $l = [GitHub]::ParseGistUri($Uri) return [GitHub]::GetGist($l.Owner, $l.Id) } static [PsObject] GetGist([string]$UserName, [string]$GistId) { $t = [GitHub]::GetToken() if ($null -eq ([GitHub]::webSession)) { [GitHub]::webSession = $(if ($null -eq $t) { [GitHub]::createSession($UserName) } else { [GitHub]::createSession($UserName, $t) } ) } if (![GitHub]::IsConnected()) { throw [System.Net.NetworkInformation.PingException]::new("PingException, PLease check your connection!"); } if ([string]::IsNullOrWhiteSpace($GistId) -or $GistId -eq '*') { return Get-Gists -UserName $UserName -SecureToken $t } return Invoke-RestMethod -Uri "https://api.github.com/gists/$GistId" -WebSession ([GitHub]::webSession) -Method Get -Verbose:$false } Static [string] GetGistContent([string]$FileName, [uri]$GistUri) { return [GitHub]::GetGist($GistUri).files.$FileName.content } static [PsObject] CreateGist([string]$description, [array]$files) { $url = 'https://api.github.com/gists' $body = @{ description = $description files = @{} } foreach ($file in $files) { $body.files[$file.Name] = @{ content = $file.Content } } $response = Invoke-RestMethod -Uri $url -WebSession ([GitHub]::webSession) -Method Post -Body ($body | ConvertTo-Json) -Verbose:$false return $response } static [PsObject] UpdateGist([GistFile]$gist, [string]$NewContent) { return '' } static [string] GetTokenFile() { if (![IO.File]::Exists([GitHub]::TokenFile)) { [GitHub]::TokenFile = [IO.Path]::Combine([GitHub]::Get_dataPath('Github', 'clicache'), "token"); } return [GitHub]::TokenFile } static [GistFile] ParseGistUri([uri]$GistUri) { $res = $null; $ogs = $GistUri.OriginalString $IsRawUri = $ogs.Contains('/raw/') -and $ogs.Contains('gist.githubusercontent.com') $seg = $GistUri.Segments $res = $(if ($IsRawUri) { $name = $seg[-1] $rtri = 'https://gist.github.com/{0}{1}' -f $seg[1], $seg[2] $rtri = $rtri.Remove($rtri.Length - 1) $info = [GitHub]::GetGist([uri]::new($rtri)) $file = $info.files."$name" [PsCustomObject]@{ language = $file.language IsPublic = $info.IsPublic raw_url = $file.raw_url Owner = $info.owner.login type = $file.type filename = $name size = $file.size Id = $seg[2].Replace('/', '') } } else { # $info = [GitHub]::GetGist($GistUri) [PsCustomObject]@{ language = '' IsPublic = $null raw_url = '' Owner = $seg[1].Split('/')[0] type = '' filename = '' size = '' Id = $seg[-1] } } ) return [GistFile]::New($res) } static [PsObject] GetUserRepositories() { if ($null -eq [GitHub]::webSession) { [Github]::createSession() } $response = Invoke-RestMethod -Uri 'https://api.github.com/user/repos' -WebSession ([GitHub]::webSession) -Method Get -Verbose:$false return $response } static [psobject] ParseLink([string]$text, [bool]$throwOnFailure) { [ValidateNotNullOrEmpty()][string]$text = $text $uri = $text -as 'Uri'; if ($uri -isnot [Uri] -and $throwOnFailure) { throw [System.InvalidOperationException]::New("Could not create uri from text '$text'.") }; $Scheme = $uri.Scheme if ([regex]::IsMatch($text, '^(\/[a-zA-Z0-9_-]+)+|([a-zA-Z]:\\(((?![<>:"\/\\|?*]).)+\\?)*((?![<>:"\/\\|?*]).)+)$')) { if ($text.ToCharArray().Where({ $_ -in [IO.Path]::InvalidPathChars }).Count -eq 0) { $Scheme = 'file' } else { Write-Debug "'$text' has invalidPathChars in it !" -Debug } } $IsValid = $Scheme -in @('file', 'https') $IsGistUrl = [Regex]::IsMatch($text, 'https?://gist\.github\.com/\w+/[0-9a-f]+') $OutptObject = [pscustomobject]@{ FullName = $text Scheme = [PSCustomObject]@{ Name = $Scheme IsValid = $IsValid IsGistUrl = $IsGistUrl } } return $OutptObject } static [string] Get_Host_Os() { # Should return one of these: [Enum]::GetNames([System.PlatformID]) return $(if ($(Get-Variable IsWindows -Value)) { "Windows" }elseif ($(Get-Variable IsLinux -Value)) { "Linux" }elseif ($(Get-Variable IsMacOS -Value)) { "macOS" }else { "UNKNOWN" }) } static [IO.DirectoryInfo] Get_dataPath([string]$appName, [string]$SubdirName) { $_Host_OS = [GitHub]::Get_Host_Os() $dataPath = if ($_Host_OS -eq 'Windows') { [System.IO.DirectoryInfo]::new([IO.Path]::Combine($Env:HOME, "AppData", "Roaming", $appName, $SubdirName)) } elseif ($_Host_OS -in ('Linux', 'MacOs')) { [System.IO.DirectoryInfo]::new([IO.Path]::Combine((($env:PSModulePath -split [IO.Path]::PathSeparator)[0] | Split-Path | Split-Path), $appName, $SubdirName)) } elseif ($_Host_OS -eq 'Unknown') { try { [System.IO.DirectoryInfo]::new([IO.Path]::Combine((($env:PSModulePath -split [IO.Path]::PathSeparator)[0] | Split-Path | Split-Path), $appName, $SubdirName)) } catch { Write-Warning "Could not resolve chat data path" Write-Warning "HostOS = '$_Host_OS'. Could not resolve data path." [System.IO.Directory]::CreateTempSubdirectory(($SubdirName + 'Data-')) } } else { throw [InvalidOperationException]::new('Could not resolve data path. Get_Host_OS FAILED!') } if (!$dataPath.Exists) { [GitHub]::Create_Dir($dataPath) } return $dataPath } static [void] Create_Dir([string]$Path) { [GitHub]::Create_Dir([System.IO.DirectoryInfo]::new($Path)) } static [void] Create_Dir([System.IO.DirectoryInfo]$Path) { [ValidateNotNullOrEmpty()][System.IO.DirectoryInfo]$Path = $Path $nF = @(); $p = $Path; while (!$p.Exists) { $nF += $p; $p = $p.Parent } [Array]::Reverse($nF); $nF | ForEach-Object { $_.Create(); Write-Verbose "Created $_" } } static [bool] ValidateBase64String([string]$base64) { return $(try { [void][Convert]::FromBase64String($base64); $true } catch { $false }) } static [bool] IsConnected() { if (![bool]("System.Net.NetworkInformation.Ping" -as 'type')) { Add-Type -AssemblyName System.Net.NetworkInformation }; $cs = $null; $re = @{ true = @{ m = "Success"; c = "Green" }; false = @{ m = "Failed"; c = "Red" } } Write-Host "[Github] Testing Connection ... " -ForegroundColor Blue -NoNewline try { [System.Net.NetworkInformation.PingReply]$PingReply = [System.Net.NetworkInformation.Ping]::new().Send("github.com"); $cs = $PingReply.Status -eq [System.Net.NetworkInformation.IPStatus]::Success } catch [System.Net.Sockets.SocketException], [System.Net.NetworkInformation.PingException] { $cs = $false } catch { $cs = $false; Write-Error $_ } $re = $re[$cs.ToString()] Write-Host $re.m -ForegroundColor $re.c return $cs } } class GistFile { [ValidateNotNullOrEmpty()][string]$Name # with extention [string]$language [string]$raw_url [bool]$IsPublic [string]$Owner [string]$type [string]$Id [int]$size GistFile([string]$filename) { $this.Name = $filename } GistFile([psobject]$InputObject) { $this.language = $InputObject.language $this.IsPublic = $InputObject.IsPublic $this.raw_url = $InputObject.raw_url $this.Owner = $InputObject.Owner $this.type = $InputObject.type $this.Name = $InputObject.filename $this.size = $InputObject.size $this.Id = $InputObject.Id } [string] ShowFileInfo() { return "File: $($this.Name)" } } class Gist { [uri] $Uri [string] $Id [string] $Owner [string] $Description [bool] $IsPublic [GistFile[]] $Files = @() Gist() {} Gist([string]$Name) { $this.AddFile([GistFile]::new($Name)) } [psobject] Post() { $gisfiles = @() $this.Files.Foreach({ $gisfiles += @{ $_.Name = @{ content = $_.Content } } } ) $data = @{ files = $gisfiles description = $this.Description public = $this.IsPublic } | ConvertTo-Json Write-Verbose ($data | Out-String) Write-Verbose "[PROCESS] Posting to https://api.github.com/gists" $invokeParams = @{ Method = 'Post' Uri = "https://api.github.com/gists" WebSession = [GitHub]::webSession Body = $data ContentType = 'application/json' } [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $r = Invoke-RestMethod @invokeParams $r = $r | Select-Object @{Name = "Url"; Expression = { $_.html_url } }, Description, Public, @{Name = "Created"; Expression = { $_.created_at -as [datetime] } } return $r } [void] AddFile([GistFile]$file) { $this.Files += $file } [string] ShowInfo() { $info = "Gist ID: $($this.Id)" $info += "`nDescription: $($this.Description)" $info += "`nFiles:" foreach ($file in $this.Files.Values) { $info += "`n - $($file.ShowFileInfo())" } return $info } } class Shuffl3r { static [Byte[]] Combine([Byte[]]$Bytes, [Byte[]]$Nonce, [securestring]$Passwod) { return [Shuffl3r]::Combine($bytes, $Nonce, [AesCrypt]::GetString($Passwod)) } static [Byte[]] Combine([Byte[]]$Bytes, [Byte[]]$Nonce, [string]$Passw0d) { # if ($Bytes.Length -lt 16) { throw [InvalidArgumentException]::New('Bytes', 'Input bytes.length should be > 16. ie: $minLength = 17, since the common $nonce length is 16') } if ($bytes.Length -lt ($Nonce.Length + 1)) { Write-Debug "Bytes.Length = $($Bytes.Length) but Nonce.Length = $($Nonce.Length)" -Debug throw [System.ArgumentOutOfRangeException]::new("Nonce", 'Make sure $Bytes.length > $Nonce.Length') } if ([string]::IsNullOrWhiteSpace($Passw0d)) { throw [System.ArgumentNullException]::new('$Passw0d') } [int[]]$Indices = [int[]]::new($Nonce.Length); Set-Variable -Name Indices -Scope local -Visibility Public -Option ReadOnly -Value ([Shuffl3r]::GenerateIndices($Nonce.Length, $Passw0d, $bytes.Length)); [Byte[]]$combined = [Byte[]]::new($bytes.Length + $Nonce.Length); for ([int]$i = 0; $i -lt $Indices.Length; $i++) { $combined[$Indices[$i]] = $Nonce[$i] } $i = 0; $ir = (0..($combined.Length - 1)) | Where-Object { $_ -NotIn $Indices }; foreach ($j in $ir) { $combined[$j] = $bytes[$i]; $i++ } return $combined } static [array] Split([Byte[]]$ShuffledBytes, [securestring]$Passwod, [int]$NonceLength) { return [Shuffl3r]::Split($ShuffledBytes, [AesCrypt]::GetString($Passwod), [int]$NonceLength); } static [array] Split([Byte[]]$ShuffledBytes, [string]$Passw0d, [int]$NonceLength) { if ($null -eq $ShuffledBytes) { throw [System.ArgumentNullException]::new('$ShuffledBytes') } if ([string]::IsNullOrWhiteSpace($Passw0d)) { throw [System.ArgumentNullException]::new('$Passw0d') } [int[]]$Indices = [int[]]::new([int]$NonceLength); Set-Variable -Name Indices -Scope local -Visibility Private -Option ReadOnly -Value ([Shuffl3r]::GenerateIndices($NonceLength, $Passw0d, ($ShuffledBytes.Length - $NonceLength))); $Nonce = [Byte[]]::new($NonceLength); $bytes = [Byte[]]$((0..($ShuffledBytes.Length - 1)) | Where-Object { $_ -NotIn $Indices } | Select-Object *, @{l = 'bytes'; e = { $ShuffledBytes[$_] } }).bytes for ($i = 0; $i -lt $NonceLength; $i++) { $Nonce[$i] = $ShuffledBytes[$Indices[$i]] }; return ($bytes, $Nonce) } static hidden [int[]] GenerateIndices([int]$Count, [string]$randomString, [int]$HighestIndex) { if ($HighestIndex -lt 3 -or $Count -ge $HighestIndex) { throw [System.ArgumentOutOfRangeException]::new('$HighestIndex >= 3 is required; and $Count should be less than $HighestIndex') } if ([string]::IsNullOrWhiteSpace($randomString)) { throw [System.ArgumentNullException]::new('$randomString') } [Byte[]]$hash = [System.Security.Cryptography.SHA256]::Create().ComputeHash([System.Text.Encoding]::UTF8.GetBytes([string]$randomString)) [int[]]$indices = [int[]]::new($Count) for ($i = 0; $i -lt $Count; $i++) { [int]$nextIndex = [Convert]::ToInt32($hash[$i] % $HighestIndex) while ($indices -contains $nextIndex) { $nextIndex = ($nextIndex + 1) % $HighestIndex } $indices[$i] = $nextIndex } return $indices } } # Custom AES Galois/Counter Mode implementation class AesCrypt { static hidden [string] $caller static [ValidateNotNull()][EncryptionScope] $EncryptionScope static [byte[]] GetDerivedSalt([securestring]$password) { $rfc2898 = $null; $s4lt = $null; [byte[]]$s6lt = if ([AesCrypt]::EncryptionScope.ToString() -eq "Machine") { [System.Text.Encoding]::UTF8.GetBytes([AesCrypt]::GetUniqueMachineId()) } else { [convert]::FromBase64String("qmkmopealodukpvdiexiianpnnutirid") } Set-Variable -Name password -Scope Local -Visibility Private -Option Private -Value $password; Set-Variable -Name s4lt -Scope Local -Visibility Private -Option Private -Value $s6lt; Set-Variable -Name rfc2898 -Scope Local -Visibility Private -Option Private -Value $([System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $s6lt)); Set-Variable -Name s4lt -Scope Local -Visibility Private -Option Private -Value $($rfc2898.GetBytes(16)); return $s4lt } static [byte[]] Encrypt([byte[]]$bytes) { return [AesCrypt]::Encrypt($bytes, [AesCrypt]::GetPassword()); } static [byte[]] Encrypt([byte[]]$Bytes, [SecureString]$Password) { [byte[]]$_salt = [AesCrypt]::GetDerivedSalt($Password) return [AesCrypt]::Encrypt($bytes, $Password, $_salt); } static [byte[]] Encrypt([byte[]]$Bytes, [SecureString]$Password, [byte[]]$Salt) { return [AesCrypt]::Encrypt($bytes, $Password, $Salt, $null, $null, 1); } static [string] Encrypt([string]$text, [SecureString]$Password, [int]$iterations) { return [convert]::ToBase64String([AesCrypt]::Encrypt([System.Text.Encoding]::UTF8.GetBytes("$text"), $Password, $iterations)); } static [byte[]] Encrypt([byte[]]$Bytes, [SecureString]$Password, [int]$iterations) { [byte[]]$_salt = [AesCrypt]::GetDerivedSalt($Password) return [AesCrypt]::Encrypt($bytes, $Password, $_salt, $null, $null, $iterations); } static [byte[]] Encrypt([byte[]]$Bytes, [SecureString]$Password, [byte[]]$Salt, [int]$iterations) { return [AesCrypt]::Encrypt($bytes, $Password, $Salt, $null, $null, $iterations); } static [byte[]] Encrypt([byte[]]$Bytes, [SecureString]$Password, [int]$iterations, [string]$Compression) { [byte[]]$_salt = [AesCrypt]::GetDerivedSalt($Password) return [AesCrypt]::Encrypt($bytes, $Password, $_salt, $null, $Compression, $iterations); } static [byte[]] Encrypt([byte[]]$Bytes, [SecureString]$Password, [byte[]]$Salt, [byte[]]$associatedData, [int]$iterations) { return [AesCrypt]::Encrypt($bytes, $Password, $Salt, $associatedData, $null, $iterations); } static [byte[]] Encrypt([byte[]]$Bytes, [SecureString]$Password, [byte[]]$Salt, [byte[]]$associatedData) { return [AesCrypt]::Encrypt($bytes, $Password, $Salt, $associatedData, $null, 1); } static [byte[]] Encrypt([byte[]]$Bytes, [SecureString]$Password, [byte[]]$Salt, [byte[]]$associatedData, [string]$Compression, [int]$iterations) { [int]$IV_SIZE = 0; Set-Variable -Name IV_SIZE -Scope Local -Visibility Private -Option Private -Value 12 [int]$TAG_SIZE = 0; Set-Variable -Name TAG_SIZE -Scope Local -Visibility Private -Option Private -Value 16 [string]$Key = $null; Set-Variable -Name Key -Scope Local -Visibility Private -Option Private -Value $([convert]::ToBase64String([System.Security.Cryptography.Rfc2898DeriveBytes]::new([AesCrypt]::GetString($Password), $Salt, 10000, [System.Security.Cryptography.HashAlgorithmName]::SHA1).GetBytes(32))); [System.IntPtr]$th = [System.IntPtr]::new(0); if ([string]::IsNullOrWhiteSpace([AesCrypt]::caller)) { [AesCrypt]::caller = '[AesCrypt]' } Set-Variable -Name th -Scope Local -Visibility Private -Option Private -Value $([System.Runtime.InteropServices.Marshal]::StringToHGlobalAnsi($TAG_SIZE)); try { $_bytes = $bytes; $aes = $null; Set-Variable -Name aes -Scope Local -Visibility Private -Option Private -Value $([ScriptBlock]::Create("[Security.Cryptography.AesGcm]::new([convert]::FromBase64String('$Key'))").Invoke()); for ($i = 1; $i -lt $iterations + 1; $i++) { # if ($Protect) { $_bytes = [xconvert]::ToProtected($_bytes, $Salt, [EncryptionScope]::User) } # Generate a random IV for each iteration: [byte[]]$IV = $null; Set-Variable -Name IV -Scope Local -Visibility Private -Option Private -Value ([System.Security.Cryptography.Rfc2898DeriveBytes]::new([AesCrypt]::GetString($password), $salt, 1, [System.Security.Cryptography.HashAlgorithmName]::SHA1).GetBytes($IV_SIZE)); $tag = [byte[]]::new($TAG_SIZE); $Encrypted = [byte[]]::new($_bytes.Length); [void]$aes.Encrypt($IV, $_bytes, $Encrypted, $tag, $associatedData); $_bytes = [Shuffl3r]::Combine([Shuffl3r]::Combine($Encrypted, $IV, $Password), $tag, $Password); Write-Debug "$([AesCrypt]::caller) [+] Encryption [$i/$iterations] ... Done" } } catch { throw $_ } finally { [void][System.Runtime.InteropServices.Marshal]::ZeroFreeGlobalAllocAnsi($th); Remove-Variable IV_SIZE, TAG_SIZE, th -ErrorAction SilentlyContinue } if (![string]::IsNullOrWhiteSpace($Compression)) { $_bytes = [AesCrypt]::Compress($_bytes, $Compression); } return $_bytes } static [byte[]] Decrypt([byte[]]$bytes) { return [AesCrypt]::Decrypt($bytes, [AesCrypt]::GetPassword()); } static [byte[]] Decrypt([byte[]]$Bytes, [SecureString]$Password) { [byte[]]$_salt = [AesCrypt]::GetDerivedSalt($Password) return [AesCrypt]::Decrypt($bytes, $Password, $_salt); } static [byte[]] Decrypt([byte[]]$Bytes, [SecureString]$Password, [byte[]]$Salt) { return [AesCrypt]::Decrypt($bytes, $Password, $Salt, $null, $null, 1); } static [string] Decrypt([string]$text, [SecureString]$Password, [int]$iterations) { return [System.Text.Encoding]::UTF8.GetString([AesCrypt]::Decrypt([convert]::FromBase64String($text), $Password, $iterations)); } static [byte[]] Decrypt([byte[]]$Bytes, [SecureString]$Password, [int]$iterations) { [byte[]]$_salt = [AesCrypt]::GetDerivedSalt($Password) return [AesCrypt]::Decrypt($bytes, $Password, $_salt, $null, $null, $iterations); } static [byte[]] Decrypt([byte[]]$Bytes, [SecureString]$Password, [byte[]]$Salt, [int]$iterations) { return [AesCrypt]::Decrypt($bytes, $Password, $Salt, $null, $null, 1); } static [byte[]] Decrypt([byte[]]$Bytes, [SecureString]$Password, [int]$iterations, [string]$Compression) { [byte[]]$_salt = [AesCrypt]::GetDerivedSalt($Password) return [AesCrypt]::Decrypt($bytes, $Password, $_salt, $null, $Compression, $iterations); } static [byte[]] Decrypt([byte[]]$Bytes, [SecureString]$Password, [byte[]]$Salt, [byte[]]$associatedData, [int]$iterations) { return [AesCrypt]::Decrypt($bytes, $Password, $Salt, $associatedData, $null, $iterations); } static [byte[]] Decrypt([byte[]]$Bytes, [SecureString]$Password, [byte[]]$Salt, [byte[]]$associatedData) { return [AesCrypt]::Decrypt($bytes, $Password, $Salt, $associatedData, $null, 1); } static [byte[]] Decrypt([byte[]]$Bytes, [SecureString]$Password, [byte[]]$Salt, [byte[]]$associatedData, [string]$Compression, [int]$iterations) { [int]$IV_SIZE = 0; Set-Variable -Name IV_SIZE -Scope Local -Visibility Private -Option Private -Value 12 [int]$TAG_SIZE = 0; Set-Variable -Name TAG_SIZE -Scope Local -Visibility Private -Option Private -Value 16 [string]$Key = $null; Set-Variable -Name Key -Scope Local -Visibility Private -Option Private -Value $([convert]::ToBase64String([System.Security.Cryptography.Rfc2898DeriveBytes]::new([AesCrypt]::GetString($Password), $Salt, 10000, [System.Security.Cryptography.HashAlgorithmName]::SHA1).GetBytes(32))); [System.IntPtr]$th = [System.IntPtr]::new(0); if ([string]::IsNullOrWhiteSpace([AesCrypt]::caller)) { [AesCrypt]::caller = '[AesCrypt]' } Set-Variable -Name th -Scope Local -Visibility Private -Option Private -Value $([System.Runtime.InteropServices.Marshal]::StringToHGlobalAnsi($TAG_SIZE)); try { $_bytes = if (![string]::IsNullOrWhiteSpace($Compression)) { [AesCrypt]::DeCompress($bytes, $Compression) } else { $bytes } $aes = [ScriptBlock]::Create("[Security.Cryptography.AesGcm]::new([convert]::FromBase64String('$Key'))").Invoke() for ($i = 1; $i -lt $iterations + 1; $i++) { # if ($UnProtect) { $_bytes = [xconvert]::ToUnProtected($_bytes, $Salt, [EncryptionScope]::User) } # Split the real encrypted bytes from nonce & tags then decrypt them: ($b, $n1) = [Shuffl3r]::Split($_bytes, $Password, $TAG_SIZE); ($b, $n2) = [Shuffl3r]::Split($b, $Password, $IV_SIZE); $Decrypted = [byte[]]::new($b.Length); $aes.Decrypt($n2, $b, $n1, $Decrypted, $associatedData); $_bytes = $Decrypted; Write-Debug "$([AesCrypt]::caller) [+] Decryption [$i/$iterations] ... Done" } } catch { if ($_.FullyQualifiedErrorId -eq "AuthenticationTagMismatchException") { Write-Host "$([AesCrypt]::caller) Wrong password" -ForegroundColor Yellow } throw $_ } finally { [void][System.Runtime.InteropServices.Marshal]::ZeroFreeGlobalAllocAnsi($th); Remove-Variable IV_SIZE, TAG_SIZE, th -ErrorAction SilentlyContinue } return $_bytes } static [byte[]] Compress([byte[]]$Bytes) { return [AesCrypt]::Compress($Bytes, 'Gzip'); } static [string] Compress([string]$Plaintext) { return [convert]::ToBase64String([AesCrypt]::Compress([System.Text.Encoding]::UTF8.GetBytes($Plaintext))); } static [byte[]] Compress([byte[]]$Bytes, [string]$Compression) { if (("$Compression" -as 'Compression') -isnot 'Compression') { Throw [System.InvalidCastException]::new("Compression type '$Compression' is unknown! Valid values: $([Enum]::GetNames([AesCrypt]) -join ', ')"); } $outstream = [System.IO.MemoryStream]::new() $Comstream = switch ($Compression) { "Gzip" { New-Object System.IO.Compression.GzipStream($outstream, [System.IO.Compression.CompressionLevel]::Optimal) } "Deflate" { New-Object System.IO.Compression.DeflateStream($outstream, [System.IO.Compression.CompressionLevel]::Optimal) } "ZLib" { New-Object System.IO.Compression.ZLibStream($outstream, [System.IO.Compression.CompressionLevel]::Optimal) } Default { throw "Failed to Compress Bytes. Could Not resolve Compression!" } } [void]$Comstream.Write($Bytes, 0, $Bytes.Length); $Comstream.Close(); $Comstream.Dispose(); [byte[]]$OutPut = $outstream.ToArray(); $outStream.Close() return $OutPut; } static [byte[]] DeCompress([byte[]]$Bytes) { return [AesCrypt]::DeCompress($Bytes, 'Gzip'); } static [string] DeCompress([string]$Base64Text) { return [System.Text.Encoding]::UTF8.GetString([AesCrypt]::DeCompress([convert]::FromBase64String($Base64Text))); } static [byte[]] DeCompress([byte[]]$Bytes, [string]$Compression) { if (("$Compression" -as 'Compression') -isnot 'Compression') { Throw [System.InvalidCastException]::new("Compression type '$Compression' is unknown! Valid values: $([Enum]::GetNames([compression]) -join ', ')"); } $inpStream = [System.IO.MemoryStream]::new($Bytes) $ComStream = switch ($Compression) { "Gzip" { New-Object System.IO.Compression.GzipStream($inpStream, [System.IO.Compression.CompressionMode]::Decompress); } "Deflate" { New-Object System.IO.Compression.DeflateStream($inpStream, [System.IO.Compression.CompressionMode]::Decompress); } "ZLib" { New-Object System.IO.Compression.ZLibStream($inpStream, [System.IO.Compression.CompressionMode]::Decompress); } Default { throw "Failed to DeCompress Bytes. Could Not resolve Compression!" } } $outStream = [System.IO.MemoryStream]::new(); [void]$Comstream.CopyTo($outStream); $Comstream.Close(); $Comstream.Dispose(); $inpStream.Close() [byte[]]$OutPut = $outstream.ToArray(); $outStream.Close() return $OutPut; } static [string] GetUniqueMachineId() { $Id = [string]($Env:MachineId) $vp = (Get-Variable VerbosePreference).Value try { Set-Variable VerbosePreference -Value $([System.Management.Automation.ActionPreference]::SilentlyContinue) $sha256 = [System.Security.Cryptography.SHA256]::Create() $HostOS = $(if ($(Get-Variable PSVersionTable -Value).PSVersion.Major -le 5 -or $(Get-Variable IsWindows -Value)) { "Windows" }elseif ($(Get-Variable IsLinux -Value)) { "Linux" }elseif ($(Get-Variable IsMacOS -Value)) { "macOS" }else { "UNKNOWN" }); if ($HostOS -eq "Windows") { if ([string]::IsNullOrWhiteSpace($Id)) { $machineId = Get-CimInstance -ClassName Win32_ComputerSystemProduct | Select-Object -ExpandProperty UUID Set-Item -Path Env:\MachineId -Value $([convert]::ToBase64String($sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($machineId)))); } $Id = [string]($Env:MachineId) } elseif ($HostOS -eq "Linux") { # $Id = (sudo cat /sys/class/dmi/id/product_uuid).Trim() # sudo prompt is a nono # Lets use mac addresses $Id = ([string[]]$(ip link show | grep "link/ether" | awk '{print $2}') -join '-').Trim() $Id = [convert]::ToBase64String($sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Id))) } elseif ($HostOS -eq "macOS") { $Id = (system_profiler SPHardwareDataType | Select-String "UUID").Line.Split(":")[1].Trim() $Id = [convert]::ToBase64String($sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Id))) } else { throw "Error: HostOS = '$HostOS'. Could not determine the operating system." } } catch { throw $_ } finally { $sha256.Clear(); $sha256.Dispose() Set-Variable VerbosePreference -Value $vp } return $Id } static [SecureString] GetSecureString([string]$String) { $SecureString = $null; Set-Variable -Name SecureString -Scope Local -Visibility Private -Option Private -Value ([System.Security.SecureString]::new()); if (![string]::IsNullOrEmpty($String)) { $Chars = $String.toCharArray() ForEach ($Char in $Chars) { $SecureString.AppendChar($Char) } } $SecureString.MakeReadOnly(); return $SecureString } static [string] GetString([System.Security.SecureString]$SecureString) { [string]$Pstr = [string]::Empty; [IntPtr]$zero = [IntPtr]::Zero; if ($null -eq $SecureString -or $SecureString.Length -eq 0) { return [string]::Empty; } try { Set-Variable -Name zero -Scope Local -Visibility Private -Option Private -Value ([System.Runtime.InteropServices.Marshal]::SecurestringToBSTR($SecureString)); Set-Variable -Name Pstr -Scope Local -Visibility Private -Option Private -Value ([System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($zero)); } finally { if ($zero -ne [IntPtr]::Zero) { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($zero); } } return $Pstr; } static [securestring] GetPassword() { $_pass = if ([AesCrypt]::EncryptionScope.ToString() -eq "Machine") { [AesCrypt]::GetSecureString([AesCrypt]::GetUniqueMachineId()) } else { $password = [SecureString]::new(); [Console]::Write("Enter your password: "); while ($true) { [ConsoleKeyInfo]$key = [Console]::ReadKey($true); if ($key.Key -eq [ConsoleKey]::Enter) { break; } $password.AppendChar($key.KeyChar); [Console]::Write("*"); } $password } [Console]::WriteLine(); return $_pass } static [void] ValidateCompression([string]$Compression) { if ($Compression -notin ([Enum]::GetNames('Compression' -as 'Type'))) { Throw [System.InvalidCastException]::new("The name '$Compression' is not a valid [Compression]`$typeName.") }; } } } process { $content = [string]::Empty if ($PSBoundParameters.Keys.Count -eq 1) { if ([GitHub]::ParseLink($FileName, $false).Scheme.IsValid) { $PrsdUrl = [GitHub]::ParseLink($FileName, $false) if ($PrsdUrl.Scheme.IsGistUrl) { $res = [GitHub]::GetGist([uri]::new($PrsdUrl.FullName)) Write-Verbose "[GitHub] Selecting first file in the gist" $fn0 = ($res.files[0] | Get-Member -MemberType noteproperty).Name[0] $content = $res.files.$fn0.content } else { Write-Error "Please Provide a valid Gist Url" } } } else { $content = [GitHub]::GetGistContent($FileName, [uri]::new($GistUrl)) } } end { return $content } } function Resolve-FilePath { <# .SYNOPSIS Resolve FilePath .DESCRIPTION Gets the full Path of any file in a repo .INPUTS [string[]] .OUTPUTS [String[]] .EXAMPLE Resolve-FilePath * -Extensions ('.ps1', '.psm1') Will get paths of powershell files in current location; thus [psimport]::ParseFile("*") will parse any powershell file in current location. .EXAMPLE Resolve-FilePath "Tests\Resources\Test-H*", "Tests\Resources\Test-F*" .EXAMPLE REsolve-FilePath ..\*.Tests.ps1 .NOTES Created to work with the "PsImport" module. (Its not tested for other use cases) TopLevel directory search takes Priority. eg: REsolve-FilePath PsImport.ps1 will return ./PsImport.ps1 instead of ./BuildOutput/PsImport/0.1.0/PsImport.psm1 Unless ./PsImport.ps1 doesn't exist; In that case it will Recursively search for other Names in the repo. .LINK https://github.com/alainQtec/PsImport/blob/main/Private/Resolve-FilePath.ps1 #> [CmdletBinding(DefaultParameterSetName = 'Query')] [OutputType([System.Object[]])] param ( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Query')] [ValidateNotNullOrEmpty()] [Alias('Path')] [string]$Query, [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ParameterSetName = 'Paths')] [ValidateNotNullOrEmpty()] [string[]]$Paths, [Parameter(Mandatory = $false, Position = 1, ValueFromPipeline = $false, ParameterSetName = '__AllParameterSets')] [ValidateNotNullOrEmpty()] [Alias('Extension')] [string[]]$Extensions, [Parameter(Mandatory = $false, Position = 2, ValueFromPipeline = $false, ParameterSetName = '__AllParameterSets')] [string[]]$Exclude, [switch]$throwOnFailure, [switch]$NoAmbiguous ) begin { $pathsToSearch = @(); $resolved = @(); $error_Msg = $null; $throwOnFailure = [string]$ErrorActionPreference -eq 'Stop' $pathsToSearch += if ($PSCmdlet.ParameterSetName.Equals('Query')) { @($Query) } else { $Paths } $GitHubRoot = $(if (Get-Command -Name git -CommandType Application -ErrorAction Ignore) { git rev-parse --show-toplevel }else { $null }) -as [IO.DirectoryInfo] $GetFiles = [scriptblock]::Create({ param ([Parameter(Mandatory)][string]$qr) $f = Get-ChildItem -Path $qr -File -ErrorAction Ignore if ($PSBoundParameters.ContainsKey('Extensions')) { return ($Files | Where-Object { $_.Extension -in $Extensions }) }; return $f } ) [string[]]$Exclude = @() $gitignore = [IO.Path]::Combine($ExecutionContext.SessionState.Path.CurrentLocation, '.gitignore') if ((Test-Path -Path $gitignore -PathType Leaf -ErrorAction Ignore)) { $Exclude += [IO.File]::ReadAllLines($gitignore).Where({ !$_.StartsWith('#') -and ![string]::IsNullOrWhiteSpace($_) }) } } process { forEach ($p in $pathsToSearch) { if ([Regex]::IsMatch($p, '^https?:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(:[0-9]+)?\/?.*$')) { $error_Msg += " '$p' is a Url! Please provide a valid File Path."; continue } # TopLevel directory search: $rslvdPaths, $error_Msg = $validPaths, $null [string[]]$rslvdPaths = (Resolve-Path $p -ErrorAction Ignore).Path [string[]]$validPaths = ($rslvdPaths | Where-Object { (Test-Path -Path "$_" -PathType Any -ErrorAction Ignore) }) if ($validPaths.Count -gt 1 -and $NoAmbiguous) { $error_Msg += "Path '$p' is ambiguous: $($validPaths -join ', ')" } $Files = $GetFiles.Invoke($p); if ($Files.FullName) { $resolved += $Files.FullName; Continue } $q = $p; $p = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($p); if ((Test-Path -Path $GitHubRoot.FullName -PathType Container -ErrorAction Ignore)) { $rslvdPaths = $( # Multi-Level directory search / -Recurse : switch ($true) { $([IO.Path]::IsPathFullyQualified($q)) { Get-Item -Path $q -ErrorAction Ignore break } $(![IO.Path]::IsPathFullyQualified($q) -and $q.Contains([IO.Path]::DirectorySeparatorChar)) { $relPath = '([IO.Path]::GetRelativePath($ExecutionContext.SessionState.Path.CurrentLocation, $_.FullName))' $IsMatch = if ($q.Contains('*')) { [scriptblock]::Create("$relPath -like `"$q`" -or `$_.FullName -like `"$q`"") } elseif ($q.EndsWith([IO.Path]::DirectorySeparatorChar)) { [scriptblock]::Create("$relPath -like `"$q*`" -or `$_.FullName -like `"$q*`"") } else { [scriptblock]::Create("$relPath -eq `"$q`" -or `$_.FullName -eq `"$q`"") } $(Get-ChildItem -Path $GitHubRoot.FullName -File -Recurse -ErrorAction Ignore).Where($IsMatch) break } (![IO.Path]::IsPathFullyQualified($q) -and !$q.Contains([IO.Path]::DirectorySeparatorChar)) { $IsMatch = if ($q.Contains('*')) { [scriptblock]::Create('$_.Name -like $q -or $_.BaseName -like $q') } else { [scriptblock]::Create('$_.Name -eq $q -or $_.BaseName -eq $q') } $(Get-ChildItem -Path $GitHubRoot.FullName -File -Recurse -ErrorAction Ignore).Where($IsMatch) break } Default { Get-ChildItem -Path $GitHubRoot.FullName -File -Recurse -Filter $q -ErrorAction Ignore } } ) | Select-Object -ExpandProperty FullName }; if (!$rslvdPaths) { $error_Msg += "No files were found in Path '$p'."; Continue } $resolved += $rslvdPaths } $resolved = $resolved | Sort-Object -Unique if ($PSBoundParameters.ContainsKey('Extensions')) { $resolved = $($resolved -as [IO.FileInfo[]] | Where-Object { $_.Extension -in $Extensions }).FullName } if ($resolved.Count -gt 1 -and $NoAmbiguous) { $error_Msg += ' Error: Resolved to Multiple paths' } } end { if ($error_Msg) { if ($throwOnFailure) { $PSCmdlet.ThrowTerminatingError( [System.Management.Automation.ErrorRecord]::New( [System.Management.Automation.ItemNotFoundException]::new($error_Msg), 'ItemNotFoundException', 'OperationStopped', [PSCustomObject]@{ Params = $PSCmdlet.MyInvocation.BoundParameters } ) ) } else { Write-Verbose $error_Msg } } return $resolved } } #endregion functions # Types that will be available to users when they import the module. $typestoExport = @( [PsImport] ) $TypeAcceleratorsClass = [psobject].Assembly.GetType('System.Management.Automation.TypeAccelerators') foreach ($Type in $typestoExport) { if ($Type.FullName -in $TypeAcceleratorsClass::Get.Keys) { $Message = @( "Unable to register type accelerator '$($Type.FullName)'" 'Accelerator already exists.' ) -join ' - ' throw [System.Management.Automation.ErrorRecord]::new( [System.InvalidOperationException]::new($Message), 'TypeAcceleratorAlreadyExists', [System.Management.Automation.ErrorCategory]::InvalidOperation, $Type.FullName ) } } # Add type accelerators for every exportable type. foreach ($Type in $typestoExport) { $TypeAcceleratorsClass::Add($Type.FullName, $Type) } # Remove type accelerators when the module is removed. $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { foreach ($Type in $typestoExport) { $TypeAcceleratorsClass::Remove($Type.FullName) } }.GetNewClosure(); $Public = Get-ChildItem ([IO.Path]::Combine($PSScriptRoot, 'Public')) -Filter "*.ps1" -ErrorAction SilentlyContinue foreach ($file in $Public) { Try { if ([string]::IsNullOrWhiteSpace($file.fullname)) { continue } . "$($file.fullname)" } Catch { Write-Warning "Failed to import function $($file.BaseName): $_" $host.UI.WriteErrorLine($_) } } Export-ModuleMember -Function @('Get-Function', 'Resolve-FilePath') -Alias @('Import', 'require') |