lib/util.ps1
### ### Aux/helper/misc functions! ### ### As funções a seguir são de caráter auxiliar, isto é, usadas apenas internamente para facilitar algo no PowershAI. ### A maioria são de uso apenas interno pelo módulo! ### ### # Simple verbose logging function Function verbose { $ParentName = (Get-Variable MyInvocation -Scope 1).Value.MyCommand.Name; write-verbose ( $ParentName +':'+ ($Args -Join ' ')) } # Aux function to set check custom types function IsType($obj, $name){ return $Obj.psobject.TypeNames -contains "PowershaiType:$name" ` -or $Obj.psobject.TypeNames -contains "Deserialized.PowershaiType:$name" } # Aux function to set custom types function SetType($obj, $name){ if(-not(IsType $obj $name)){ $Obj.psobject.TypeNames.insert(0,"PowershaiType:$name") } } # retorna a lista de parametros do caller! # Return caller function param list function GetMyParams(){ $cmd = Get-PSCallStack $Caller = $cmd[1]; $CmdParams = @($Caller.InvocationInfo.MyCommand.Parameters.values) $ParamList = @{}; function GetCommonParams { [CmdletBinding()] param() $MyInvocation.MyCommand.Parameters.Keys } $CommonParams = GetCommonParams foreach($Param in $CmdParams){ if($Caller.InvocationInfo.MyCommand.CmdletBinding){ if($Param.name -in $CommonParams){ continue; } } $ParamList[$Param.name] = Get-Variable -Name $Param.name -Scope 1 -ValueOnly -EA SilentlyContinue } return @{ bound = $ParamList args = $Caller.InvocationInfo.UnboundArguments caller = $Caller } } function Get-PowershaiErrorDetails { <# .SYNOPSIS Obtém mais detalhes sobre exceptions e ErrorRecords disparaods pelo Powershai! #> param( #O erro a ser analisado. Se null, utiliza o ultimo error! $ErrorObject = $error[0] ) write-host $ErrorObject.GetType().FullName; write-host ([string]$ErrorObject); write-host "=== STACK === " if($ErrorObject.ScriptStackTrace){ write-host $ErrorObject.ScriptStackTrace } } <# Obtém uma string codificada com um encoding específico. Internamente, o Powershell (.NET) armazena tudo como unicode utf16 (https://learn.microsoft.com/en-us/dotnet/standard/base-types/character-encoding-introduction) Mas, podemos usar as classes de Text.Encoding para obter strings formadas por oturas sequências. Isso é útil para enviar via protocolos (como HTTP), etc. Se você tentar printar o resultado dessa função, pode não ser legível, devido a sequência de bytes diferentes. Esta função é um auxiliar usada para testes somentes #> function GetPowershaiAuxEncodedString { param( $SourceStr ,$TargetEncoding = "UTF-8" ,#Se especifciado retorna apenas os bytes [switch]$Bytes ) #Aqui obtemos os bytes no encoding desejado. #Internamente, o Powershell armazena os caraceres como UTF-16. As classes Text.Encoding convertem de UTF-16 para a sequencia de bytes no encoding desejado. #FOr exemplo, innternamente, a string "á" é armazenada como um array contendo apenas 1 elemento: [char]225 (225 é o codepoint do á no Unicode). #Quando usamos UTF8.Getbytes("á"), estamos pedindo que o powershel retorne o UTF-8 equiuvalente de "á", que é um array cim 2 posicoes: 195,196 (é assim que o "á" é representando no UTF-8) [byte[]]$TargetEncBytes = [Text.Encoding]::GetEncoding($TargetEncoding).GetBytes($SourceStr) if($Bytes){ return $TargetEncBytes } #Uma vez que temos um array de bytes, podemos construir um tipo string de volta. #Assi,podemos concatenar essa string em lugares que aceitam string, como concatenção. #Aqui, usamos uma "gambi": O encoding iso-8859-1 não faz nenhuma conversão de bytes. Por isso, podemos usar ele para ober a string a partir do byte. #Assim, considerando o exemplo, anterior, será retornada uma string, formada peçla sequencia 195,196. Ao printar ela, vai parecer uma string completamente difernete (pos o Powershell exibe em outra codificação). return [Text.Encoding]::GetEncoding("iso-8859-1").GetString($TargetEncBytes) } # # funcoes usadas para auxiliar alguma operacao ou encasuplar logica complexa! function JoinPath { $Args -Join [IO.Path]::DirectorySeparatorChar } <# .SYNOPSIS Obtém uma referência para variável que define os default parameters .DESCRIPTION No Powershell, módulos tem seu próprio escopo de variáveis. Portanto, ao tentar definir essa variável fora do escopo correto, não afetará os comandos dos módulos. Este comando permite que o usuário tenha acesso a variável que controla o default parameter dos comandos do módulo. Na maior parte, isso vai ser usado para debug, mas, eventualmente, um usuário pode querer definir parâmetros default. .EXAMPLE O exemplo abaixo mostra como definir a variável de ebug default do comanod Invoke-Http. $HttpDebug = @{} $ModDefaults = Get-PowershaiDefaultParams $ModDefaults['Invoke-Http:DebugVarName'] = 'HttpDebug'; Note que o parãmetro -DebugVarName é um parâmetro existente no comando Invoke-Http. #> function Get-PowershaiDefaultParams { return $PSDefaultParameterValues } <# .SYNOPSIS Cria um nova Exception cusotmizada para o PowershaAI .DESCRIPTION FAciltia a criação de exceptions customizadas! É usada internamente pelos providers para criar exceptions com propriedades e tipos que podem ser restados posteriormente. #> function New-PowershaiError { param( #Unique erro identification $Name ,#A mensagem da exception! $Message ,#Propriedades personazalidas $Props = @{} ,#Tipo adicional! $Type = $null ,#Exception pai! $Parent = $null ) if($message){ $message = $message -Join "`n" } $Ex = New-Object System.Exception("$($Name):$Message",$Parent) foreach($PropName in @($Props.keys)){ $Ex | Add-Member -force Noteproperty $PropName $Props[$PropName] } $Ex | Add-Member -force Noteproperty ErrorName $Name; if($Type){ SetType $Ex $Type } return $ex; } <# .SYNOPSIS Mescla hashtables em uma hashtable de destino .DESCRIPTION A hashtable de destino irá conter o valor atualizado das hashtables de origem. Pode se especificar várias hashtables de origem, basta informar. Uma nova hashtable é retornada, sem alterar as existentes! #> function HashTableMerge { [CmdletBinding(PositionalBinding=$false)] param( [parameter(Position=1)] $Target ,[parameter(Position=2,ValueFromRemainingArguments)] $SourceTables ,$filter = $null ) $Me = $MyInvocation.MyCommand; $NewTable = @{} if(!$Target){ $Target = @{} } if(!$SourceTables){ $SourceTables = @{} } $TableList = @($Target) $TableList += $SourceTables; foreach($SrcTable in $TableList){ if($SrcTable -eq $null){ continue; } if($SrcTable -is [psobject] -and $SrcTable -isnot [hashtable]){ $ObjHash = @{} $SrcTable.psobject.properties | %{ $ObjHash[$_.name] = $_.Value } $SrcTable = $ObjHash; } elseif($SrcTable -isnot [hashtable]){ $type = "null"; if($SrcTable){ $type = $SrcTable.getType().FullName; } throw "POWERSHAI_MERGEHASH_ISNOT_HASHTABLE: $type"; } foreach($key in @($SrcTable.keys) ){ $KeyPath = $ParentKeyPath +"/"+ $key #Se as duas são com tipos diferentes, então sobrescreve $SrcValue = $SrcTable[$key] $NewValue = $NewTable[$key]; if($filter){ $FilterResult = & $filter @{ key = $key new = $SrcValue current = $NewValue path = $KeyPath } if(!$FilterResult){ continue; } } # Se for um hashtable, recursivamente atualiza! if($SrcValue -is [hashtable] -and $NewValue -is [hashtable]){ $ParentKeyPath = $KeyPath $SrcValue = & $Me $NewValue $SrcValue } elseif($SrcValue -is [hashtable]){ # make copy! $SrcValue = & $Me @{} $SrcValue } #Em todos o caso, src substitui o valor! $NewTable[$key] = $SrcValue; } } return $NewTable; } function RegArgCompletion { param( $Command ,$Parameter ,$Script ) if(Get-Command -EA SilentlyContinue Register-ArgumentCompleter){ @($Command) | %{ $CommandName = $_; @($Parameter) | %{ Register-ArgumentCompleter -CommandName $CommandName -ParameterName $_ -ScriptBlock $Script } } } } function GetParams($FunctionName){ $c = Get-Command $FunctionName; $ParamsAst = $Command.ScriptBlock.Ast.Parameters; if(!$ParamsAst){ $ParamsAst = $c.ScriptBlock.Ast.Body.ParamBlock.Parameters; } $CommandHelp = get-help $c; $Global:HelpIndex = @{} $CommandHelp.parameters.parameter | %{ $AllText = $_.description | %{ $_.text -split '\r?\n' | ?{$_ -and $_.trim()} | %{ $_.trim() } } $HelpIndex[$_.name] = $AllText } foreach($param in $ParamsAst){ $ParamName = $param.name.toString() -replace '^\$',''; [PsCustomObject]@{ name = $ParamName definition = $param.toString() help = $HelpIndex[$ParamName] source = $c } } } function Invoke-AESEncryption { <# .SYNOPSIS CRiptografa/Descruptografa string usando AES .DESCRIPTION Adaptado deste link: https://www.powershellgallery.com/packages/DRTools/4.0.2.3/Content/Functions%5CInvoke-AESEncryption.ps1 Obrigado! #> [CmdletBinding()] [OutputType([string])] Param ( [Parameter(Mandatory = $true)] [ValidateSet('Encrypt', 'Decrypt')] [String]$Mode, [Parameter(Mandatory = $true)] [String]$Key, [Parameter(Mandatory = $true, ParameterSetName = "CryptText")] [String]$Text, [Parameter(Mandatory = $true, ParameterSetName = "CryptFile")] [String]$Path ) Begin { $shaManaged = New-Object System.Security.Cryptography.SHA256Managed $aesManaged = New-Object System.Security.Cryptography.AesManaged $aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC $aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::Zeros $aesManaged.BlockSize = 128 $aesManaged.KeySize = 256 } Process { $aesManaged.Key = $shaManaged.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Key)) switch ($Mode) { 'Encrypt' { if ($Text) {$plainBytes = [System.Text.Encoding]::UTF8.GetBytes($Text)} if ($Path) { $File = Get-Item -Path $Path -ErrorAction SilentlyContinue if (!$File.FullName) { Write-Error -Message "File not found!" break } $plainBytes = [System.IO.File]::ReadAllBytes($File.FullName) $outPath = $File.FullName + ".aes" } $encryptor = $aesManaged.CreateEncryptor() $encryptedBytes = $encryptor.TransformFinalBlock($plainBytes, 0, $plainBytes.Length) $encryptedBytes = $aesManaged.IV + $encryptedBytes $aesManaged.Dispose() if ($Text) {return [System.Convert]::ToBase64String($encryptedBytes)} if ($Path) { [System.IO.File]::WriteAllBytes($outPath, $encryptedBytes) (Get-Item $outPath).LastWriteTime = $File.LastWriteTime return "File encrypted to $outPath" } } 'Decrypt' { if ($Text) {$cipherBytes = [System.Convert]::FromBase64String($Text)} if ($Path) { $File = Get-Item -Path $Path -ErrorAction SilentlyContinue if (!$File.FullName) { Write-Error -Message "File not found!" break } $cipherBytes = [System.IO.File]::ReadAllBytes($File.FullName) $outPath = $File.FullName -replace ".aes" } $aesManaged.IV = $cipherBytes[0..15] $decryptor = $aesManaged.CreateDecryptor() $decryptedBytes = $decryptor.TransformFinalBlock($cipherBytes, 16, $cipherBytes.Length - 16) $aesManaged.Dispose() if ($Text) {return [System.Text.Encoding]::UTF8.GetString($decryptedBytes).Trim([char]0)} if ($Path) { [System.IO.File]::WriteAllBytes($outPath, $decryptedBytes) (Get-Item $outPath).LastWriteTime = $File.LastWriteTime return "File decrypted to $outPath" } } } } End { $shaManaged.Dispose() $aesManaged.Dispose() } } # Stronger Version! function Invoke-AESEncryptionV2 { <# .SYNOPSIS CRiptografa/Descruptografa string usando AES com derivação de chaves e salt! #> [CmdletBinding()] Param ( [ValidateSet('Encrypt', 'Decrypt')] [String]$Mode, [String]$Key, [String]$Text ,[switch]$DebugData ) # Helper function to generate a secure random byte array function Generate-RandomBytes($size) { $randomBytes = New-Object byte[] $size [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($randomBytes) return $randomBytes } $shaManaged = New-Object System.Security.Cryptography.SHA256Managed $aesManaged = New-Object System.Security.Cryptography.AesManaged $aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC $aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::Zeros $aesManaged.BlockSize = 128 $EncryptedResult = $null; try { switch ($Mode) { 'Encrypt' { # Key derivation $SaltBytes = Generate-RandomBytes 16 $keyDerivation = New-Object System.Security.Cryptography.Rfc2898DeriveBytes($Key, $SaltBytes, 10000) $EncryptKey = $keyDerivation.GetBytes(32) # 32 bytes = 256bits! $plainBytes = [System.Text.Encoding]::UTF8.GetBytes($Text) # keys and encrytpr! $aesManaged.KeySize = $EncryptKey.length*8 $aesManaged.Key = $EncryptKey $aesManaged.GenerateIV() $encryptor = $aesManaged.CreateEncryptor() # encrypt data! $DataEncrypted = $encryptor.TransformFinalBlock($plainBytes, 0, $plainBytes.Length) $encryptedBytes = $SaltBytes + $aesManaged.IV + $DataEncrypted $EncryptedResult = [System.Convert]::ToBase64String($encryptedBytes) #PWSHAICV2: SALT(16bytes) + IV(16bytes) + DATA $result = [PsCustomObject]@{ salt = $SaltBytes iv = $aesManaged.IV enc = $DataEncrypted plain = $plainBytes full = $EncryptedResult key = $EncryptKey } $result | Add-Member -Force ScriptMethod ToString { $d = $this.full; return "PWSHAICV2:$d" } if($DebugData){ return $result; } return ''+$result; } 'Decrypt' { if($Text -match 'PWSHAICV2:(.+)'){ $Text = $matches[1]; } else { return $null; } $EncryptedBytes = [System.Convert]::FromBase64String($Text) $SaltBytes = $EncryptedBytes[0..15] $aesManaged.IV = $EncryptedBytes[16..31] $EncryptedLast = $EncryptedBytes.length - 1; $EncryptedData = $encryptedBytes[32..$EncryptedLast] $keyDerivation = New-Object System.Security.Cryptography.Rfc2898DeriveBytes($Key, $SaltBytes, 10000) $DecryptKey = $keyDerivation.GetBytes(32) # 32 bytes = 256bits! $aesManaged.KeySize = $DecryptKey.length*8 $aesManaged.Key = $DecryptKey $decryptor = $aesManaged.CreateDecryptor() $DecryptedBytes = $decryptor.TransformFinalBlock($EncryptedData, 0, $EncryptedData.Length) $DecryptedData = [System.Text.Encoding]::UTF8.GetString($DecryptedBytes).Trim([char]0); $result = [PsCustomObject]@{ salt = $SaltBytes iv = $aesManaged.IV enc = $EncryptedData plain = $DecryptedBytes data = $DecryptedData key = $DecryptKey } if($DebugData){ return $result; } return $result.data; } } } finally { $shaManaged.Dispose() $aesManaged.Dispose() } } function PowerShaiEncrypt { param($str, $password) Invoke-AESEncryptionV2 -Mode Encrypt -text $str -key $password } function PowerShaiDecrypt { param($str, $password) $cmd = "Invoke-AESEncryption" if($str -like "PWSHAICV2:*"){ $cmd = "Invoke-AESEncryptionV2" } & $Cmd -Mode Decrypt -text $str -key $password } function PowershaiHash { param($str) if(!$str){ $str = ""; } $shaManaged = New-Object System.Security.Cryptography.SHA256Managed [System.Convert]::ToBase64String($shaManaged.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($str))) } function Confirm-PowershaiObjectSchema { <# .SYNOPSIS Valida se um objeto segue um schema correto .DESCRIPTION Este comando valida se a estrutura deum objeto, ou hashtable, segue um schema. O schema é definindo no parametro scehma, e possui a seguinte sintaxe: @{ #Definir cada key esperada no objeto! Key1 = [string] # tipo de dado esperado! Key2 = "val1","val2" # lista de valores esperados # Uma key que é um objeto! Cada item deve especificar as props do objeto esperado! Key3 = @{ SubKey1 = [int] # Significa: Key3.SubKey1 deve ser int } # Uma key que é um array! Key4 = @{ '$schema' = "array" # a key especial $schema é uma metadado que define o schema! SubKey1 = [string] # Entao, cada subkey é tratado como } } '$schema' deve seguir as mesmas especificacoes da OpenaAPI. Se $schema for um type, é o meso que especificr $schema.type = "array". #> param($obj, $schema, $parent) $Me = $MyInvocation.MyCommand; $Options = $schema.'$schema'; if($Options -is [type]){ $Options = @{ type = $Options } } if($parent){ $Validations = $parent.validations $ParentPath = $parent.path; } else { $ParentPath = "" $Validations = @{} } $IsValid = $true; $errors = @() $hashvals = @{} if($Options.type -eq [array]){ $n = -1; $ItemSchema = HashTableMerge @{} $schema; $ItemSchema.remove('$schema'); if($obj -is [array]){ $obj | %{ $n++; $result = & $me $_ $ItemSchema @{ path = $ParentPath+"[$n]" validations = $Validations } if(!$result.valid){ $IsValid = $false; } } } else { $IsValid = $false; $IsTypeValid = $false; $Validations[$ParentPath] = [pscustomobject]@{ path = $ParentPath valid = $false; errors = "NotArray" } } $schema = @{}; } elseif($obj -eq $null){ $hashvals = @{} } elseif($obj -isnot [hashtable]){ $obj.psobject.properties | %{ $hashvals[$_.name] = $_.value } } else { $hashvals = $obj } foreach($key in @($schema.keys)){ $KeyPath = $ParentPath +"/"+ $key; if($key -eq '$schema'){ continue; } $KeySchema = @{ type = [object] values = @() } $KeySchemaValue = $schema[$key]; $KeyValue = $hashvals[$key]; if($KeySchemaValue -is [type]){ $KeySchema.type = $KeySchemaValue } if($KeySchemaValue -is [array]){ $KeySchema.values = $KeySchemaValue } if($KeySchemaValue -is [hashtable]){ # recursive validation! $result = & $Me $KeyValue $KeySchemaValue @{ path = $KeyPath validations = $Validations } $IsValidSchema = $result.valid; $IsContentValid = $IsValidSchema; $IsTypeValid = $true; } else { $ExpectedType = $KeySchema.type; $ExpectedTypes = @($ExpectedType) if($ExpectedType -eq [int]){ $ExpectedTypes += [int64]; } [bool]$IsTypeValid = @( $ExpectedTypes | ?{ $KeyValue -is $_ }).count if(!$IsTypeValid){ $CurrenType = "NULL"; if($KeyValue -ne $null){ $CurrenType = $KeyValue.getType().Name } $errors += @( "InvalidType:$CurrenType" "Expected:"+(@($ExpectedType|%{$_.name}) -Join ",") ) -Join "," } $IsContentValid = !$KeySchema.values -or $KeyValue -in @($KeySchema.values); if(!$IsContentValid){ $errors += "InvalidContent:"+$KeyValue; } $IsValidSchema = $IsTypeValid -and $IsContentValid; } if(!$IsValidSchema){ $IsValid = $false; } $Validations[$KeyPath] = [pscustomobject]@{ path = $KeyPath name = $key valid = $IsValidSchema IsTypeValid = $IsTypeValid ValueValid = $IsContentValid errors = $errors } } $result = @{ valid = $IsValid } if(!$ParentPath){ $result.validations = @($Validations.values) } return [PsCustomObject]$result; } function Enter-PowershaiRetry { <# .SYNOPSIS Gerencia a execução de comandos com base no resultado .DESCRIPTION Este cmdlet ajuda a executar comandos enquanto um determinado resultado não for alcançado. Com isso, é possível, por exemplo, solicitar o LLM que gere um resultado novamente caso a resposta não seja a solicitada! #> [CmdletBinding()] param( #O scriptblock com o código a ser executado $Code ,#Resultado esperado #Pode ser uma string co o qual o resultado do código será comparado. #Pode ser um script block que será invocado! #Deve retornar um bool true para ser considerado como válido! #$_ aponta para o resultado atual! $Expected ,#Máximo de retry $Retries = 1 ,#Exibe o progresso das tentativas [switch]$ShowProgress ,#Inclui exceptions no check! #Se não especifciado, se o codigo em -Code resultar em erro, o erro é disparado de volta para quem chamou. #Ao ser especificado, o erro é enviado como resultado para que o codigo -Expected decida o que fzer! [switch]$CheckErrors ,# Permite modificar o vlaor a ser usado no check. $_ apontará para o objeto resultante da execução! $ModifyResult = $null ) function progress { if($ShowProgress){ write-host @Args; } else { verbose @Args; } } if($Expected -is [ScriptBlock]){ $CheckScript = $Expected } else { $CheckScript = { return $Expected } } $CheckDetails = @{} $MustRetry = $true; $CurrentTry = 0; while($CurrentTry -lt $Retries){ $CurrentTry++; progress "Running code... Try: $($CurrentTry)"; $IsError = $false; try { $result = & $code } catch { if(!$CheckErrors){ throw; } progress "Resulted in error"; $IsError = $true; $result = $_ } progress " Result:`n$result" [bool]$CheckResult = & { if($ModifyResult -is [scriptblock]){ $result = $ModifyResult.InvokeWithContext($null,[psvariable]::new('_', $result)) | %{$_} } $CheckScriptResult = $CheckScript.InvokeWithContext( $null, [psvariable]::new('_', $result)) | %{$_}; verbose "CheckScriptResult: $CheckScriptResult"; $CheckDetails.CheckScriptResult = $CheckScriptResult; if($CheckScriptResult -is [bool]){ verbose "Result is bool!"; return $CheckScriptResult; } if($CheckScriptResult -is [type]){ verbose "Validating if is type $CheckScriptResult"; return $result -is $CheckScriptResult; } if($CheckScriptResult -is [hashtable]){ verbose "result is hashtable. Validating as schema!"; # Result must be a valid object! # If text, assume JSON! $ResultObject = $result; if($ResultObject -is [string]){ $ResultObject = $ResultObject | ConvertFrom-Json; } $SchemaValidation = Confirm-PowershaiObjectSchema $ResultObject $CheckScriptResult $CheckDetails.SchemaValidation = $SchemaValidation; return $SchemaValidation.valid; } verbose "Performing simple eq comparison..."; return $result -eq $CheckScriptResult } progress " CheckResult: $CheckResult"; if($CheckResult){ write-output -NoEnumerate $result; return; } } $error = New-PowershaiError POWERSHAI_RETRY_MAXREACHED -Props @{ details = $CheckDetails } throw $error; } function GetParamCallAlias { param($ParamName) $ParentInvocation = Get-Variable -Scope 1 MyInvocation -Value; $MyAliases =$ParentInvocation.MyCommand.Parameters[$ParamName].Aliases $MyAliasesReg = $MyAliases -Join "|"; $AliasFound = $ParentInvocation.Line -match "-\b($MyAliasesReg)\b" if($AliasFound){ return $matches[1]; } } function Bytes2Human { param ( [int64]$bytes ) if ($bytes -lt 1024) { return "$bytes B" } $units = @("KB", "MB", "GB", "TB", "PB", "EB") $index = 0 $value = $bytes / 1024 while ($value -ge 1024 -and $index -lt ($units.Count - 1)) { $value /= 1024 $index++ } return "{0:N2} {1}" -f $value, $units[$index] } |