PSSecretScanner.psm1
#region AssertParameter function AssertParameter { <# .SYNOPSIS Simplifies custom error messages for ValidateScript .DESCRIPTION Windows PowerShell implementation of the ErrorMessage functionality available for ValidateScript in PowerShell core .EXAMPLE [ValidateScript({ Assert-Parameter -ScriptBlock {Test-Path $_} -ErrorMessage "Path not found." })] #> param( [Parameter(Position = 0)] [scriptblock] $ScriptBlock , [Parameter(Position = 1)] [string] $ErrorMessage = 'Failed parameter assertion' ) if (& $ScriptBlock) { $true } else { throw $ErrorMessage } } #endregion AssertParameter #region ConvertToHashtable function ConvertToHashtable { <# .SYNOPSIS Converts PowerShell object to hashtable .DESCRIPTION Converts PowerShell objects, including nested objets, arrays etc. to a hashtable .PARAMETER InputObject The object that you want to convert to a hashtable .EXAMPLE Get-Content -Raw -Path C:\Path\To\file.json | ConvertFrom-Json | ConvertTo-Hashtable .NOTES Based on function by Dave Wyatt found on Stack Overflow https://stackoverflow.com/questions/3740128/pscustomobject-to-hashtable #> param ( [Parameter(ValueFromPipeline)] $InputObject ) process { if ($null -eq $InputObject) { return $null } if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) { $collection = @( foreach ($object in $InputObject) { ConvertToHashtable -InputObject $object } ) Write-Output -NoEnumerate $collection } elseif ($InputObject -is [psobject]) { $hash = @{} foreach ($property in $InputObject.PSObject.Properties) { $hash[$property.Name] = ConvertToHashtable -InputObject $property.Value } $hash } else { $InputObject } } } #endregion ConvertToHashtable #region GetConfig function GetConfig { param ( $ConfigPath ) try { if ($PSVersionTable.PSEdition -eq 'Core') { $Config = Get-Content $ConfigPath -ErrorAction Stop | ConvertFrom-Json -AsHashtable } else { $Config = Get-Content $ConfigPath -ErrorAction Stop -Raw | ConvertFrom-Json | ConvertToHashtable } } catch { Throw "Failed to get config. $_" } $Config } #endregion GetConfig #region GetExclusions function GetExclusions { param ( $Excludelist ) [string[]]$Exclusions = Get-Content $Excludelist $Exclusions } #endregion GetExclusions $script:PSSSConfigPath = "$PSScriptRoot\config.json" #region Find-Secret function Find-Secret { [CmdletBinding(DefaultParameterSetName = 'Path')] param ( [Parameter(ParameterSetName = 'Path', Position = 0)] [ValidateScript({ AssertParameter -ScriptBlock {Test-Path $_} -ErrorMessage "Path not found." })] [string[]]$Path = "$PWD", [Parameter(ParameterSetName = 'Path')] [string[]]$Filetype, [Parameter(ParameterSetName = 'Path')] [bool]$Recursive = $true, [Parameter(ParameterSetName = 'File', Position = 0)] [ValidateScript({ AssertParameter -ScriptBlock {Test-Path $_} -ErrorMessage "File not found." })] [string]$File, [Parameter()] [ValidateSet('Output','Warning','Error','Object','IgnoreSecrets')] [string]$OutputPreference = 'Error', [Parameter()] [string]$ConfigPath = $script:PSSSConfigPath, [Parameter()] [ValidateScript({ AssertParameter -ScriptBlock {Test-Path $_} -ErrorMessage "Excludelist path not found." })] [string]$Excludelist ) $Config = GetConfig -ConfigPath $ConfigPath switch ($PSCmdLet.ParameterSetName) { 'Path' { if ( ($Path.Count -eq 1) -and ((Get-Item $Path[0]) -is [System.IO.FileInfo]) ) { [Array]$ScanFiles = Get-ChildItem $Path[0] -File } else { if ($Filetype -and $Filetype.Contains('*')) { [Array]$ScanFiles = Get-ChildItem $Path -File -Recurse:$Recursive } elseif ($Filetype) { $ScanExtensions = $Filetype | ForEach-Object { if (-not $_.StartsWith('.')) { ".$_" } else { $_ } } [Array]$ScanFiles = Get-ChildItem $Path -File -Recurse:$Recursive | Where-Object -Property Extension -in $ScanExtensions } else { [Array]$ScanFiles = Get-ChildItem $Path -File -Recurse:$Recursive | Where-Object -Property Extension -in $Config['fileextensions'] } } } 'File' { [Array]$ScanFiles = Get-ChildItem $File -File } } if ($ScanFiles.Count -ge 1) { Write-Verbose "Scanning files:`n$($ScanFiles.FullName -join ""`n"")" $Res = $Config['regexes'].Keys | ForEach-Object { $RegexName = $_ $Pattern = ($Config['regexes'])."$RegexName" Write-Verbose "Performing $RegexName scan`nPattern '$Pattern'`n" Get-Item $ScanFiles.FullName | Select-String -Pattern $Pattern } if (-not [string]::IsNullOrEmpty($Excludelist)) { [string[]]$Exclusions = GetExclusions $Excludelist Write-Verbose "Using excludelist $Excludelist. Found $($Exclusions.Count) exlude strings." $Res = $Res | Where-Object { "$($_.Path);$($_.LineNumber);$($_.Line)" -notin $Exclusions } } $Result = "Found $($Res.Count) strings.`n" if ($res.Count -gt 0) { if ($OutputPreference -eq 'IgnoreSecrets') { $Result = [string]::Empty foreach ($line in $res) { $Result += "$($line.Path);$($line.LineNumber);$($line.Line)`n" } } else { $Result += "Path`tLine`tLineNumber`tPattern`n" foreach ($line in $res) { $Result += "$($line.Path)`t$($line.Line)`t$($line.LineNumber)`t$($line.Pattern)`n" } } } } else { $Result = 'Found no files to scan' $res = @() } switch ($OutputPreference) { 'Output' { Write-Output $Result } 'IgnoreSecrets' { Write-Output $Result } 'Warning' { Write-Warning $Result } 'Error' { Write-Error $Result } 'Object' { $res } } } #endregion Find-Secret #region New-PSSSConfig function New-PSSSConfig { param ( [Parameter(Mandatory)] [string]$Path ) $ConfigFileName = Split-Path $script:PSSSConfigPath -leaf $InvokeSplat = @{ Path = $script:PSSSConfigPath Destination = $Path } if (Test-Path (Join-Path -Path $Path -ChildPath $ConfigFileName)) { Write-Warning 'Config file already exists!' $InvokeSplat.Add('Confirm',$true) } Copy-Item @InvokeSplat } #endregion New-PSSSConfig #region Write-SecretStatus function Write-SecretStatus { param () try { [array]$IsGit = (git status *>&1).ToString() if ( $IsGit[0] -eq 'fatal: not a git repository (or any of the parent directories): .git' ) { break } else { $FindSplat = @{ Recursive = $false OutputPreference = 'Object' } $ExcludePath = Join-Path -Path (git rev-parse --show-toplevel) -ChildPath '.ignoresecrets' if (Test-Path $ExcludePath) { $FindSplat.Add('Excludelist',$ExcludePath) } $Secrets = Find-Secret @FindSplat $SecretsCount = $Secrets.Count if ((Get-Command Prompt).ModuleName -eq 'posh-git') { if ($SecretsCount -ge 1) { $GitPromptSettings.DefaultPromptBeforeSuffix.ForegroundColor = 'Red' } else { $GitPromptSettings.DefaultPromptBeforeSuffix.ForegroundColor = 'LightBlue' } } Write-Output "[$SecretsCount]" } } catch {} } #endregion Write-SecretStatus |