├── .github └── workflows │ └── RunPSSecretScanner.yml ├── .gitignore ├── .ignoresecrets ├── CHANGELOG.md ├── Docs └── Help │ ├── Find-Secret.md │ ├── New-PSSSConfig.md │ └── Write-SecretStatus.md ├── Formatting ├── PSSecretScanner.Result.format.ps1 └── PSSecretScanner.ResultSet.format.ps1 ├── LICENSE ├── PSSecretScanner.Build.ps1 ├── PSSecretScanner.ezout.ps1 ├── README.md ├── Source ├── PSSecretScanner.format.ps1xml ├── PSSecretScanner.psd1 ├── PSSecretScanner.psm1 ├── PSSecretScanner.types.ps1xml ├── Private │ ├── AssertParameter.ps1 │ ├── ConvertToHashtable.ps1 │ ├── GetConfig.ps1 │ └── GetExclusions.ps1 ├── Public │ ├── Find-Secret.ps1 │ ├── New-PSSSConfig.ps1 │ └── Write-SecretStatus.ps1 └── config.json ├── Tests ├── RegexPatternTests │ ├── RegexPattern.Tests.ps1 │ └── TestCases.json └── UnitTests │ ├── AssertParameter.Tests.ps1 │ ├── ConvertToHashtable.Tests.ps1 │ ├── Find-Secret.Tests.ps1 │ └── New-PSSSConfig.Tests.ps1 ├── Types └── PSSecretScanner.ResultSet │ ├── get_Count.ps1 │ ├── get_FailedFailCount.ps1 │ └── get_FileCount.ps1 ├── action.yaml └── images ├── PSSecretScanner.png └── output.png /.github/workflows/RunPSSecretScanner.yml: -------------------------------------------------------------------------------- 1 | name: PSSecretScanner 2 | on: 3 | workflow_dispatch: 4 | jobs: 5 | ScanForSecrets: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | - uses: ./ 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/ExcludeList* 2 | 3 | # Testcoverage output from pester 4 | **/coverage.xml 5 | 6 | Debug/* 7 | Bin/* 8 | -------------------------------------------------------------------------------- /.ignoresecrets: -------------------------------------------------------------------------------- 1 | # Comments supported 2 | 3 | # Relative paths supported (starting with .\) 4 | ./Docs/Help/Find-Secret.md 5 | ./Source/config.json 6 | 7 | # Wildcards supported. All files within this and subfolders will be excluded. 8 | ./bin/* 9 | 10 | # Paths to files. All matches in these files will be excluded 11 | ./Tests/RegexPatternTests/TestCases.json 12 | C:/MyRepo/PSSecretScanner/README.md 13 | 14 | # Any directory separator character should work (Linux and Windows) 15 | .\README.md 16 | 17 | # Patterns on specific lines supported in the format 18 | # ;; 19 | C:/MyRepo/PSSecretScanner/Docs/Help/Find-Secret.md;51;"C:\MyFiles\template.json;51;-----BEGIN RSA PRIVATE KEY-----" 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | - 2022-10-26 - 2.0.1 4 | - Fixed pattern bug where quantifiers were wrong 5 | - Improved / simplified patterns (thank you [@mthreer](https://github.com/mthreer)) 6 | - 2022-10-24 - 2.0.0 7 | - Added Types and output formating (thank you [@StartAutomating](https://github.com/StartAutomating)) 8 | - Improved exclude list to support wildcards, relative paths, and more 9 | - changed parameter -Recurse (bool) to -NoRecurse (switch) to be more like PowerShell standard 10 | - Added GitHub Action (thank you [@StartAutomating](https://github.com/StartAutomating) for inspiration) 11 | - Minor speed and test improvements 12 | - Break out changelog to separate file 13 | - 2022-09-20 14 | - Increased speed by almost 50% by not fetching every file twice 🤦 15 | - Fixed tests that was badly written and returned false positives. 16 | - 2022-08-08 17 | - Added `-OutputPreference IgnoreSecrets` To make it easer to manage ExcludeLists. See help documentation for example. 18 | - Added support for ignorelist in Write-SecretStatus. 19 | - Added colours to Write-SecretStatus. 20 | - 1.0.8, 2022-07-29 21 | - Change from Get-ChildItem to Get-Item which is marginaly faster. (2 seconds/10000 objects) 22 | - Add boolean-Recurse parameter defaulted to $true to support non recursive scans ([#18](https://github.com/bjompen/PSSecretScanner/issues/18)) 23 | - Added Write-SecretStatus to add to posh-git profile ([#8](https://github.com/bjompen/PSSecretScanner/issues/8). 24 | - 2022-07-28 25 | - Added `-File` parameter ([#12](https://github.com/bjompen/PSSecretScanner/issues/12)) 26 | - Changed the **firebaseio** pattern to make scanning faster. 27 | - 2022-07-28 28 | - Added tests for Find-Secret ([#11](https://github.com/bjompen/PSSecretScanner/issues/11)) 29 | - Corrected logo (Really, how the hell does one write the wrong name! 🤦) 30 | - Moved GetExclusions to private function to make it easier to mock ([#11](https://github.com/bjompen/PSSecretScanner/issues/11)) 31 | - 2022-07-26 32 | - Moved GetConfig to external helper function in order to make Find-Secret easier to write tests for. ([#11](https://github.com/bjompen/PSSecretScanner/issues/11)) 33 | - 2022-07-24 34 | - Added a pattern for personal access token 35 | - Added tests for regex patterns (some, not all of them) ([#11](https://github.com/bjompen/PSSecretScanner/issues/11)) 36 | - Added unit tests for some functions (WIP) ([#11](https://github.com/bjompen/PSSecretScanner/issues/11)) 37 | - Added the logo, becuse it's fun to draw stuff. 38 | - 1.0.7, 2022-07-22 39 | - This is the first changelog entry so anything before this is without dates. ([#5](https://github.com/bjompen/PSSecretScanner/issues/5)) 40 | - Refactored the module to a folder structure. ([#10](https://github.com/bjompen/PSSecretScanner/issues/10)) 41 | - Added build script (Invoke-Build) 42 | - Changed from comment based to markdown documentation ([#7](https://github.com/bjompen/PSSecretScanner/issues/7)) 43 | - 1.0.6 44 | - Full support for PS 5.1 and later. 45 | - 1.0.5 46 | - Some messing about with PS7 support 47 | - 1.0.4 48 | - Added 24 patterns from the h33tlit list 49 | - 1.0.(1..3) 50 | - Added and changed some patterns and added some functionality 51 | - 1.0.0 52 | - First release. Basically a wrapper around `Select-String` with a .txt file of OWASP patterns. 53 | -------------------------------------------------------------------------------- /Docs/Help/Find-Secret.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: PSSecretScanner-help.xml 3 | Module Name: PSSecretScanner 4 | online version: 5 | schema: 2.0.0 6 | --- 7 | 8 | # Find-Secret 9 | 10 | ## SYNOPSIS 11 | 12 | Scans for secrets in one or more folders or files. 13 | 14 | ## SYNTAX 15 | 16 | ### Path (Default) 17 | ``` 18 | Find-Secret [[-Path] ] [-Filetype ] [-NoRecurse] [-ConfigPath ] 19 | [-Excludelist ] [] 20 | ``` 21 | 22 | ### File 23 | ``` 24 | Find-Secret [[-File] ] [-ConfigPath ] [-Excludelist ] [] 25 | ``` 26 | 27 | ## DESCRIPTION 28 | 29 | This function scans for secrets accidently exposed in one or more folder(s) or file(s). 30 | It requires the config.json file containing regexes and file extensions to scan. 31 | 32 | You can select which output stream to use to make it behave the way you want to in a pipeline, 33 | Or output the result to pipeline as an object to wrap it in your own script. 34 | 35 | Excludelist can be used to ignore false positives. 36 | 37 | Exclusions can be in the format 38 | > \;\;\ 39 | 40 | Ex. 41 | 42 | > "C:\MyFiles\template.json;51;-----BEGIN RSA PRIVATE KEY-----" 43 | > "C:\MyRepo\MyModule.psm1:18:password = supersecret!!" 44 | 45 | or excluding entire files 46 | Ex. 47 | 48 | > "C:\MyFiles\template.json" 49 | 50 | or excluding entire folders and all subfolders / files 51 | Ex. 52 | 53 | > "C:\MyFiles\\*" 54 | 55 | Relative paths are also supported (relative to the ignorefile) 56 | 57 | > ".\MySubFolder\\*" 58 | 59 | ## EXAMPLES 60 | 61 | ### EXAMPLE 1 62 | 63 | ```PowerShell 64 | Find-Secret 65 | ``` 66 | 67 | This command will scan the current directory, $PWD, and all subfolders for secrets using the default config.json. 68 | 69 | ### EXAMPLE 2 70 | 71 | ```PowerShell 72 | Find-Secret -Path c:\MyPowerShellFiles\, C:\MyBicepFiles\MyModule.bicep 73 | ``` 74 | 75 | This command will scan the c:\MyPowerShellFiles\ directory recursively and the C:\MyBicepFiles\MyModule.bicep for secrets using the default config.json. 76 | 77 | ### EXAMPLE 3 78 | 79 | ```PowerShell 80 | Find-Secret -Path c:\MyPowerShellFiles\ -NoRecurse 81 | ``` 82 | 83 | This command will scan only the c:\MyPowerShellFiles\ directory for secrets using the default config.json. 84 | Any subfolders will be excluded from scan. 85 | 86 | ### EXAMPLE 4 87 | 88 | ```PowerShell 89 | Find-Secret -Path c:\MyPowerShellFiles\ -Filetype 'bicep','.json' 90 | ``` 91 | 92 | This command will scan the c:\MyPowerShellFiles\ directory recursively for secrets using the default config.json. 93 | It will only scan files with the '.bicep' or '.json' extensions 94 | 95 | ### EXAMPLE 5 96 | 97 | ```PowerShell 98 | Find-Secret -Path c:\MyPowerShellFiles\ -Filetype '*' 99 | ``` 100 | 101 | This command will scan the c:\MyPowerShellFiles\ directory recursively for secrets using the default config.json. 102 | It will try to scan all filetypes in this folder including non clear text. This might be very slow. 103 | 104 | ## PARAMETERS 105 | 106 | ### -ConfigPath 107 | 108 | Path to the config.json file. 109 | If you change this, make sure the format of the custom one is correct. 110 | 111 | ```yaml 112 | Type: String 113 | Parameter Sets: (All) 114 | Aliases: 115 | 116 | Required: False 117 | Position: Named 118 | Default value: "$PSScriptRoot\config.json" 119 | Accept pipeline input: False 120 | Accept wildcard characters: False 121 | ``` 122 | 123 | ### -Excludelist 124 | 125 | Path to exclude list. 126 | 127 | ```yaml 128 | Type: String 129 | Parameter Sets: (All) 130 | Aliases: 131 | 132 | Required: False 133 | Position: Named 134 | Default value: None 135 | Accept pipeline input: False 136 | Accept wildcard characters: False 137 | ``` 138 | 139 | ### -File 140 | 141 | This parameter should be used to scan single files. 142 | 143 | In some cases using the -Path parameter for single file scans alongside extension patterns behaves unexpected. 144 | 145 | ```yaml 146 | Type: String 147 | Parameter Sets: File 148 | Aliases: 149 | 150 | Required: False 151 | Position: 0 152 | Default value: None 153 | Accept pipeline input: False 154 | Accept wildcard characters: False 155 | ``` 156 | 157 | ### -Filetype 158 | 159 | Filetype(s) to scan. 160 | If this parameter is set we will only scan files of type in thes list. 161 | Use '*' to scan all filetypes. 162 | (This will even try to scan non clear text files, and may be slow.) 163 | 164 | ```yaml 165 | Type: String[] 166 | Parameter Sets: Path 167 | Aliases: 168 | 169 | Required: False 170 | Position: Named 171 | Default value: None 172 | Accept pipeline input: False 173 | Accept wildcard characters: False 174 | ``` 175 | 176 | ### -NoRecurse 177 | 178 | Prevent recursive scan. If this switch is set we will _only_ scan the given folder, no subfolders. 179 | 180 | ```yaml 181 | Type: SwitchParameter 182 | Parameter Sets: Path 183 | Aliases: 184 | 185 | Required: False 186 | Position: Named 187 | Default value: None 188 | Accept pipeline input: False 189 | Accept wildcard characters: False 190 | ``` 191 | 192 | ### -Path 193 | 194 | The folders and files to scan. 195 | Folders are recursively scanned. 196 | 197 | ```yaml 198 | Type: String[] 199 | Parameter Sets: Path 200 | Aliases: 201 | 202 | Required: False 203 | Position: 0 204 | Default value: "$PWD" 205 | Accept pipeline input: False 206 | Accept wildcard characters: False 207 | ``` 208 | 209 | ### CommonParameters 210 | This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). 211 | 212 | ## INPUTS 213 | 214 | ## OUTPUTS 215 | 216 | ## NOTES 217 | 218 | ## RELATED LINKS 219 | 220 | [PSSecretScanner on GitHub](https://github.com/bjompen/PSSecretScanner) 221 | -------------------------------------------------------------------------------- /Docs/Help/New-PSSSConfig.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: PSSecretScanner-help.xml 3 | Module Name: PSSecretScanner 4 | online version: 5 | schema: 2.0.0 6 | --- 7 | 8 | # New-PSSSConfig 9 | 10 | ## SYNOPSIS 11 | Creates a new copy of the PSSecretScanner config.json file for custom configurations. 12 | 13 | ## SYNTAX 14 | 15 | ``` 16 | New-PSSSConfig [-Path] [] 17 | ``` 18 | 19 | ## DESCRIPTION 20 | This function copies the current modules config.json to a path where you may customise it and include or exclude your own settings. 21 | 22 | ## EXAMPLES 23 | 24 | ### EXAMPLE 1 25 | ``` 26 | New-PSSSConfig -Path C:\MyPWSHRepo\MyCystomSecretScannerConfig.json 27 | This command will copy the default config.json to C:\MyPWSHRepo\MyCystomSecretScannerConfig.json. 28 | ``` 29 | 30 | ## PARAMETERS 31 | 32 | ### -Path 33 | Path where the config.json will be copied to. 34 | 35 | ```yaml 36 | Type: String 37 | Parameter Sets: (All) 38 | Aliases: 39 | 40 | Required: True 41 | Position: 1 42 | Default value: None 43 | Accept pipeline input: False 44 | Accept wildcard characters: False 45 | ``` 46 | 47 | ### CommonParameters 48 | This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). 49 | 50 | ## INPUTS 51 | 52 | ## OUTPUTS 53 | 54 | ## NOTES 55 | 56 | ## RELATED LINKS 57 | -------------------------------------------------------------------------------- /Docs/Help/Write-SecretStatus.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: PSSecretScanner-help.xml 3 | Module Name: PSSecretScanner 4 | online version: 5 | schema: 2.0.0 6 | --- 7 | 8 | # Write-SecretStatus 9 | 10 | ## SYNOPSIS 11 | 12 | This command is created to get a quick and easy way of having secrets found shown in your prompt function. 13 | You can use it side by side with [posh-git](https://github.com/dahlbyk/posh-git), or as a stand alone function. 14 | 15 | ## SYNTAX 16 | 17 | ```PowerShell 18 | Write-SecretStatus 19 | ``` 20 | 21 | ## DESCRIPTION 22 | 23 | This command is created to get a quick and easy way of having secrets found shown in your prompt function. 24 | You can use it side by side with [posh-git](https://github.com/dahlbyk/posh-git), or as a stand alone function. 25 | 26 | --- 27 | 28 | To add output to your default prompt, create or edit your prompt function and add `Write-SecretStatus` where you want it to show. 29 | 30 | --- 31 | 32 | To add this to your posh-git prompt add the following to your `$PROFILE` script **after the `Import-Module posh-git` statement!** 33 | 34 | $GitPromptSettings.DefaultPromptBeforeSuffix.Text = ' $(Write-SecretStatus)' 35 | 36 | It will automatically set the output to red if secrets are found. 37 | 38 | If you have a file named `.ignoresecrets` in the rootfolder of your git repo it will use this for exclusions. 39 | 40 | --- 41 | 42 | You _may_ also add this to your oh-my-posh thing, but I don't use it and have no idea how that works. 43 | 44 | ## EXAMPLES 45 | 46 | ### EXAMPLE 8 47 | 48 | ```PowerShell 49 | $GitRoot = git rev-parse --show-toplevel 50 | $IgnoreFile = Join-Path -Path $GitRoot -ChildPath '.ignoresecrets' 51 | Find-Secret -Path $GitRoot -OutputPreference IgnoreSecrets | Out-File $IgnoreFile -Force 52 | ``` 53 | 54 | This command will find the root folder of the current git repo, 55 | and create a file called .ignoresecrets in it. 56 | It will output _all_ secrets currently found in the repository in to that folder in the correct format for an ignore file. 57 | It will then automatically pick this file up as IgnoreFile when running Write-SecretStatus. 58 | 59 | ## PARAMETERS 60 | 61 | ## INPUTS 62 | 63 | ### None 64 | 65 | ## OUTPUTS 66 | 67 | ### System.Object 68 | 69 | ## NOTES 70 | 71 | ## RELATED LINKS 72 | -------------------------------------------------------------------------------- /Formatting/PSSecretScanner.Result.format.ps1: -------------------------------------------------------------------------------- 1 | #requires -Module EZOut 2 | Write-FormatView -TypeName PSSecretScanner.Result -Property LineNumber, Path -GroupByProperty PatternName -------------------------------------------------------------------------------- /Formatting/PSSecretScanner.ResultSet.format.ps1: -------------------------------------------------------------------------------- 1 | #requires -Module EZOut 2 | Write-FormatView -TypeName PSSecretScanner.ResultSet -Action { 3 | Write-FormatViewExpression -Text "PSSecretScanner Scan Results" 4 | 5 | Write-FormatViewExpression -ScriptBlock { 6 | ' ' + (@(if ( $_.Results.Count -eq 0 ) { 7 | Format-RichText -ForegroundColor Verbose -InputObject ' @ ' 8 | } 9 | else { 10 | Format-RichText -ForegroundColor Error -InputObject ' @ ' 11 | }) -join '') + ' ' 12 | } 13 | 14 | Write-FormatViewExpression -ScriptBlock { 15 | "[ $($_.ScanStart.ToLongTimeString()) - $($_.ScanEnd.ToLongTimeString())] $([Math]::Round($_.ScanTimespan.TotalSeconds,2))s" 16 | } 17 | Write-FormatViewExpression -Newline 18 | 19 | Write-FormatViewExpression -ScriptBlock { 20 | $_.Results | Out-String 21 | } 22 | 23 | Write-FormatViewExpression -Newline 24 | 25 | Write-FormatViewExpression -If { 26 | $env:BUILD_BUILDID -and $_.Results.Count -gt 0 27 | } -ScriptBlock { 28 | @( 29 | "##vso[task.logissue type=error]$($_.Results.Count) secrets found$('!' * $_.Results.Count)" 30 | foreach ($bad in $_.results) { 31 | "##vso[task.logissue type=error;sourcepath=$($bad.Path);linenumber=$($bad.LineNumber)]$($bad.PatternName) found" 32 | } 33 | ) -join [Environment]::NewLine 34 | } 35 | 36 | Write-FormatViewExpression -If { 37 | $env:GITHUB_JOB -and $_.Results.Count -gt 0 38 | } -ScriptBlock { 39 | @( 40 | "::error::$($_.Results.Count) secrets found$('!' * $_.Results.Count)" 41 | foreach ($bad in $_.results) { 42 | "::error file=$($bad.Path),line=$($bad.LineNumber)::$($bad.PatternName) found" 43 | } 44 | ) -join [Environment]::NewLine 45 | } 46 | 47 | Write-FormatViewExpression -If { 48 | $_.Results.Count -gt 0 -and -not ($env:GITHUB_JOB -or $env:BUILD_BUILDID) 49 | } -ScriptBlock { 50 | "found $($_.Results.Count) secrets$('!' * $_.Results.Count)" 51 | } -ForegroundColor Error 52 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Björn Sundling 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PSSecretScanner.Build.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules 'InvokeBuild', 'PlatyPS', 'Pester', 'EZOut' 2 | 3 | [string]$ModuleName = 'PSSecretScanner' 4 | [string]$ModuleSourcePath = "$PSScriptRoot\Source" 5 | [string]$HelpSourcePath = "$PSScriptRoot\Docs\Help" 6 | [string]$EzoutSourcePath = "$PSScriptRoot" 7 | 8 | [string]$Version = '2.0.1' 9 | 10 | [string]$OutputPath = "$PSScriptRoot\Bin\$ModuleName\$Version" 11 | 12 | task Clean { 13 | If (Test-Path -Path $OutputPath) { 14 | "Removing existing files and folders in $OutputPath" 15 | Get-ChildItem $OutputPath | Remove-Item -Force -Recurse 16 | } 17 | Else { 18 | "$OutputPath is not present, nothing to clean up." 19 | $Null = New-Item -ItemType Directory -Path $OutputPath 20 | } 21 | } 22 | 23 | task Unit_Tests { 24 | # .$PSScriptRoot\Tests\TestRunner.ps1 -Verbosity Normal -CodeCoverage 25 | Invoke-Pester .\Tests -Output Detailed 26 | } 27 | 28 | task RunScriptAnalyzer { 29 | Invoke-ScriptAnalyzer -Path $ModuleSourcePath -Recurse -Severity Error -EnableExit 30 | } 31 | 32 | Task Build_Documentation { 33 | New-ExternalHelp -Path $HelpSourcePath -OutputPath "$OutputPath\en-US" 34 | } 35 | 36 | Task Build_TypesAndFormat { 37 | & "$EzoutSourcePath\PSSecretScanner.ezout.ps1" 38 | } 39 | 40 | task Compile_Module { 41 | $PSM1Name = "$ModuleName.psm1" 42 | New-Item -Name $PSM1Name -Path $OutputPath -ItemType File -Force 43 | $PSM1Path = (Join-Path -Path $OutputPath -ChildPath $PSM1Name) 44 | 45 | $PSD1Name = "$ModuleName.psd1" 46 | New-Item -Name $PSD1Name -Path $OutputPath -ItemType File -Force 47 | $PSD1Path = (Join-Path -Path $OutputPath -ChildPath $PSD1Name) 48 | 49 | $ExportedFunctionList = [System.Collections.Generic.List[string]]::new() 50 | 51 | # Private functions 52 | Get-ChildItem "$ModuleSourcePath\Private" *.ps1 | ForEach-Object { 53 | $FileContent = Get-Content $_.FullName 54 | "#region $($_.BaseName)`n" | Out-File $PSM1Path -Append 55 | $FileContent | Out-File $PSM1Path -Append 56 | "#endregion $($_.BaseName)`n" | Out-File $PSM1Path -Append 57 | } 58 | 59 | # Public functions 60 | '$script:PSSSConfigPath = "$PSScriptRoot\config.json"' | Out-File $PSM1Path -Append 61 | "`n" | Out-File $PSM1Path -Append 62 | 63 | Get-ChildItem "$ModuleSourcePath\Public" *.ps1 | ForEach-Object { 64 | $ExportedFunctionList.Add($_.BaseName) 65 | 66 | $FileContent = Get-Content $_.FullName 67 | "#region $($_.BaseName)`n" | Out-File $PSM1Path -Append 68 | $FileContent | Out-File $PSM1Path -Append 69 | "#endregion $($_.BaseName)`n" | Out-File $PSM1Path -Append 70 | } 71 | 72 | # Manifest 73 | $ManifestContent = (Get-Content "$ModuleSourcePath\$ModuleName.psd1" ) -replace 'ModuleVersion\s*=\s*[''"][0-9\.]{1,10}[''"]',"Moduleversion = '$Version'" -replace 'FunctionsToExport\s*=\s*[''"]\*[''"]',"FunctionsToExport = @('$($ExportedFunctionList -join "','")')" 74 | $ManifestContent | Out-File $PSD1Path 75 | 76 | # Formating and types 77 | Copy-Item "$ModuleSourcePath\*.ps1xml" -Destination $OutputPath 78 | } 79 | 80 | task Include_Resources { 81 | Copy-Item -Path $PSScriptRoot\Source\config.json -Destination $OutputPath 82 | } 83 | 84 | # task Publish_Module_To_PSGallery { 85 | # Remove-Module -Name 'PSCodeHealth' -Force -ErrorAction SilentlyContinue 86 | 87 | # Write-Host "OutputModulePath : $($Settings.OutputModulePath)" 88 | # Write-Host "PSGalleryKey : $($Settings.PSGalleryKey)" 89 | # Get-PackageProvider -ListAvailable 90 | # Publish-Module -Path $Settings.OutputModulePath -NuGetApiKey $Settings.PSGalleryKey -Verbose 91 | # } 92 | 93 | Get-Module -Name $ModuleName | Remove-Module -Force 94 | # Default task : 95 | task . Clean, 96 | Unit_Tests, 97 | RunScriptAnalyzer, 98 | Build_Documentation, 99 | Build_TypesAndFormat, 100 | Compile_Module, 101 | Include_Resources 102 | -------------------------------------------------------------------------------- /PSSecretScanner.ezout.ps1: -------------------------------------------------------------------------------- 1 | #requires -Module EZOut 2 | # Install-Module EZOut or https://github.com/StartAutomating/EZOut 3 | $myFile = $MyInvocation.MyCommand.ScriptBlock.File 4 | $myModuleName = 'PSSecretScanner' 5 | $myRoot = $myFile | Split-Path 6 | Push-Location $myRoot 7 | $formatting = @( 8 | # Add your own Write-FormatView here, 9 | # or put them in a Formatting or Views directory 10 | foreach ($potentialDirectory in 'Formatting','Views') { 11 | Join-Path $myRoot $potentialDirectory | 12 | Get-ChildItem -ea ignore | 13 | Import-FormatView -FilePath {$_.Fullname} 14 | } 15 | ) 16 | 17 | $destinationRoot = $myRoot 18 | $destinationRoot = Join-Path $destinationRoot 'Source' 19 | 20 | if ($formatting) { 21 | $myFormatFile = Join-Path $destinationRoot "$myModuleName.format.ps1xml" 22 | $formatting | Out-FormatData -Module $MyModuleName | Set-Content $myFormatFile -Encoding UTF8 23 | Get-Item $myFormatFile 24 | } 25 | 26 | $types = @( 27 | # Add your own Write-TypeView statements here 28 | # or declare them in the 'Types' directory 29 | Join-Path $myRoot Types | 30 | Get-Item -ea ignore | 31 | Import-TypeView 32 | 33 | ) 34 | 35 | if ($types) { 36 | $myTypesFile = Join-Path $destinationRoot "$myModuleName.types.ps1xml" 37 | $types | Out-TypeData | Set-Content $myTypesFile -Encoding UTF8 38 | Get-Item $myTypesFile 39 | } 40 | Pop-Location 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![PSSecretScanner logo goes here](./images/PSSecretScanner.png) 2 | 3 | # PSSecretScanner 4 | 5 | Super simple passwordscanner built using PowerShell. 6 | 7 | Scan your code, files, folders, and repos for accidentily exposed secrets using PowerShell. 8 | 9 | ## Features 10 | 11 | - Give a list of files to scan and we will check for any pattern matches in those files. 12 | 13 | - Outputs the result and metadata. (Use [Get-Member](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-member?view=powershell-7.2) to get all scan data) 14 | 15 | ![Example output](./images/output.png) 16 | 17 | - Use an excludelist to prevent false positives, or if you _really_ want to include secrets in your code, by creating a exclude file and passing it to the `-Excludelist` parameter. Either be specific and include File, LineNumber, Pattern, _or_ use wildcards to exclude entire files or folders. 18 | 19 | ```Text 20 | # Comments supported 21 | 22 | # Relative paths supported (starting with .\) 23 | # NOTE! Relative paths are calculated _relative_ to the excludelist path. 24 | # If this file is located in c:\mypath\.ignoresecrets 25 | .\Docs\Help\Find-Secret.md 26 | .\Source\config.json 27 | # The resolved exclude paths will be c:\mypath\Docs\Help\Find-Secret.md and c:\mypath\Source\config.json 28 | 29 | # Wildcards supported. All files within this and subfolders will be excluded. 30 | .\bin\* 31 | 32 | # Paths to files. All matches in these files will be excluded 33 | .\Tests\RegexPatternTests\TestCases.json 34 | C:\MyRepo\PSSecretScanner\README.md 35 | 36 | # Patterns on specific lines supported in the format 37 | # ;; 38 | .\ExcludeList.csv;1;"C:\BicepLab\template.json;51;-----BEGIN RSA PRIVATE KEY-----" 39 | C:\MyRepo\PSSecretScanner\Docs\Help\Find-Secret.md;51;"C:\MyFiles\template.json;51;-----BEGIN RSA PRIVATE KEY-----" 40 | ``` 41 | 42 | To have `Write-SecretStatus` automatically pick up and use your ignore list for all your repo, name your excludelist `.ignoresecrets` and put it in your repo root folder! 43 | 44 | ## Installation 45 | 46 | - From the PSGallery, run `Install-Module PSSecretScanner` 47 | 48 | - Clone this repo, and run `Invoke-Build` to build the module localy. 49 | 50 | ## Background 51 | 52 | I couldn't find a proper secret scanner for PowerShell so I wrote my own. 53 | 54 | From the beginning it was just a list of regex patterns stolen from the [OWASP SEDATED security scanner repo](https://github.com/OWASP/SEDATED) that I ran through `Select-String`, as I thought the OWASP tools was way to advanced for my needs, and way to hard to wrap in a powershell script. 55 | From there it kind of grew, and hopefully it will grow even more. 56 | 57 | ## About Regex patterns 58 | 59 | - The baseline is the list found at the OWASP repo, but converted to PowerShell Regex standard (PCRE I think it's called..) 60 | - Added `_Azure_AccountKey` pattern found at [Detect-secrets from YELP](https://github.com/Yelp/detect-secrets) 61 | - Added patterns from [h33tlit](https://github.com/h33tlit/secret-regex-list#readme) (thank you [Simon Wåhlin](https://github.com/SimonWahlin/) for telling me) 62 | 63 | _The added underscore `_` to names in the pattern list is simply to make them easier to work with in PowerShell._ 64 | 65 | ## Features to add 66 | 67 | Yes, even keeping it simple there are stuff I might want to add some day, or if you want to, feel free to create a PR. 68 | 69 | - Parallelization - make it faster on huge repos. 70 | - More filetypes! I kind of just winged it for now. 71 | -------------------------------------------------------------------------------- /Source/PSSecretScanner.format.ps1xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PSSecretScanner.Result 7 | 8 | PSSecretScanner.Result 9 | 10 | 11 | PatternName 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | LineNumber 25 | 26 | 27 | Path 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | PSSecretScanner.ResultSet 36 | 37 | PSSecretScanner.ResultSet 38 | 39 | 40 | 41 | 42 | 43 | PSSecretScanner Scan Results 44 | 45 | $moduleName = 'PSSecretScanner' 46 | 47 | do { 48 | $lm = Get-Module -Name $moduleName -ErrorAction Ignore 49 | if (-not $lm) { continue } 50 | if ($lm.FormatPartsLoaded) { break } 51 | $wholeScript = @(foreach ($formatFilePath in $lm.exportedFormatFiles) { 52 | foreach ($partNodeName in Select-Xml -LiteralPath $formatFilePath -XPath "/Configuration/Controls/Control/Name[starts-with(., '$')]") { 53 | $ParentNode = $partNodeName.Node.ParentNode 54 | "$($ParentNode.Name)={ 55 | $($ParentNode.CustomControl.CustomEntries.CustomEntry.CustomItem.ExpressionBinding.ScriptBlock)}" 56 | } 57 | }) -join [Environment]::NewLine 58 | New-Module -Name "${ModuleName}.format.ps1xml" -ScriptBlock ([ScriptBlock]::Create(($wholeScript + ';Export-ModuleMember -Variable *'))) | 59 | Import-Module -Global 60 | $onRemove = [ScriptBlock]::Create("Remove-Module '${ModuleName}.format.ps1xml'") 61 | 62 | if (-not $lm.OnRemove) { 63 | $lm.OnRemove = $onRemove 64 | } else { 65 | $lm.OnRemove = [ScriptBlock]::Create($onRemove.ToString() + '' + [Environment]::NewLine + $lm.OnRemove) 66 | } 67 | $lm | Add-Member NoteProperty FormatPartsLoaded $true -Force 68 | 69 | } while ($false) 70 | 71 | 72 | 73 | ' ' + (@(if ( $_.Results.Count -eq 0 ) { 74 | & ${PSSecretScanner_Format-RichText} -ForegroundColor Verbose -InputObject ' @ ' 75 | } 76 | else { 77 | & ${PSSecretScanner_Format-RichText} -ForegroundColor Error -InputObject ' @ ' 78 | }) -join '') + ' ' 79 | 80 | 81 | 82 | 83 | "[ $($_.ScanStart.ToLongTimeString()) - $($_.ScanEnd.ToLongTimeString())] $([Math]::Round($_.ScanTimespan.TotalSeconds,2))s" 84 | 85 | 86 | 87 | 88 | 89 | $_.Results | Out-String 90 | 91 | 92 | 93 | 94 | 95 | 96 | $env:BUILD_BUILDID -and $_.Results.Count -gt 0 97 | 98 | 99 | 100 | @( 101 | "##vso[task.logissue type=error]$($_.Results.Count) secrets found$('!' * $_.Results.Count)" 102 | foreach ($bad in $_.results) { 103 | "##vso[task.logissue type=error;sourcepath=$($bad.Path);linenumber=$($bad.LineNumber)]$($bad.PatternName) found" 104 | } 105 | ) -join [Environment]::NewLine 106 | 107 | 108 | 109 | 110 | 111 | $env:GITHUB_JOB -and $_.Results.Count -gt 0 112 | 113 | 114 | 115 | @( 116 | "::error::$($_.Results.Count) secrets found$('!' * $_.Results.Count)" 117 | foreach ($bad in $_.results) { 118 | "::error file=$($bad.Path),line=$($bad.LineNumber)::$($bad.PatternName) found" 119 | } 120 | ) -join [Environment]::NewLine 121 | 122 | 123 | 124 | @(& ${PSSecretScanner_Format-RichText} -ForegroundColor 'Error' -NoClear) -join '' 125 | 126 | 127 | 128 | 129 | $_.Results.Count -gt 0 -and -not ($env:GITHUB_JOB -or $env:BUILD_BUILDID) 130 | 131 | 132 | 133 | "found $($_.Results.Count) secrets$('!' * $_.Results.Count)" 134 | 135 | 136 | 137 | @(& ${PSSecretScanner_Format-RichText} -ForegroundColor 'Error' ) -join '' 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | ${PSSecretScanner_Format-RichText} 148 | 149 | 150 | 151 | 152 | 153 | 154 | <# 155 | .Synopsis 156 | Formats the text color of output 157 | .Description 158 | Formats the text color of output 159 | 160 | * ForegroundColor 161 | * BackgroundColor 162 | * Bold 163 | * Underline 164 | .Notes 165 | Stylized Output works in two contexts at present: 166 | * Rich consoles (Windows Terminal, PowerShell.exe, Pwsh.exe) (when $host.UI.SupportsVirtualTerminal) 167 | * Web pages (Based off the presence of a $Request variable, or when $host.UI.SupportsHTML (you must add this property to $host.UI)) 168 | #> 169 | [Management.Automation.Cmdlet("Format","Object")] 170 | [ValidateScript({ 171 | $canUseANSI = $host.UI.SupportsVirtualTerminal 172 | $canUseHTML = $Request -or $host.UI.SupportsHTML -or $OutputMode -eq 'HTML' 173 | if (-not ($canUseANSI -or $canUseHTML)) { return $false} 174 | return $true 175 | })] 176 | param( 177 | # The input object 178 | [Parameter(ValueFromPipeline)] 179 | [PSObject] 180 | $InputObject, 181 | 182 | # The foreground color 183 | [string]$ForegroundColor, 184 | 185 | # The background color 186 | [string]$BackgroundColor, 187 | 188 | # If set, will render as bold 189 | [switch]$Bold, 190 | 191 | # If set, will render as italic. 192 | [switch]$Italic, 193 | 194 | # If set, will render as faint 195 | [switch]$Faint, 196 | 197 | # If set, will render as hidden text. 198 | [switch]$Hide, 199 | 200 | # If set, will render as blinking (not supported in all terminals or HTML) 201 | [switch]$Blink, 202 | 203 | # If set, will render as strikethru 204 | [Alias('Strikethrough', 'Crossout')] 205 | [switch]$Strikethru, 206 | 207 | # If set, will underline text 208 | [switch]$Underline, 209 | 210 | # If set, will double underline text. 211 | [switch]$DoubleUnderline, 212 | 213 | # If set, will invert text 214 | [switch]$Invert, 215 | # If set, will not clear formatting 216 | [switch]$NoClear 217 | ) 218 | 219 | begin { 220 | $canUseANSI = $host.UI.SupportsVirtualTerminal 221 | $canUseHTML = $Request -or $host.UI.SupportsHTML -or $OutputMode -eq 'HTML' 222 | $knownStreams = @{ 223 | Output='';Error='BrightRed';Warning='BrightYellow'; 224 | Verbose='BrightCyan';Debug='Yellow';Progress='Cyan'; 225 | Success='BrightGreen';Failure='Red';Default=''} 226 | $esc = [char]0x1b 227 | $standardColors = 'Black', 'Red', 'Green', 'Yellow', 'Blue','Magenta', 'Cyan', 'White' 228 | $brightColors = 'BrightBlack', 'BrightRed', 'BrightGreen', 'BrightYellow', 'BrightBlue','BrightMagenta', 'BrightCyan', 'BrightWhite' 229 | 230 | $n =0 231 | $cssClasses = @() 232 | $styleAttributes = 233 | @(:nextColor foreach ($hc in $ForegroundColor,$BackgroundColor) { 234 | $n++ 235 | if (-not $hc) { continue } 236 | if ($hc[0] -eq $esc) { 237 | if ($canUseANSI) { 238 | $hc; continue 239 | } 240 | } 241 | 242 | $ansiStartPoint = if ($n -eq 1) { 30 } else { 40 } 243 | if ($knownStreams.ContainsKey($hc)) { 244 | $i = $brightColors.IndexOf($knownStreams[$hc]) 245 | if ($canUseHTML) { 246 | $cssClasses += $hc 247 | } else { 248 | if ($i -ge 0 -and $canUseANSI) { 249 | '' + $esc + "[1;$($ansiStartPoint + $i)m" 250 | } else { 251 | $i = $standardColors.IndexOf($knownStreams[$hc]) 252 | if ($i -ge 0 -and $canUseANSI) { 253 | '' + $esc + "[1;$($ansiStartPoint + $i)m" 254 | } elseif ($i -le 0 -and $canUseANSI) { 255 | '' + $esc + "[$($ansistartpoint + 8):5m" 256 | } 257 | } 258 | } 259 | continue nextColor 260 | } 261 | elseif ($standardColors -contains $hc) { 262 | for ($i = 0; $i -lt $standardColors.Count;$i++) { 263 | if ($standardColors[$i] -eq $hc) { 264 | if ($canUseANSI -and -not $canUseHTML) { 265 | '' + $esc + "[$($ansiStartPoint + $i)m" 266 | } else { 267 | $cssClasses += $standardColors[$i] 268 | } 269 | continue nextColor 270 | } 271 | } 272 | } elseif ($brightColors -contains $hc) { 273 | for ($i = 0; $i -lt $brightColors.Count;$i++) { 274 | if ($brightColors[$i] -eq $hc) { 275 | if ($canUseANSI -and -not $canUseHTML) { 276 | '' + $esc + "[1;$($ansiStartPoint + $i)m" 277 | } else { 278 | $cssClasses += $standardColors[$i] 279 | } 280 | continue nextColor 281 | } 282 | } 283 | } 284 | elseif ($psStyle -and $psStyle.Formatting.$hc -and 285 | $psStyle.Formatting.$hc -match '^\e') { 286 | if ($canUseANSI -and -not $canUseHTML) { 287 | $psStyle.Formatting.$hc 288 | } else { 289 | $cssClasses += "formatting-$hc" 290 | } 291 | } 292 | elseif (-not $n -and $psStyle -and $psStyle.Foreground.$hc -and 293 | $psStyle.Foreground.$hc -match '^\e' ) { 294 | if ($canUseANSI -and -not $canUseHTML) { 295 | $psStyle.Foreground.$hc 296 | } else { 297 | $cssClasses += "foreground-$hc" 298 | } 299 | } 300 | elseif ($n -and $psStyle -and $psStyle.Background.$hc -and 301 | $psStyle.Background.$hc -match '^\e') { 302 | if ($canUseANSI -and -not $canUseHTML) { 303 | $psStyle.Background.$hc 304 | } else { 305 | $cssClasses += "background-$hc" 306 | } 307 | } 308 | 309 | 310 | 311 | if ($hc -and $hc -notmatch '^[\#\e]') { 312 | $placesToLook= 313 | @(if ($hc.Contains('.')) { 314 | $module, $setting = $hc -split '\.', 2 315 | $theModule = Get-Module $module 316 | $theModule.PrivateData.Color, 317 | $theModule.PrivateData.Colors, 318 | $theModule.PrivateData.Colour, 319 | $theModule.PrivateData.Colours, 320 | $theModule.PrivateData.EZOut, 321 | $global:PSColors, 322 | $global:PSColours 323 | } else { 324 | $setting = $hc 325 | $moduleColorSetting = $theModule.PrivateData.PSColors.$setting 326 | }) 327 | 328 | foreach ($place in $placesToLook) { 329 | if (-not $place) { continue } 330 | foreach ($propName in $setting -split '\.') { 331 | $place = $place.$propName 332 | if (-not $place) { break } 333 | } 334 | if ($place -and "$place".StartsWith('#') -and 4,7 -contains "$place".Length) { 335 | $hc = $place 336 | continue 337 | } 338 | } 339 | if (-not $hc.StartsWith -or -not $hc.StartsWith('#')) { 340 | continue 341 | } 342 | } 343 | $r,$g,$b = if ($hc.Length -eq 7) { 344 | [int]::Parse($hc[1..2]-join'', 'HexNumber') 345 | [int]::Parse($hc[3..4]-join '', 'HexNumber') 346 | [int]::Parse($hc[5..6] -join'', 'HexNumber') 347 | }elseif ($hc.Length -eq 4) { 348 | [int]::Parse($hc[1], 'HexNumber') * 16 349 | [int]::Parse($hc[2], 'HexNumber') * 16 350 | [int]::Parse($hc[3], 'HexNumber') * 16 351 | } 352 | 353 | if ($canUseHTML) { 354 | if ($n -eq 1) { "color:$hc" } 355 | elseif ($n -eq 2) { "background-color:$hc"} 356 | } 357 | elseif ($canUseANSI) { 358 | if ($n -eq 1) { $esc+"[38;2;$r;$g;${b}m" } 359 | elseif ($n -eq 2) { $esc+"[48;2;$r;$g;${b}m" } 360 | } 361 | 362 | }) 363 | 364 | 365 | $styleAttributes += @( 366 | if ($Bold) { 367 | if ($canUseHTML) {"font-weight:bold"} 368 | elseif ($canUseANSI) { '' + $esc + "[1m" } 369 | } 370 | if ($Faint) { 371 | if ($canUseHTML) { "opacity:.5" } 372 | elseif ($canUseANSI) { '' + $esc + "[2m" } 373 | } 374 | if ($Italic) { 375 | if ($canUseHTML) { "font-weight:bold" } 376 | elseif ($canUseANSI) {'' + $esc + "[3m" } 377 | } 378 | 379 | if ($Underline -and -not $doubleUnderline) { 380 | if ($canUseHTML) { "text-decoration:underline"} 381 | elseif ($canUseANSI) {'' +$esc + "[4m" } 382 | } 383 | 384 | if ($Blink) { 385 | if ($canUseANSI) { '' +$esc + "[5m" } 386 | } 387 | 388 | if ($invert) { 389 | if ($canUseHTML) {"filter:invert(100%)"} 390 | elseif ($canUseANSI) { '' + $esc + "[7m"} 391 | } 392 | 393 | if ($hide) { 394 | if ($canUseHTML) {"opacity:0"} 395 | elseif ($canUseANSI) { '' + $esc + "[8m"} 396 | } 397 | 398 | if ($Strikethru) { 399 | if ($canUseHTML) {"text-decoration: line-through"} 400 | elseif ($canUseANSI) { '' +$esc + "[9m" } 401 | } 402 | 403 | if ($DoubleUnderline) { 404 | if ($canUseHTML) { "border-bottom: 3px double;"} 405 | elseif ($canUseANSI) {'' +$esc + "[21m" } 406 | } 407 | 408 | ) 409 | 410 | $header = 411 | if ($canUseHTML) { 412 | "<span$( 413 | if ($styleAttributes) { " style='$($styleAttributes -join ';')'"} 414 | )$( 415 | if ($cssClasses) { " class='$($cssClasses -join ' ')'"} 416 | )>" 417 | } elseif ($canUseANSI) { 418 | $styleAttributes -join '' 419 | } 420 | } 421 | 422 | process { 423 | if ($header) { 424 | "$header" + "$(if ($inputObject) { $inputObject | Out-String})".Trim() 425 | } 426 | elseif ($inputObject) { 427 | ($inputObject | Out-String).Trim() 428 | } 429 | } 430 | 431 | end { 432 | 433 | if (-not $NoClear) { 434 | if ($canUseHTML) { 435 | "</span>" 436 | } 437 | elseif ($canUseANSI) { 438 | if ($Bold -or $Faint) { 439 | "$esc[22m" 440 | } 441 | if ($Italic) { 442 | "$esc[23m" 443 | } 444 | if ($Underline -or $doubleUnderline) { 445 | "$esc[24m" 446 | } 447 | if ($Blink) { 448 | "$esc[25m" 449 | } 450 | if ($Invert) { 451 | "$esc[27m" 452 | } 453 | if ($hide) { 454 | "$esc[28m" 455 | } 456 | if ($Strikethru) { 457 | "$esc[29m" 458 | } 459 | if ($ForegroundColor) { 460 | "$esc[39m" 461 | } 462 | if ($BackgroundColor) { 463 | "$esc[49m" 464 | } 465 | 466 | if (-not ($Underline -or $Bold -or $Invert -or $ForegroundColor -or $BackgroundColor)) { 467 | '' + $esc + '[0m' 468 | } 469 | } 470 | } 471 | } 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | -------------------------------------------------------------------------------- /Source/PSSecretScanner.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'PSSecretScanner' 3 | # 4 | # Generated by: Björn Sundling 5 | # 6 | # Generated on: 2022-02-10 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'PSSecretScanner.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '2.0.0' 16 | 17 | # Supported PSEditions 18 | CompatiblePSEditions = @('Core','Desktop') 19 | 20 | # ID used to uniquely identify this module 21 | GUID = '8ee01e4c-44a3-409d-b6e9-e73ff72f1556' 22 | 23 | # Author of this module 24 | Author = 'Björn Sundling' 25 | 26 | # Company or vendor of this module 27 | CompanyName = 'bjompen.com' 28 | 29 | # Copyright statement for this module 30 | Copyright = 'Björn Sundling' 31 | 32 | # Description of the functionality provided by this module 33 | Description = 'Scan for secrets in code to prevent accidentaly commited secrets' 34 | 35 | # Minimum version of the PowerShell engine required by this module 36 | PowerShellVersion = '5.1' 37 | 38 | # Name of the PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # ClrVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | # RequiredModules = @() 55 | 56 | # Assemblies that must be loaded prior to importing this module 57 | # RequiredAssemblies = @() 58 | 59 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 60 | # ScriptsToProcess = @() 61 | 62 | # Type files (.ps1xml) to be loaded when importing this module 63 | TypesToProcess = @('PSSecretScanner.types.ps1xml') 64 | 65 | # Format files (.ps1xml) to be loaded when importing this module 66 | FormatsToProcess = @('PSSecretScanner.format.ps1xml') 67 | 68 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 69 | # NestedModules = @() 70 | 71 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 72 | FunctionsToExport = @( 73 | 'Find-Secret', 74 | 'New-PSSSConfig', 75 | 'Write-SecretStatus' 76 | ) 77 | 78 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 79 | # CmdletsToExport = '*' 80 | 81 | # Variables to export from this module 82 | # VariablesToExport = '*' 83 | 84 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 85 | # AliasesToExport = '*' 86 | 87 | # DSC resources to export from this module 88 | # DscResourcesToExport = @() 89 | 90 | # List of all modules packaged with this module 91 | # ModuleList = @() 92 | 93 | # List of all files packaged with this module 94 | # FileList = @() 95 | 96 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 97 | PrivateData = @{ 98 | 99 | PSData = @{ 100 | 101 | # Tags applied to this module. These help with module discovery in online galleries. 102 | Tags = @('Secret','SecretScanner') 103 | 104 | # A URL to the license for this module. 105 | LicenseUri = 'https://github.com/bjompen/PSSecretScanner/blob/main/LICENSE' 106 | 107 | # A URL to the main website for this project. 108 | ProjectUri = 'https://github.com/bjompen/PSSecretScanner' 109 | 110 | # A URL to an icon representing this module. 111 | # IconUri = '' 112 | 113 | # ReleaseNotes of this module 114 | # ReleaseNotes = '' 115 | 116 | # Prerelease string of this module 117 | # Prerelease = '' 118 | 119 | # Flag to indicate whether the module requires explicit user acceptance for install/update/save 120 | # RequireLicenseAcceptance = $false 121 | 122 | # External dependent modules of this module 123 | # ExternalModuleDependencies = @() 124 | 125 | } # End of PSData hashtable 126 | 127 | } # End of PrivateData hashtable 128 | 129 | # HelpInfo URI of this module 130 | # HelpInfoURI = '' 131 | 132 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 133 | # DefaultCommandPrefix = '' 134 | 135 | } 136 | 137 | -------------------------------------------------------------------------------- /Source/PSSecretScanner.psm1: -------------------------------------------------------------------------------- 1 | # Set config path 2 | $script:PSSSConfigPath = "$PSScriptRoot\config.json" 3 | 4 | # import private functions 5 | foreach ($file in (Get-ChildItem "$PSScriptRoot\Private\*.ps1")) 6 | { 7 | try { 8 | Write-Verbose "Importing $($file.FullName)" 9 | . $file.FullName 10 | } 11 | catch { 12 | Write-Error "Failed to import '$($file.FullName)'. $_" 13 | } 14 | } 15 | 16 | # import public functions 17 | foreach ($file in (Get-ChildItem "$PSScriptRoot\Public\*.ps1")) 18 | { 19 | try { 20 | Write-Verbose "Importing $($file.FullName)" 21 | . $file.FullName 22 | } 23 | catch { 24 | Write-Error "Failed to import '$($file.FullName)'. $_" 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /Source/PSSecretScanner.types.ps1xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PSSecretScanner.ResultSet 6 | 7 | 8 | Count 9 | 10 | $this.Results.Count 11 | 12 | 13 | 14 | 15 | FailedFailCount 16 | 17 | @($this.Results | Select-Object -ExpandProperty Path -Unique).Length 18 | 19 | 20 | 21 | 22 | FileCount 23 | 24 | $this.ScanFiles.Count 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Source/Private/AssertParameter.ps1: -------------------------------------------------------------------------------- 1 | function AssertParameter { 2 | <# 3 | .SYNOPSIS 4 | Simplifies custom error messages for ValidateScript 5 | 6 | .DESCRIPTION 7 | Windows PowerShell implementation of the ErrorMessage functionality available 8 | for ValidateScript in PowerShell core 9 | 10 | .EXAMPLE 11 | [ValidateScript({ Assert-Parameter -ScriptBlock {Test-Path $_} -ErrorMessage "Path not found." })] 12 | #> 13 | param( 14 | [Parameter(Position = 0)] 15 | [scriptblock] $ScriptBlock 16 | , 17 | [Parameter(Position = 1)] 18 | [string] $ErrorMessage = 'Failed parameter assertion' 19 | ) 20 | 21 | if (& $ScriptBlock) { 22 | $true 23 | } else { 24 | throw $ErrorMessage 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Source/Private/ConvertToHashtable.ps1: -------------------------------------------------------------------------------- 1 | function ConvertToHashtable { 2 | <# 3 | .SYNOPSIS 4 | Converts PowerShell object to hashtable 5 | 6 | .DESCRIPTION 7 | Converts PowerShell objects, including nested objets, arrays etc. to a hashtable 8 | 9 | .PARAMETER InputObject 10 | The object that you want to convert to a hashtable 11 | 12 | .EXAMPLE 13 | Get-Content -Raw -Path C:\Path\To\file.json | ConvertFrom-Json | ConvertTo-Hashtable 14 | 15 | .NOTES 16 | Based on function by Dave Wyatt found on Stack Overflow 17 | https://stackoverflow.com/questions/3740128/pscustomobject-to-hashtable 18 | #> 19 | param ( 20 | [Parameter(ValueFromPipeline)] 21 | $InputObject 22 | ) 23 | 24 | process { 25 | if ($null -eq $InputObject) { return $null } 26 | 27 | if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) { 28 | $collection = @( 29 | foreach ($object in $InputObject) { ConvertToHashtable -InputObject $object } 30 | ) 31 | 32 | Write-Output -NoEnumerate $collection 33 | } elseif ($InputObject -is [psobject]) { 34 | $hash = @{} 35 | 36 | foreach ($property in $InputObject.PSObject.Properties) { 37 | $hash[$property.Name] = ConvertToHashtable -InputObject $property.Value 38 | } 39 | 40 | $hash 41 | } else { 42 | $InputObject 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Source/Private/GetConfig.ps1: -------------------------------------------------------------------------------- 1 | function GetConfig { 2 | param ( 3 | $ConfigPath 4 | ) 5 | 6 | try { 7 | if ($PSVersionTable.PSEdition -eq 'Core') { 8 | $Config = Get-Content $ConfigPath -ErrorAction Stop | ConvertFrom-Json -AsHashtable 9 | } 10 | else { 11 | $Config = Get-Content $ConfigPath -ErrorAction Stop -Raw | ConvertFrom-Json | ConvertToHashtable 12 | } 13 | } 14 | catch { 15 | Throw "Failed to get config. $_" 16 | } 17 | 18 | $Config 19 | } -------------------------------------------------------------------------------- /Source/Private/GetExclusions.ps1: -------------------------------------------------------------------------------- 1 | function GetExclusions { 2 | param ( 3 | $Excludelist 4 | ) 5 | [string[]]$Exclusions = Get-Content $Excludelist | Where-Object {$_ -and $_ -notlike "#*"} 6 | 7 | [System.Collections.Generic.List[HashTable]]$ExcludeResults = @() 8 | 9 | foreach ($e in $Exclusions) { 10 | $eObj = ConvertFrom-Csv -InputObject $e -Delimiter ';' -Header 'Path', 'LineNumber', 'Line' 11 | 12 | # Normalize path 13 | $eObj.Path = $eObj.Path -replace '[\\\/]', [IO.Path]::DirectorySeparatorChar 14 | 15 | if ($eObj.Path -match '^\..*') { 16 | # Path starts with '.', is relative. Replace with root folder 17 | $BasePath = split-path (Resolve-Path $Excludelist).Path 18 | $eobj.Path = $eobj.Path -replace '^\.', $BasePath 19 | } 20 | 21 | if ([string]::IsNullOrEmpty($eObj.LineNumber) -and [string]::IsNullOrEmpty($eObj.Line)) { 22 | # Path or fileexclusion 23 | if ($eObj.Path -match '.*\\\*$') { 24 | # Full path excluded 25 | Get-ChildItem -Path $eObj.Path -Recurse -File -ErrorAction SilentlyContinue | ForEach-Object { 26 | $ExcludeResults.Add(@{ 27 | StringValue = $_.FullName 28 | Type = 'File' 29 | }) 30 | } 31 | } 32 | else { 33 | # Full filename excluded 34 | $ExcludeResults.Add(@{ 35 | StringValue = $eObj.Path 36 | Type = 'File' 37 | }) 38 | } 39 | } 40 | else { 41 | # File, line, and pattern excluded 42 | $ExcludeResults.Add(@{ 43 | StringValue = "$($eObj.Path);$($eObj.LineNumber);$($eObj.Line)" 44 | Type = 'LinePattern' 45 | }) 46 | } 47 | } 48 | 49 | $ExcludeResults 50 | } -------------------------------------------------------------------------------- /Source/Public/Find-Secret.ps1: -------------------------------------------------------------------------------- 1 | function Find-Secret { 2 | [CmdletBinding(DefaultParameterSetName = 'Path')] 3 | param ( 4 | [Parameter(ParameterSetName = 'Path', Position = 0)] 5 | [ValidateScript({ AssertParameter -ScriptBlock {Test-Path $_} -ErrorMessage "Path not found." })] 6 | [string[]]$Path = "$PWD", 7 | 8 | [Parameter(ParameterSetName = 'Path')] 9 | [string[]]$Filetype, 10 | 11 | [Parameter(ParameterSetName = 'Path')] 12 | [switch]$NoRecurse, 13 | 14 | [Parameter(ParameterSetName = 'File', Position = 0)] 15 | [ValidateScript({ AssertParameter -ScriptBlock {Test-Path $_} -ErrorMessage "File not found." })] 16 | [string]$File, 17 | 18 | [Parameter()] 19 | [string]$ConfigPath = $script:PSSSConfigPath, 20 | 21 | [Parameter()] 22 | [ValidateScript({ AssertParameter -ScriptBlock {Test-Path $_} -ErrorMessage "Excludelist path not found." })] 23 | [string]$Excludelist 24 | ) 25 | 26 | $Config = GetConfig -ConfigPath $ConfigPath 27 | 28 | [bool]$Recursive = -not $NoRecurse 29 | 30 | switch ($PSCmdLet.ParameterSetName) { 31 | 'Path' { 32 | if ( ($Path.Count -eq 1) -and ((Get-Item $Path[0]) -is [System.IO.FileInfo]) ) { 33 | [Array]$ScanFiles = Get-ChildItem $Path[0] -File 34 | } 35 | else { 36 | if ($Filetype -and $Filetype.Contains('*')) { 37 | [Array]$ScanFiles = Get-ChildItem $Path -File -Recurse:$Recursive 38 | } 39 | elseif ($Filetype) { 40 | $ScanExtensions = $Filetype | ForEach-Object { 41 | if (-not $_.StartsWith('.')) { 42 | ".$_" 43 | } 44 | else { 45 | $_ 46 | } 47 | } 48 | [Array]$ScanFiles = Get-ChildItem $Path -File -Recurse:$Recursive | Where-Object -Property Extension -in $ScanExtensions 49 | 50 | } 51 | else { 52 | [Array]$ScanFiles = Get-ChildItem $Path -File -Recurse:$Recursive | Where-Object -Property Extension -in $Config['fileextensions'] 53 | } 54 | } 55 | } 56 | 'File' { 57 | [Array]$ScanFiles = Get-ChildItem $File -File 58 | } 59 | } 60 | 61 | if (-not [string]::IsNullOrEmpty($Excludelist)) { 62 | # Remove the excludelist from scanfiles. Otherwise patternmatches will be found here... 63 | $ScanFiles = $ScanFiles.Where({ 64 | $_.FullName -ne (Resolve-Path $Excludelist).Path 65 | }) 66 | 67 | $Exclusions = GetExclusions $Excludelist 68 | $FileExclusions = $Exclusions.Where({$_.Type -eq 'File'}).StringValue 69 | $LinePatternExclusions = $Exclusions.Where({$_.Type -eq 'LinePattern'}).StringValue 70 | Write-Verbose "Using excludelist $Excludelist. Found $($Exclusions.Count) exlude strings." 71 | 72 | if ($FileExclusions.count -ge 1) { 73 | Write-Verbose "Excluding files from scan:`n$($FileExclusions -join ""`n"")" 74 | $ScanFiles = $ScanFiles.Where({ 75 | $_.FullName -notin $FileExclusions 76 | }) 77 | } 78 | } 79 | 80 | $scanStart = [DateTime]::Now 81 | 82 | if ($ScanFiles.Count -ge 1) { 83 | Write-Verbose "Scanning files:`n$($ScanFiles.FullName -join ""`n"")" 84 | 85 | $Res = foreach ($key in $Config['regexes'].Keys) { 86 | $RegexName = $key 87 | $Pattern = ($Config['regexes'])."$RegexName" 88 | 89 | Write-Verbose "Performing $RegexName scan`nPattern '$Pattern'`n" 90 | 91 | $ScanFiles | 92 | Select-String -Pattern $Pattern | 93 | Add-Member NoteProperty PatternName ( 94 | $key -replace '_', ' ' -replace '^\s{0,}' 95 | ) -Force -PassThru | 96 | & { process { 97 | $_.pstypenames.clear() 98 | $_.pstypenames.add('PSSecretScanner.Result') 99 | $_ 100 | } } 101 | } 102 | 103 | if (-not [string]::IsNullOrEmpty($Excludelist)) { 104 | if ($LinePatternExclusions.count -ge 1) { 105 | $Res = $Res | Where-Object { 106 | "$($_.Path);$($_.LineNumber);$($_.Line)" -notin $LinePatternExclusions 107 | } 108 | } 109 | } 110 | 111 | $resultSet = [Ordered]@{ 112 | Results = $res 113 | ScanFiles = $ScanFiles 114 | ScanStart = $scanStart 115 | } 116 | } 117 | else { 118 | $resultSet = [Ordered]@{ 119 | Results = @() 120 | ScanFiles = @() 121 | ScanStart = $scanStart 122 | } 123 | } 124 | 125 | 126 | $scanEnd = [DateTime]::Now 127 | $scanTook = $scanEnd - $scanStart 128 | 129 | $resultSet.Add('PSTypeName','PSSecretScanner.ResultSet') 130 | $resultSet.Add('ScanEnd', $scanEnd) 131 | $resultSet.Add('ScanTimespan', $scanTook) 132 | 133 | $result = [PSCustomObject]$resultSet 134 | 135 | $Result 136 | } 137 | -------------------------------------------------------------------------------- /Source/Public/New-PSSSConfig.ps1: -------------------------------------------------------------------------------- 1 | function New-PSSSConfig { 2 | param ( 3 | [Parameter(Mandatory)] 4 | [string]$Path 5 | ) 6 | 7 | $ConfigFileName = Split-Path $script:PSSSConfigPath -leaf 8 | 9 | $InvokeSplat = @{ 10 | Path = $script:PSSSConfigPath 11 | Destination = $Path 12 | } 13 | 14 | if (Test-Path (Join-Path -Path $Path -ChildPath $ConfigFileName)) { 15 | Write-Warning 'Config file already exists!' 16 | $InvokeSplat.Add('Confirm',$true) 17 | } 18 | 19 | Copy-Item @InvokeSplat 20 | } 21 | -------------------------------------------------------------------------------- /Source/Public/Write-SecretStatus.ps1: -------------------------------------------------------------------------------- 1 | function Write-SecretStatus { 2 | param () 3 | 4 | try { 5 | [array]$IsGit = (git status *>&1).ToString() 6 | if ( $IsGit[0] -eq 'fatal: not a git repository (or any of the parent directories): .git' ) { 7 | break 8 | } 9 | else { 10 | $FindSplat = @{ 11 | NoRecurse = $true 12 | } 13 | 14 | $ExcludePath = Join-Path -Path (git rev-parse --show-toplevel) -ChildPath '.ignoresecrets' 15 | if (Test-Path $ExcludePath) { 16 | $FindSplat.Add('Excludelist',$ExcludePath) 17 | } 18 | 19 | $Secrets = Find-Secret @FindSplat 20 | $SecretsCount = $Secrets.Count 21 | 22 | if ((Get-Command Prompt).ModuleName -eq 'posh-git') { 23 | if ($SecretsCount -ge 1) { 24 | $GitPromptSettings.DefaultPromptBeforeSuffix.ForegroundColor = 'Red' 25 | } 26 | else { 27 | $GitPromptSettings.DefaultPromptBeforeSuffix.ForegroundColor = 'LightBlue' 28 | } 29 | } 30 | 31 | Write-Output "[$SecretsCount]" 32 | } 33 | } 34 | catch {} 35 | } -------------------------------------------------------------------------------- /Source/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "regexes":[ 3 | {"_Private_Key":"[-]{5}BEGIN\\s(?:[DR]SA|OPENSSH|EC|PGP)\\sPRIVATE\\sKEY(?:\\sBLOCK)?[-]{5}"}, 4 | {"_AWS_Key":"[\\s'\"=]A[KS]IA[0-9A-Z]{16}[\\s'\"]"}, 5 | {"_AWS_Key_line_end":"[\\s=]A[KS]IA[0-9A-Z]{16}$"}, 6 | {"_Slack_token":"xox[pboa]-[0-9]{11,12}-[0-9]{12}-[0-9]{12}-[a-z0-9]{32}"}, 7 | {"_Basic_Auth":"Authorization:\\sBasic\\s(?:[a-zA-Z0-9\\+/]{4})*(?:[a-zA-Z0-9\\+/]{3}=|[a-zA-Z0-9\\+/]{2}==)?(?:$|[\\s;'\"])"}, 8 | {"_Basic_Auth_Only_Pattern":"Basic\\s(?:[a-zA-Z0-9\\+/]{4})*(?:[a-zA-Z0-9\\+/]{3}=|[a-zA-Z0-9\\+/]{2}==)?(?:$|[\\s;'\"])"}, 9 | {"_.npmrc_auth":"^\\s{0,20}_auth\\s{0,20}="}, 10 | {"_Connection_String":"[0-9a-z-]{3,30}(?%#*&_^-]{6,45}@[0-9a-z-.]{1,50}(?:[\\s;'\",:]|$)"}, 11 | {"_JDBC_Connection_Creds":"jdbc:oracle:thin:[0-9a-z-]{3,30}/[a-z0-9!?$)%@#*&_^-]+@"}, 12 | {"_Keys":"(?:(?:a(?:ws|ccess|p(?:i|p(?:lication)?)))|private|se(?:nsitive|cret))[\\s_-]?key\\s{1,20}[=:]{1,2}\\s{0,20}['\"]?(?=[a-z0-9!?$)=<\/>%@#*&{}_^-]{0,1200}[^\\sa-z;'\",\/.(_-][a-z0-9!?$)=<\/>%@#*&{}_^-]{0,1200})(?![$][a-z_-]{1,60}[\\s;'\",])(?![#$]?{[^'\"]{1,60}})[a-z0-9!?$)=<\/>%@#*&{}_^-]{8,1200}(?:[\\s;'\",]|$)"}, 13 | {"_Keys_no_space":"(?:(?:a(?:ws|ccess|p(?:i|p(?:lication)?)))|private|se(?:nsitive|cret))[\\s_-]?key[=:]{1,2}\\s{0,20}['\"]?(?=[a-z0-9!?$)=<\/>%@#*&{}_^-]{0,1200}[^\\sa-z;'\",\/.(_-][a-z0-9!?$)=<\/>%@#*&{}_^-]{0,1200})(?![$][a-z_-]{1,60}[\\s;'\",])(?![#$]?{[^'\"]{1,60}})[a-z0-9!?$)=<\/>%@#*&{}_^-]{8,1200}(?:[\\s;'\",]|$)"}, 14 | {"_Password_Generic_with_quotes":"(?:(?:pass(?:w(?:or)?d)?)|(?:p(?:s)?w(?:r)?d)|secret)['\"]?\\s{0,20}[=:]{1,3}\\s{0,20}[@]?['\"](?=[a-z0-9!?$)=<\/>%@#*&{}_^-]{0,45}[^\\sa-z;'\",\/.(_-][a-z0-9!?$)=<\/>%@#*&{}_^-]{0,45})(?![$][a-z_-]{1,45}[\\s;'\",])(?![#$]?{[^'\"]{1,60}})[a-z0-9!?$)=<\/>%@#*&{}_^-]{6,45}['\"]"}, 15 | {"_Password_equal_no_quotes":"(?:(?:pass(?:w(?:or)?d)?)|(?:p(?:s)?w(?:r)?d)|secret)\\s{0,20}[=]\\s{0,20}(?=[a-z0-9!?$)=<\/>%@#*&{}_^-]{0,45}[^\\sa-z;'\",\/.(_-][a-z0-9!?$)=<\/>%@#*&{}_^-]{0,45})(?![$#]?{[^\\s;'\",]{1,60}})(?![$][a-z_-]{1,45}(?:(?:%@#*&{}_^-]{6,45}(?:(?:%@#*&{}_^-]{0,45}[^\\sa-z;'\",\/.(_-][a-z0-9!?$)=<\/>%@#*&{}_^-]{0,45})(?![$][a-z_-]{1,45}[\\s;'\",])(?![#$]?{[^'\"]{1,60}})[a-z0-9!?$)=<\/>%@#*&{}_^-]{6,45}['\"]"}, 17 | {"_Password_primary":"(?:(?:pass(?:w(?:or)?d)?)|(?:p(?:s)?w(?:r)?d)|secret)\\sprimary[=]['\"](?=[a-z0-9!?$)=<\/>%@#*&{}_^-]{0,45}[^\\sa-z;'\",\/.(_-][a-z0-9!?$)=<\/>%@#*&{}_^-]{0,45})(?![$][a-z_-]{1,45}[\\s;'\",])(?![#$]?{[^'\"]{1,60}})[a-z0-9!?$)=<\/>%@#*&{}_^-]{6,45}['\"]"}, 18 | {"_Password_set":"(?:(?:pass(?:w(?:or)?d)?)|(?:p(?:s)?w(?:r)?d)|secret)[(]['\"](?=[a-z0-9!?$)=<\/>%@#*&{}_^-]{0,45}[^\\sa-z;'\",\/.(_-][a-z0-9!?$)=<\/>%@#*&{}_^-]{0,45})(?![$][a-z_-]{1,45}[\\s;'\",])(?![#$]?{[^'\"]{1,60}})[a-z0-9!?$)=<\/>%@#*&{}_^-]{6,45}['\"][)][;]"}, 19 | {"_Password_admin":"(?:(?:pass(?:w(?:or)?d)?)|(?:p(?:s)?w(?:r)?d)|secret)['\"]?\\s{0,20}[=:]{1,3}\\s{0,20}['\"]?admin[\\s;'\",]"}, 20 | {"_Password_String":"(?:(?:pass(?:w(?:or)?d)?)|(?:p(?:s)?w(?:r)?d)|secret)\\s{0,20}[:]{1,3}\\s{1,20}string\\s{1,20}['\"](?=[a-z0-9!?$)=<\/>%@#*&{}_^-]{0,45}[^\\sa-z;'\",\/.(_-][a-z0-9!?$)=<\/>%@#*&{}_^-]{0,45})(?![$][a-z_-]{1,45}[\\s;'\",])(?![#$]?{[^'\"]{1,60}})[a-z0-9!?$)=<\/>%@#*&{}_^-]{6,45}['\"]"}, 21 | {"_Password_XML":"<(?:(?:pass(?:w(?:or)?d)?)|(?:p(?:s)?w(?:r)?d)|secret)>(?![$#]{.{1,100}})(?!%%.{1,100}%%).{5,1200}"}, 22 | {"_Password_colon_no_quotes":"(?:(?:pass(?:w(?:or)?d)?)|(?:p(?:s)?w(?:r)?d)|secret)\\s{0,20}(?!:=):\\s{0,20}(?=[a-z0-9!?$)=<\/>%@#*&{}_^-]{0,45}[^\\sa-z;'\",\/.)(_-][a-z0-9!?$)=<\/>%@#*&{}_^-]{0,45})(?![$#]?{[^\\s,;]{1,60}})(?![$][a-z_-]{1,45}(?:[\\s,;]|$))[a-z0-9!?$)=<\/>%@#*&{}_^-]{6,45}(?:[\\s,;]|$)"}, 23 | {"_GitHub_Token":"github.{0,20}['\"][a-z0-9]{35,40}['\"]"}, 24 | {"_Facebook_Token":"facebook.{0,30}['\"][0-9a-f]{32,255}['\"]"}, 25 | {"_Twitter_Token":"twitter.{0,30}['\"][a-z0-9]{35,44}['\"]"}, 26 | {"_Heroku_Key":"heroku.{0,30}[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"}, 27 | {"_Azure_AccountKey":"AccountKey\\s+=\\s+[a-zA-Z0-9+\/=]{88}"}, 28 | {"_PWSH_Personal_Acces_Token_PAT":"\\$pat\\s*=\\s*['\"][a-z0-9]{52}['\"]"}, 29 | {"_Personal_Acces_Token_PAT":"(\\s|^)[a-z0-9]{52}(\\s|$)"}, 30 | {"_Cloudinary":"cloudinary://.*"}, 31 | {"_Firebase_URL":"firebaseio\\.com"}, 32 | {"_Amazon_MWS_Auth_Token":"amzn\\.mws\\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"}, 33 | {"_Facebook_Access_Token":"EAACEdEose0cBA[0-9A-Za-z]+"}, 34 | {"_Facebook_OAuth":"(?i)facebook(?-i).*['|\"][0-9a-f]{32}['|\"]"}, 35 | {"_GitHub":"(?i)github.*['|\"][0-9a-z]{35,40}['|\"]"}, 36 | {"_Generic_API_Key":"(?i)api_?key.*['|\"][0-9a-z]{32,45}['|\"]"}, 37 | {"_Generic_Secret":"(?i)secret.*['|\"][0-9a-z]{32,45}['|\"]"}, 38 | {"_Google_API_Key":"AIza[\\w\\-]{35}"}, 39 | {"_Google_Cloud_Platform_OAuth":"[0-9]+-\\w{32}\\.apps\\.googleusercontent\\.com"}, 40 | {"_Google_GCP_Service_account":"\"type\":\"service_account\""}, 41 | {"_Google_OAuth_Access_Token":"ya29\\.[\\w\\-]+"}, 42 | {"_Heroku_API_Key":"(?i)heroku(?-i).*[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}"}, 43 | {"_MailChimp_API_Key":"[0-9a-f]{32}-us[0-9]{1,2}"}, 44 | {"_Mailgun_API_Key":"key-[0-9a-zA-Z]{32}"}, 45 | {"_Password_in_URL":"[a-zA-Z]{3,10}://[^$][^/\\s:@]{3,20}:[^$][^/\\s:@]{3,20}@.{1,100}[\"'\\s]"}, 46 | {"_PayPal_Braintree_Access_Token":"access_token\\$production\\$[0-9a-z]{16}\\$[0-9a-f]{32}"}, 47 | {"_Picatic_API_Key":"sk_live_[0-9a-z]{32}"}, 48 | {"_Slack_Webhook":"https://hooks.slack.com/services/T\\w{8}/B\\w{8}/\\w{24}"}, 49 | {"_Stripe_API_Key":"sk_live_[0-9a-zA-Z]{24}"}, 50 | {"_Stripe_Restricted_API_Key":"rk_live_[0-9a-zA-Z]{24}"}, 51 | {"_Square_Access_Token":"sq0atp-[\\w\\-]{22}"}, 52 | {"_Square_OAuth_Secret":"sq0csp-[\\w\\-]{43}"}, 53 | {"_Twilio_API_Key":"SK[0-9a-fA-F]{32}"}, 54 | {"_Twitter_Access_Token":"(?i)twitter.*[1-9][0-9]+-[0-9a-z]{40}"}, 55 | {"_Twitter_OAuth":"(?i)twitter.*['|\"][0-9a-z]{35,44}['|\"]"} 56 | ], 57 | "fileextensions":[ 58 | ".ps1", 59 | ".psm1", 60 | ".psd1", 61 | ".md", 62 | ".txt", 63 | ".xml", 64 | ".csv", 65 | ".bicep", 66 | ".json", 67 | "" 68 | ] 69 | } 70 | 71 | 72 | -------------------------------------------------------------------------------- /Tests/RegexPatternTests/RegexPattern.Tests.ps1: -------------------------------------------------------------------------------- 1 | # OWASP test cases is in the format 2 | # +pattern to search in>>should match 3 | # fail - should be found by scanner 4 | # pass - should not be found by scanner 5 | 6 | Remove-Module PSSecretScanner -Force -ErrorAction SilentlyContinue 7 | Import-Module $PSScriptRoot\..\..\Source\PSSecretScanner -Force 8 | 9 | $TestCasesFile = (Resolve-Path $PSScriptRoot\TestCases.json).Path 10 | $TestCases = Get-Content $TestCasesFile | ConvertFrom-Json 11 | $ShouldFindPatterns = $TestCases | Where-Object -Property ShouldMatch -EQ $True 12 | $ShouldNotFindPatterns = $TestCases | Where-Object -Property ShouldMatch -EQ $False 13 | 14 | Describe 'Pattern verification tests' { 15 | BeforeEach { 16 | if (Test-Path TestDrive:\MatchFile.txt) { 17 | Remove-Item TestDrive:\MatchFile.txt 18 | } 19 | } 20 | Context 'Should find patterns' -Tag 'match' { 21 | It 'Should find pattern <_.Pattern>' -TestCases $ShouldFindPatterns { 22 | $_.Pattern | Out-File TestDrive:\MatchFile.txt 23 | $r = Find-Secret -Path TestDrive:\MatchFile.txt 24 | $r.count | Should -BeGreaterOrEqual 1 25 | } 26 | } 27 | Context 'Should not find patterns' -Tag 'notmatch' { 28 | It 'Should not find pattern <_.Pattern>' -TestCases $ShouldNotFindPatterns { 29 | $_.Pattern | Out-File TestDrive:\MatchFile.txt 30 | $r = Find-Secret -Path TestDrive:\MatchFile.txt 31 | $r.count | Should -Be 0 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /Tests/UnitTests/AssertParameter.Tests.ps1: -------------------------------------------------------------------------------- 1 | Remove-Module PSSecretScanner -Force -ErrorAction SilentlyContinue 2 | Import-Module $PSScriptRoot\..\..\Source\PSSecretScanner -Force 3 | 4 | InModuleScope -ModuleName PSSecretScanner { 5 | Describe 'AssertParameter' { 6 | It 'Should not throw when scriptblock is successful' { 7 | {AssertParameter -ScriptBlock {$true} -ErrorMessage 'error!' }| Should -Not -Throw 8 | } 9 | 10 | It 'Should throw when scriptblock is not successful' { 11 | {AssertParameter -ScriptBlock {$false} -ErrorMessage 'error!'} | Should -Throw 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /Tests/UnitTests/ConvertToHashtable.Tests.ps1: -------------------------------------------------------------------------------- 1 | Remove-Module PSSecretScanner -Force -ErrorAction SilentlyContinue 2 | Import-Module $PSScriptRoot\..\..\Source\PSSecretScanner -Force 3 | 4 | InModuleScope -ModuleName PSSecretScanner { 5 | Describe 'ConvertToHashtable' { 6 | It 'Should convert PSObject to hashtable the way we currently use it' { 7 | $Config = @' 8 | { 9 | "regexes":[ 10 | {"_Private_Key":"[-]{5}BEGIN\\s(?:[DR]SA|OPENSSH|EC|PGP)\\sPRIVATE\\sKEY(?:\\sBLOCK)?[-]{5}"}, 11 | ], 12 | "fileextensions":[ 13 | ".ps1", 14 | ] 15 | } 16 | '@ 17 | $r = $Config | ConvertFrom-Json | ConvertToHashtable 18 | $r | Should -BeOfType [hashtable] 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /Tests/UnitTests/Find-Secret.Tests.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 7 2 | 3 | Remove-Module PSSecretScanner -Force -ErrorAction SilentlyContinue 4 | Import-Module $PSScriptRoot\..\..\Source\PSSecretScanner -Force 5 | 6 | Describe 'Find-Secret' { 7 | Context 'Basic design tests' { 8 | It 'Should have non mandatory parameter <_>' -TestCases 'Path', 'ConfigPath', 'Excludelist', 'Filetype', 'File', 'NoRecurse' { 9 | Get-Command Find-Secret | Should -HaveParameter $_ -Because 'If parameters change behaviour we need to do a major bump' 10 | } 11 | 12 | It 'Path should be the default parameterset' { 13 | $r = (Get-Command Find-Secret).ParameterSets.Where({$_.IsDefault}) 14 | $r.Name | Should -Be 'Path' 15 | } 16 | } 17 | 18 | Context 'Version 2.0 - Output object' { 19 | BeforeAll { 20 | $TestFile = 'TestDrive:\TestFile.ps1' 21 | 22 | # Create a test file 23 | 'pat1' | Out-File -FilePath $TestFile -Force 24 | 25 | # Mock for parameter validation. Tested in separate test file 26 | Mock -CommandName AssertParameter -ModuleName PSSecretScanner -MockWith { 27 | return $true 28 | } 29 | 30 | # Mock to always return one file to scan 31 | Mock -CommandName Get-ChildItem -ModuleName PSSecretScanner -MockWith { 32 | @{ 33 | FullName = $TestFile 34 | Extension = '.ps1' 35 | } 36 | } -ParameterFilter {$Path -and $File -and $Recurse} 37 | 38 | # Mock GetConfig - wrapper function to make Find-Secret testable 39 | Mock -CommandName GetConfig -ModuleName PSSecretScanner -MockWith { 40 | return '{"regexes":[{"_Pattern1":"pat1"},{"_Pattern2":"pat2"}],"fileextensions":[".ps1",".ps2"]}' | ConvertFrom-Json -AsHashtable 41 | } 42 | } 43 | 44 | It 'Output object should be of type "PSCustomObject"' { 45 | $r = Find-Secret $TestFile 46 | $r | Should -BeOfType PSCustomObject 47 | } 48 | 49 | It 'Output object typenames should be "PSSecretScanner.ResultSet"' { 50 | $r = Find-Secret $TestFile 51 | 'PSSecretScanner.ResultSet' | Should -BeIn $r.pstypenames 52 | } 53 | 54 | It 'Each result should be of type "MatchInfo"' { 55 | $r = Find-Secret $TestFile 56 | $r.Results[0] | Should -BeOfType Microsoft.PowerShell.Commands.MatchInfo 57 | } 58 | 59 | It 'Each result object typename should be "PSSecretScanner.Result"' { 60 | $r = Find-Secret $TestFile 61 | 'PSSecretScanner.Result' | Should -BeIn $r.Results[0].pstypenames 62 | } 63 | 64 | It 'Result should always contain property <_> - no scanresults' -TestCases 'Results', 'ScanEnd', 'ScanFiles', 'ScanStart', 'ScanTimespan', 'Count', 'FailedFailCount', 'FileCount' { 65 | $r = Find-Secret 66 | $r.Results.Count | Should -be 0 67 | $_ | Should -BeIn ($r | Get-Member).Name 68 | } 69 | 70 | It 'Result should always contain property <_> - one scanresult' -TestCases 'Results', 'ScanEnd', 'ScanFiles', 'ScanStart', 'ScanTimespan', 'Count', 'FailedFailCount', 'FileCount' { 71 | $r = Find-Secret $TestFile 72 | $r.Results.Count | Should -be 1 73 | $_ | Should -BeIn ($r | Get-Member).Name 74 | } 75 | 76 | It 'FileCount should be the amount of files where secrets are found' { 77 | $r = Find-Secret $TestFile 78 | $r.FileCount | Should -be 1 79 | } 80 | } 81 | 82 | Context 'Functionality - Exclusion list' { 83 | BeforeAll { 84 | $TestFile = 'TestDrive:\TestFile.ps1' 85 | $ScanFolder = Split-Path $TestFile 86 | 87 | # Create a test file 88 | 'pat1' | Out-File -FilePath $TestFile -Force 89 | 90 | # Mock for parameter validation. Tested in separate test file 91 | Mock -CommandName AssertParameter -ModuleName PSSecretScanner -MockWith { 92 | return $true 93 | } 94 | 95 | # Mock GetConfig - wrapper function to make Find-Secret testable 96 | Mock -CommandName GetConfig -ModuleName PSSecretScanner -MockWith { 97 | return '{"regexes":[{"_Pattern1":"pat1"},{"_Pattern2":"pat2"}],"fileextensions":[".ps1",".ps2"]}' | ConvertFrom-Json -AsHashtable 98 | } 99 | } 100 | 101 | It 'If an exclusion list is given it should excluse matches - zero results' { 102 | Mock -CommandName GetExclusions -ModuleName PSSecretScanner -MockWith { 103 | $p = $((resolve-path TestDrive:\TestFile.ps1).ProviderPath).Replace('\','\\') 104 | "[{""Type"": ""LinePattern"",""StringValue"": ""$p;1;pat1""}]" | ConvertFrom-Json 105 | } 106 | $r = Find-Secret $ScanFolder -Excludelist 'TestDrive:\TestFile.ps1' 107 | $r.results.count | Should -Be 0 108 | } 109 | 110 | It 'If an exclusion list is given it should excluse matches - one result' { 111 | "pat1`npat2" | Out-File -FilePath $TestFile -Force 112 | Mock -CommandName GetExclusions -ModuleName PSSecretScanner -MockWith { 113 | $p = $((resolve-path TestDrive:\TestFile.ps1).ProviderPath).Replace('\','\\') 114 | "[{""Type"": ""LinePattern"",""StringValue"": ""$p;1;pat1""}]" | ConvertFrom-Json 115 | } 116 | $r = Find-Secret $ScanFolder -Excludelist 'TestDrive:\TestFile.ps1' 117 | $r.results.count | Should -Be 1 118 | } 119 | } 120 | 121 | Context 'Functionality - ParameterSet "File"' { 122 | BeforeAll { 123 | $TestFile = 'TestDrive:\TestFile.ps1' 124 | 125 | # Create a test file 126 | 'pat1' | Out-File -FilePath $TestFile -Force 127 | 128 | # Mock for parameter validation. Tested in separate test file 129 | Mock -CommandName AssertParameter -ModuleName PSSecretScanner -MockWith { 130 | return $true 131 | } 132 | 133 | # Mock to always return one file to scan 134 | Mock -CommandName Get-ChildItem -ModuleName PSSecretScanner -MockWith { 135 | @{ 136 | FullName = $TestFile 137 | Extension = '.ps1' 138 | } 139 | } -ParameterFilter {$Path -and $File -and $Recurse} 140 | 141 | # Mock GetConfig - wrapper function to make Find-Secret testable 142 | Mock -CommandName GetConfig -ModuleName PSSecretScanner -MockWith { 143 | return '{"regexes":[{"_Pattern1":"pat1"},{"_Pattern2":"pat2"}],"fileextensions":[".ps1",".ps2"]}' | ConvertFrom-Json -AsHashtable 144 | } 145 | } 146 | 147 | It 'If given one single file it should scan that file' { 148 | $r = Find-Secret -File $TestFile 149 | $r.ScanFiles.count | Should -Be 1 150 | } 151 | } 152 | 153 | Context 'Functionality - ParameterSet "Path"' { 154 | BeforeEach { 155 | # Start every test by cleaning up and recreating test environment 156 | 157 | Get-ChildItem TestDrive:\ | Remove-Item -Recurse 158 | $TestFile = 'TestDrive:\TestFile.ps1' 159 | $ScanFolder = Split-Path $TestFile 160 | 161 | # Create a test file 162 | 'pat1' | Out-File -FilePath $TestFile -Force 163 | } 164 | 165 | BeforeAll { 166 | # Mock for parameter validation. Tested in separate test file 167 | Mock -CommandName AssertParameter -ModuleName PSSecretScanner -MockWith { 168 | return $true 169 | } 170 | 171 | # Mock to always return one file to scan 172 | Mock -CommandName Get-ChildItem -ModuleName PSSecretScanner -MockWith { 173 | @{ 174 | FullName = $TestFile 175 | Extension = '.ps1' 176 | } 177 | } -ParameterFilter {$Path -and $Path -eq $TestFile -and $File -and $Recurse} 178 | 179 | # Mock GetConfig - wrapper function to make Find-Secret testable 180 | Mock -CommandName GetConfig -ModuleName PSSecretScanner -MockWith { 181 | return '{"regexes":[{"_Pattern1":"pat1"},{"_Pattern2":"pat2"}],"fileextensions":[".ps1",".ps2"]}' | ConvertFrom-Json -AsHashtable 182 | } 183 | } 184 | 185 | It 'Given a folder it should scan that folder - Using positional paramneter' { 186 | $r = Find-Secret $ScanFolder 187 | $r.ScanFiles.count | Should -Be 1 188 | } 189 | 190 | It 'Given a folder it should scan that folder - Not using positional paramneter' { 191 | $r = Find-Secret -Path $ScanFolder 192 | $r.ScanFiles.count | Should -Be 1 193 | } 194 | 195 | It 'Given a folder and a file it should scan both' { 196 | New-Item 'TestDrive:\Folder1\' -ItemType Directory 197 | New-Item 'TestDrive:\Folder2\' -ItemType Directory 198 | 'pat1' | Out-File -FilePath TestDrive:\Folder1\file1.ps1 -Force 199 | 'pat1' | Out-File -FilePath TestDrive:\Folder2\file2.ps1 -Force 200 | 201 | $r = Find-Secret 'TestDrive:\Folder1','TestDrive:\Folder2\file2.ps1' 202 | $r.ScanFiles.count | Should -Be 2 203 | } 204 | 205 | It 'Given only a file it should work as expected' { 206 | $r = Find-Secret 'TestDrive:\TestFile.ps1' 207 | $r.ScanFiles.count | Should -Be 1 208 | } 209 | 210 | It 'If NoRecurse is set we should only scan root dir' { 211 | # Make sure we have a subfolder tree with at least three matching files. 212 | New-Item 'TestDrive:\Folder1\' -ItemType Directory 213 | New-Item 'TestDrive:\Folder2\' -ItemType Directory 214 | 'pat1' | Out-File -FilePath TestDrive:\Folder1\file1.ps1 -Force 215 | 'pat1' | Out-File -FilePath TestDrive:\Folder2\file2.ps1 -Force 216 | 217 | $r = Find-Secret $ScanFolder -NoRecurse 218 | $r.ScanFiles.count | Should -Be 1 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /Tests/UnitTests/New-PSSSConfig.Tests.ps1: -------------------------------------------------------------------------------- 1 | Remove-Module PSSecretScanner -Force -ErrorAction SilentlyContinue 2 | Import-Module $PSScriptRoot\..\..\Source\PSSecretScanner -Force 3 | 4 | Describe 'New-PSSSConfig' { 5 | Context 'Copying new config file' { 6 | BeforeEach { 7 | if (Test-Path TestDrive:\config.json) { 8 | Remove-Item TestDrive:\config.json -Force 9 | } 10 | } 11 | 12 | It 'Should copy a new config.json to the patch given.' { 13 | New-PSSSConfig -Path TestDrive:\config.json 14 | Test-Path TestDrive:\config.json | Should -Be $true 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Types/PSSecretScanner.ResultSet/get_Count.ps1: -------------------------------------------------------------------------------- 1 | $this.Results.Count 2 | -------------------------------------------------------------------------------- /Types/PSSecretScanner.ResultSet/get_FailedFailCount.ps1: -------------------------------------------------------------------------------- 1 | @($this.Results | Select-Object -ExpandProperty Path -Unique).Length 2 | -------------------------------------------------------------------------------- /Types/PSSecretScanner.ResultSet/get_FileCount.ps1: -------------------------------------------------------------------------------- 1 | $this.ScanFiles.Count 2 | -------------------------------------------------------------------------------- /action.yaml: -------------------------------------------------------------------------------- 1 | name: PSSecretScanner 2 | description: Scan for secrets in your source files. 3 | 4 | branding: 5 | icon: lock 6 | color: red 7 | 8 | runs: 9 | using: "composite" 10 | steps: 11 | - name: Scan for secrets 12 | id: Scan 13 | shell: pwsh 14 | run: | 15 | $PSD1Found = Get-ChildItem -Recurse -Filter "*.psd1" | Where-Object Name -eq 'PSSecretScanner.psd1' | Select-Object -First 1 16 | 17 | if ($PSD1Found) { 18 | $PSSecretScannerModulePath = $PSD1Found 19 | Import-Module $PSD1Found -Force -PassThru | Out-Host 20 | } 21 | elseif ($env:GITHUB_ACTION_PATH) { 22 | $PSSecretScannerModulePath = Join-Path $env:GITHUB_ACTION_PATH 'Source/PSSecretScanner.psd1' 23 | if (Test-path $PSSecretScannerModulePath) { 24 | Import-Module $PSSecretScannerModulePath -Force -PassThru | Out-Host 25 | } 26 | else { 27 | throw "PSSecretScanner not found" 28 | } 29 | } 30 | else { 31 | try { 32 | Import-Module PSSecretScanner 33 | } 34 | catch { 35 | throw 'Cant find PSSecretScanner module.' 36 | } 37 | } 38 | 39 | $FindSplat = @{} 40 | 41 | $ExcludePath = Join-Path -Path (git rev-parse --show-toplevel) -ChildPath '.ignoresecrets' 42 | if (Test-Path $ExcludePath) { 43 | $FindSplat.Add('Excludelist',$ExcludePath) 44 | } 45 | 46 | $Secrets = Find-Secret @FindSplat 47 | 48 | Write-Output "::group::Scanned files" 49 | Write-Output "Scanned $($Secrets.ScanFiles.Count) files" 50 | Write-Output $Secrets.ScanFiles.FullName 51 | Write-Output "::endgroup::" 52 | 53 | if ($Secrets.Count -ne 0) { 54 | foreach ($r in $Secrets.Results) { 55 | Write-Output "::error title=$($r.PatternName), file=$($r.Path), line=$($r.LineNumber)::$($r.Line)" 56 | } 57 | Write-Error "Found secrets!" 58 | } 59 | else { 60 | Write-Output "::group::Scan results" 61 | Write-Output "::notice::No secrets found. #ShareCodeNotSecrets" 62 | Write-Output "::endgroup::" 63 | } 64 | -------------------------------------------------------------------------------- /images/PSSecretScanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bjompen/PSSecretScanner/c499dc3c982e4a7da4268964e561c1fc714afaac/images/PSSecretScanner.png -------------------------------------------------------------------------------- /images/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bjompen/PSSecretScanner/c499dc3c982e4a7da4268964e561c1fc714afaac/images/output.png --------------------------------------------------------------------------------