PSSecretScanner.psm1

<#
.SYNOPSIS
    Scans for secrets in one or more folders or files.
.DESCRIPTION
    This function scans for secrets accidently exposed in one or more folder(s) or file(s).
    It requires the config.json file containing regexes and file extensions to scan.
 
    You can select which output stream to use to make it behave the way you want to in a pipeline,
    Or output the result to pipeline as an object to wrap it in your own script.
 
    Excludelist can be used to ignore false positives
    Exclusions must then be in the format
    <Full\path\to\file.txt>;<linenumber>;<Line>
    Ex.
        "C:\MyFiles\template.json;51;-----BEGIN RSA PRIVATE KEY-----"
        "C:\MyRepo\MyModule.psm1:18:password = supersecret!!"
.EXAMPLE
    PS C:\> Find-Secrets
    This command will scan the current directory, $PWD, for secrets using the default config.json.
.EXAMPLE
    PS C:\> Find-Secret -Path c:\MyPowerShellFiles\, C:\MyBicepFiles\MyModule.bicep
    This command will scan the c:\MyPowerShellFiles\ directory and the C:\MyBicepFiles\MyModule.bicep for secrets using the default config.json.
.EXAMPLE
    PS C:\> Find-Secret -Path c:\MyPowerShellFiles\ -OutputPrefence Output
    This command will scan the c:\MyPowerShellFiles\ directory for secrets using the default config.json.
    Output will be made to the default Output stream instead of Error.
.EXAMPLE
    PS C:\> Find-Secret -Path c:\MyPowerShellFiles\ -OutputPrefence Object
    This command will scan the c:\MyPowerShellFiles\ directory for secrets using the default config.json.
    Instead of outputting a string of the result to any stream, It will output a Select-String object that you can use in your own pipelines.
#>

function Find-Secret {
    [CmdletBinding()]
    param (
        # The folders and files to scan. Folders are recursively scanned.
        [ValidateScript({Test-Path $_}, ErrorMessage = "Path not found.")]
        [string[]]$Path = "$PWD",

        # Set the stream to output data to, or output the Select-String object to create your own handling.
        [ValidateSet('Output','Warning','Error','Object')]
        [string]$OutputPreference = 'Error',

        # Path to the config.json file. If you change this, make sure the format of the custom one is correct.
        [string[]]$ConfigPath = "$PSScriptRoot\config.json",

        # Path to exclude list.
        [ValidateScript({Test-Path $_}, ErrorMessage = "Excludelist path not found.")]
        [string]$Excludelist
    )

    try {
        $Config = Get-Content $ConfigPath | ConvertFrom-Json -AsHashtable
    }
    catch {
        Throw "Failed to get config. Is the format correct? $_"
    }

    $ScanFiles = Get-ChildItem $Path -File -Recurse | Where-Object -Property Extension -in $Config['fileextensions']
    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-ChildItem $ScanFiles | Select-String -Pattern $Pattern
    }
    
    if (-not [string]::IsNullOrEmpty($Excludelist)) {
        [string[]]$Exclusions = Get-Content $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) {
        $Result += "Path`tLine`tLineNumber`tPattern`n"
        foreach ($line in $res) {
            $Result += "$($line.Path)`t$($line.Line)`t$($line.LineNumber)`t$($line.Pattern)`n"
        }
    }

    switch ($OutputPreference) {
        'Output'  { Write-Output $Result }
        'Warning' { Write-Warning $Result }
        'Error'   { Write-Error $Result }
        'Object'  { $res }
    }
}