mirror of
https://github.com/omegaleo/SecretSecurity.git
synced 2026-11-09 01:53:31 +00:00
Initial implementation
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
github: omegaleo
|
||||||
|
ko_fi: omegaleo
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
name: Build and Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: '10.0.x'
|
||||||
|
|
||||||
|
- name: Extract version from tag
|
||||||
|
id: version
|
||||||
|
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: dotnet build SecretSecurity/SecretSecurity.csproj --configuration Release
|
||||||
|
|
||||||
|
- name: Pack NuGet tool
|
||||||
|
run: >
|
||||||
|
dotnet pack SecretSecurity/SecretSecurity.csproj
|
||||||
|
--configuration Release
|
||||||
|
--no-build
|
||||||
|
-p:Version=${{ steps.version.outputs.VERSION }}
|
||||||
|
--output ./nupkg
|
||||||
|
|
||||||
|
- name: Push to GitHub Packages
|
||||||
|
run: >
|
||||||
|
dotnet nuget push ./nupkg/*.nupkg
|
||||||
|
--api-key ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
--source https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: ./nupkg/*.nupkg
|
||||||
|
generate_release_notes: true
|
||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
|
||||||
|
[Dd]ebug/
|
||||||
|
[Rr]elease/
|
||||||
|
x64/
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
|
||||||
|
# MSTest test Results
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
[Bb]uild[Ll]og.*
|
||||||
|
|
||||||
|
*_i.c
|
||||||
|
*_p.c
|
||||||
|
*_i.h
|
||||||
|
*.ilk
|
||||||
|
*.meta
|
||||||
|
*.obj
|
||||||
|
*.pch
|
||||||
|
*.pdb
|
||||||
|
*.pgc
|
||||||
|
*.pgd
|
||||||
|
*.rsp
|
||||||
|
*.sbr
|
||||||
|
*.tlb
|
||||||
|
*.tli
|
||||||
|
*.tlh
|
||||||
|
*.tmp
|
||||||
|
*.tmp_proj
|
||||||
|
*.log
|
||||||
|
*.vspscc
|
||||||
|
*.vssscc
|
||||||
|
.builds
|
||||||
|
*.pidb
|
||||||
|
*.log
|
||||||
|
*.svclog
|
||||||
|
*.scc
|
||||||
|
|
||||||
|
# Visual C++ cache files
|
||||||
|
ipch/
|
||||||
|
*.aps
|
||||||
|
*.ncb
|
||||||
|
*.opensdf
|
||||||
|
*.sdf
|
||||||
|
*.cachefile
|
||||||
|
|
||||||
|
# Visual Studio profiler
|
||||||
|
*.psess
|
||||||
|
*.vsp
|
||||||
|
*.vspx
|
||||||
|
|
||||||
|
# Guidance Automation Toolkit
|
||||||
|
*.gpState
|
||||||
|
|
||||||
|
# ReSharper is a .NET coding add-in
|
||||||
|
_ReSharper*/
|
||||||
|
*.[Rr]e[Ss]harper
|
||||||
|
*.DotSettings.user
|
||||||
|
|
||||||
|
# Click-Once directory
|
||||||
|
publish/
|
||||||
|
|
||||||
|
# Publish Web Output
|
||||||
|
*.Publish.xml
|
||||||
|
*.pubxml
|
||||||
|
*.azurePubxml
|
||||||
|
|
||||||
|
# NuGet Packages Directory
|
||||||
|
## TODO: If you have NuGet Package Restore enabled, uncomment the next line
|
||||||
|
packages/
|
||||||
|
## TODO: If the tool you use requires repositories.config, also uncomment the next line
|
||||||
|
!packages/repositories.config
|
||||||
|
|
||||||
|
# Windows Azure Build Output
|
||||||
|
csx/
|
||||||
|
*.build.csdef
|
||||||
|
|
||||||
|
# Windows Store app package directory
|
||||||
|
AppPackages/
|
||||||
|
|
||||||
|
# Others
|
||||||
|
sql/
|
||||||
|
*.Cache
|
||||||
|
ClientBin/
|
||||||
|
[Ss]tyle[Cc]op.*
|
||||||
|
![Ss]tyle[Cc]op.targets
|
||||||
|
~$*
|
||||||
|
*~
|
||||||
|
*.dbmdl
|
||||||
|
*.[Pp]ublish.xml
|
||||||
|
|
||||||
|
*.publishsettings
|
||||||
|
|
||||||
|
# RIA/Silverlight projects
|
||||||
|
Generated_Code/
|
||||||
|
|
||||||
|
# Backup & report files from converting an old project file to a newer
|
||||||
|
# Visual Studio version. Backup files are not needed, because we have git ;-)
|
||||||
|
_UpgradeReport_Files/
|
||||||
|
Backup*/
|
||||||
|
UpgradeLog*.XML
|
||||||
|
UpgradeLog*.htm
|
||||||
|
|
||||||
|
# SQL Server files
|
||||||
|
App_Data/*.mdf
|
||||||
|
App_Data/*.ldf
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# Windows detritus
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
# Windows image file caches
|
||||||
|
Thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
|
||||||
|
# Folder config file
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
|
# Recycle Bin used on file shares
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
|
||||||
|
# Mac desktop service store files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
_NCrunch*
|
||||||
|
|
||||||
|
.vs
|
||||||
|
.vscode
|
||||||
|
.rider
|
||||||
|
|
||||||
|
test-files/
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules/
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
*.min.js
|
||||||
|
*.lock
|
||||||
|
testfiles/
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Omega Leo
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# SecretSecurity
|
||||||
|
|
||||||
|
[](LICENSE)
|
||||||
|
[](https://github.com/sponsors/omegaleo)
|
||||||
|
[](https://ko-fi.com/omegaleo)
|
||||||
|
|
||||||
|
A .NET CLI tool that scans staged git changes for secrets, API keys, tokens, and sensitive credentials — and blocks commits when critical findings are detected.
|
||||||
|
|
||||||
|
## What it detects
|
||||||
|
|
||||||
|
| Type | Examples | Severity |
|
||||||
|
|------|----------|----------|
|
||||||
|
| Provider tokens | AWS access keys (`AKIA…`), Stripe live keys (`sk_live_…`), GitHub PATs (`ghp_…`), Slack tokens | Critical |
|
||||||
|
| Private keys | RSA, EC, OpenSSH private key blocks | Critical |
|
||||||
|
| Azure connection strings | `DefaultEndpointsProtocol`, `AccountKey` | Critical |
|
||||||
|
| Explicit secret assignments | `api_key=`, `token=`, `secret:` with a value | High |
|
||||||
|
| Database connection strings | General ADO.NET, Postgres/MySQL/MongoDB/Redis URLs | High |
|
||||||
|
| Suspicious blobs | High-entropy strings in suspicious context | Warning |
|
||||||
|
|
||||||
|
Dev-safe connections (localhost, 127.0.0.1, integrated security) are automatically ignored.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Requires [.NET 10 SDK](https://dotnet.microsoft.com/download) or later.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet tool install --global SecretSecurity --add-source https://nuget.pkg.github.com/omegaleo/index.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Install the pre-commit hook
|
||||||
|
|
||||||
|
Run once inside your repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
secretsecurity install
|
||||||
|
```
|
||||||
|
|
||||||
|
This writes a `pre-commit` hook to `.git/hooks/` that runs `secretsecurity scan` automatically on every commit.
|
||||||
|
|
||||||
|
### Scan manually
|
||||||
|
|
||||||
|
```bash
|
||||||
|
secretsecurity scan
|
||||||
|
```
|
||||||
|
|
||||||
|
Scans all currently staged changes (`git diff --cached`). Exits with code `1` if any High or Critical findings are found, blocking the commit.
|
||||||
|
|
||||||
|
### Ignore files
|
||||||
|
|
||||||
|
Create a `.secretsecurityignore` file in your repo root with one pattern per line:
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/fixtures/
|
||||||
|
mock_secrets.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Any staged file whose path contains a listed pattern is skipped.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
1. Runs `git diff --cached -U0` to get only the lines you are about to commit.
|
||||||
|
2. Each added line is checked against a set of compiled regex patterns ordered by confidence.
|
||||||
|
3. Matched values are partially masked in the output (e.g. `ghp_****Ab`).
|
||||||
|
4. If any finding is High or Critical, the scan exits with code `1`, preventing the commit.
|
||||||
|
|
||||||
|
## Building from source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/omegaleo/SecretSecurity.git
|
||||||
|
cd SecretSecurity
|
||||||
|
dotnet build
|
||||||
|
dotnet run --project SecretSecurity -- scan
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! To get started:
|
||||||
|
|
||||||
|
1. Fork the repository and create a branch from `main`.
|
||||||
|
2. Make your changes and ensure the project builds (`dotnet build`).
|
||||||
|
3. Open a pull request describing what you changed and why.
|
||||||
|
|
||||||
|
Please open an issue first for any significant change so we can discuss the approach before you invest time in it.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
If SecretSecurity saves you from an accidental secret leak, consider supporting its development:
|
||||||
|
|
||||||
|
- [GitHub Sponsors](https://github.com/sponsors/omegaleo) — sponsor directly on GitHub
|
||||||
|
- [Ko-fi](https://ko-fi.com/omegaleo) — buy me a coffee
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT — see [LICENSE.md](LICENSE) for details.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<Solution>
|
||||||
|
<Project Path="SecretSecurity/SecretSecurity.csproj" />
|
||||||
|
</Solution>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace SecretSecurity.MethodExtensions
|
||||||
|
{
|
||||||
|
public static class ListExtensions
|
||||||
|
{
|
||||||
|
public static ReadOnlySpan<string> AsReadOnlySpan(this IEnumerable<string> strings)
|
||||||
|
{
|
||||||
|
return new ReadOnlySpan<string>(strings.ToArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace SecretSecurity.Models
|
||||||
|
{
|
||||||
|
public class DiffEntry
|
||||||
|
{
|
||||||
|
public string File { get; set; }
|
||||||
|
public int Line { get; set; }
|
||||||
|
public string Content { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace SecretSecurity.Models
|
||||||
|
{
|
||||||
|
public enum FindingType
|
||||||
|
{
|
||||||
|
ApiKey,
|
||||||
|
Token,
|
||||||
|
PrivateKey,
|
||||||
|
ConnectionString,
|
||||||
|
CloudCredential,
|
||||||
|
SuspiciousBlob
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Severity
|
||||||
|
{
|
||||||
|
Info,
|
||||||
|
Warning,
|
||||||
|
High,
|
||||||
|
Critical
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SecretFinding
|
||||||
|
{
|
||||||
|
public string File { get; set; }
|
||||||
|
public int Line { get; set; }
|
||||||
|
public FindingType Type { get; set; } // "API Key", "Connection String"
|
||||||
|
public string ValuePreview { get; set; } // partially masked
|
||||||
|
public Severity Severity { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
using SecretSecurity;
|
||||||
|
using SecretSecurity.MethodExtensions;
|
||||||
|
using SecretSecurity.Models;
|
||||||
|
using Spectre.Console;
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
static List<string> _ignorePatterns = new();
|
||||||
|
|
||||||
|
static void Main(string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length == 0)
|
||||||
|
{
|
||||||
|
PrintHelp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var arg = args[0].ToLower();
|
||||||
|
|
||||||
|
switch (arg)
|
||||||
|
{
|
||||||
|
case "scan":
|
||||||
|
PerformScan();
|
||||||
|
break;
|
||||||
|
case "install":
|
||||||
|
InstallCommand();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
PrintHelp();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void LoadIgnoreFile()
|
||||||
|
{
|
||||||
|
if (!File.Exists(".secretsecurityignore"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_ignorePatterns = File.ReadAllLines(".secretsecurityignore")
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void InstallCommand()
|
||||||
|
{
|
||||||
|
var root = RunGit("rev-parse --show-toplevel").Trim();
|
||||||
|
var hookPath = Path.Combine(root, ".git", "hooks", "pre-commit");
|
||||||
|
|
||||||
|
var script = """
|
||||||
|
#!/bin/sh
|
||||||
|
SecretSecurity scan
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "❌ SecretSecurity blocked commit"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
""";
|
||||||
|
|
||||||
|
File.WriteAllText(hookPath, script);
|
||||||
|
|
||||||
|
Console.WriteLine("✅ SecretSecurity installed successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void PrintHelp()
|
||||||
|
{
|
||||||
|
Console.WriteLine(@"
|
||||||
|
SecretSecurity CLI
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
scan Scan staged git changes
|
||||||
|
install Install git pre-commit hook
|
||||||
|
ignore Add ignore-rule
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void PerformScan()
|
||||||
|
{
|
||||||
|
LoadIgnoreFile();
|
||||||
|
|
||||||
|
AnsiConsole.MarkupLine("[cyan]🔍 SecretSecurity scanning...[/]\n");
|
||||||
|
var findings = GetFindings();
|
||||||
|
|
||||||
|
PrintFindings(findings);
|
||||||
|
|
||||||
|
bool shouldBlockCommit = findings.Any(f =>
|
||||||
|
f.Severity == Severity.High ||
|
||||||
|
f.Severity == Severity.Critical);
|
||||||
|
|
||||||
|
if (shouldBlockCommit)
|
||||||
|
{
|
||||||
|
AnsiConsole.Write(
|
||||||
|
new Panel("[red]Commit blocked: High/Critical secrets detected[/]")
|
||||||
|
.Header("SecretSecurity")
|
||||||
|
.Border(BoxBorder.Double)
|
||||||
|
);
|
||||||
|
|
||||||
|
var critical = findings
|
||||||
|
.Where(f => f.Severity is Severity.High or Severity.Critical)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var f in critical)
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine($"[red]• {f.Type}[/] in [grey]{f.File}:{f.Line}[/]");
|
||||||
|
}
|
||||||
|
|
||||||
|
Environment.Exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void PrintFindings(IEnumerable<SecretFinding> findings)
|
||||||
|
{
|
||||||
|
if (findings.Count() == 0)
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine("[green]✅ SecretSecurity: No secrets detected.[/]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AnsiConsole.Write(
|
||||||
|
new Panel($"[red]❌ {findings.Count()} issue(s) detected[/]")
|
||||||
|
.Border(BoxBorder.Rounded)
|
||||||
|
.Header("SecretSecurity Report")
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach (var f in findings)
|
||||||
|
{
|
||||||
|
var color = f.Severity switch
|
||||||
|
{
|
||||||
|
Severity.Critical => "red",
|
||||||
|
Severity.High => "darkred",
|
||||||
|
Severity.Warning => "yellow",
|
||||||
|
_ => "grey"
|
||||||
|
};
|
||||||
|
|
||||||
|
AnsiConsole.MarkupLine($"""
|
||||||
|
[{color}]{f.Severity}[/]
|
||||||
|
📄 [bold]{f.File}:{f.Line}[/]
|
||||||
|
🔎 {f.ValuePreview}
|
||||||
|
|
||||||
|
────────────────────────────
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void PrintSeverity(Severity severity)
|
||||||
|
{
|
||||||
|
switch (severity)
|
||||||
|
{
|
||||||
|
case Severity.Critical:
|
||||||
|
Console.ForegroundColor = ConsoleColor.Red;
|
||||||
|
break;
|
||||||
|
case Severity.High:
|
||||||
|
Console.ForegroundColor = ConsoleColor.DarkRed;
|
||||||
|
break;
|
||||||
|
case Severity.Warning:
|
||||||
|
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Console.ForegroundColor = ConsoleColor.Gray;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<SecretFinding> GetFindings()
|
||||||
|
{
|
||||||
|
var findings = new List<SecretFinding>();
|
||||||
|
|
||||||
|
var diffLines = GetStagedDiffWithContext();
|
||||||
|
|
||||||
|
foreach (var entry in diffLines)
|
||||||
|
{
|
||||||
|
var result = Validator.FindSecret(entry.Content);
|
||||||
|
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
if (IsIgnored(entry.File))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (IsDocumentationFile(entry.File))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
result.File = entry.File;
|
||||||
|
result.Line = entry.Line;
|
||||||
|
findings.Add(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return findings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<DiffEntry> GetStagedDiffWithContext()
|
||||||
|
{
|
||||||
|
var output = RunGit("diff --cached -U0");
|
||||||
|
|
||||||
|
var lines = output.Split('\n');
|
||||||
|
|
||||||
|
var results = new List<DiffEntry>();
|
||||||
|
|
||||||
|
string currentFile = null;
|
||||||
|
int lineNumber = 0;
|
||||||
|
|
||||||
|
foreach (var line in lines)
|
||||||
|
{
|
||||||
|
// detect file
|
||||||
|
if (line.StartsWith("+++ b/"))
|
||||||
|
{
|
||||||
|
currentFile = line.Substring(6);
|
||||||
|
lineNumber = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// detect added line
|
||||||
|
else if (line.StartsWith("+") && !line.StartsWith("+++"))
|
||||||
|
{
|
||||||
|
lineNumber++;
|
||||||
|
|
||||||
|
results.Add(new DiffEntry
|
||||||
|
{
|
||||||
|
File = currentFile ?? "unknown",
|
||||||
|
Line = lineNumber,
|
||||||
|
Content = line.Substring(1) // remove "+"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string RunGit(string args)
|
||||||
|
{
|
||||||
|
var process = new Process();
|
||||||
|
process.StartInfo.FileName = "git";
|
||||||
|
process.StartInfo.Arguments = args;
|
||||||
|
process.StartInfo.RedirectStandardOutput = true;
|
||||||
|
process.StartInfo.UseShellExecute = false;
|
||||||
|
process.Start();
|
||||||
|
|
||||||
|
return process.StandardOutput.ReadToEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsIgnored(string file)
|
||||||
|
{
|
||||||
|
return _ignorePatterns.Any(p =>
|
||||||
|
file.Contains(p.Replace("*", "")));
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsDocumentationFile(string file)
|
||||||
|
{
|
||||||
|
file = file.ToLowerInvariant();
|
||||||
|
|
||||||
|
return file.EndsWith(".md") ||
|
||||||
|
file.EndsWith(".txt") ||
|
||||||
|
file.Contains("readme") ||
|
||||||
|
file.Contains("docs");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"profiles": {
|
||||||
|
"SecretSecurity": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"commandLineArgs": "scan"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<PackAsTool>true</PackAsTool>
|
||||||
|
<ToolCommandName>secretsecurity</ToolCommandName>
|
||||||
|
<PackageId>SecretSecurity</PackageId>
|
||||||
|
<Version>1.0.0</Version>
|
||||||
|
<Authors>Omega Leo</Authors>
|
||||||
|
<Description>CLI tool for scanning secrets and sensitive data in files.</Description>
|
||||||
|
<RepositoryUrl>https://github.com/omegaleo/SecretSecurity</RepositoryUrl>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Spectre.Console" Version="0.57.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
using SecretSecurity.Models;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace SecretSecurity
|
||||||
|
{
|
||||||
|
public static class Validator
|
||||||
|
{
|
||||||
|
// 1. Explicit secret assignments (HIGH confidence)
|
||||||
|
static Regex _explicitSecret = new Regex(
|
||||||
|
@"(?i)(api[_-]?key|secret|token|access[_-]?key|auth[_-]?token)\s*[:=]\s*[\""']?(?<value>[a-z0-9_\-]{16,})[\""']?",
|
||||||
|
RegexOptions.Compiled);
|
||||||
|
|
||||||
|
// 2. Provider-specific tokens (VERY high confidence)
|
||||||
|
static Regex _providerSecrets = new Regex(
|
||||||
|
@"(AKIA[0-9A-Z]{16}|sk_live_[0-9a-zA-Z]{24,}|ghp_[0-9a-zA-Z]{36}|xox[baprs]-[0-9a-zA-Z-]{10,})",
|
||||||
|
RegexOptions.Compiled);
|
||||||
|
|
||||||
|
// 3. Private key blocks
|
||||||
|
static Regex _privateKey = new Regex(
|
||||||
|
@"-----BEGIN (RSA|EC|OPENSSH) PRIVATE KEY-----",
|
||||||
|
RegexOptions.Compiled);
|
||||||
|
|
||||||
|
// 4. High entropy fallback (LIMITED usage only)
|
||||||
|
static Regex _randomBlob = new Regex(
|
||||||
|
@"\b[a-zA-Z0-9_\-]{40,}\b",
|
||||||
|
RegexOptions.Compiled);
|
||||||
|
|
||||||
|
// 5. General Connection Strings
|
||||||
|
static Regex _generalConnString = new Regex(
|
||||||
|
"(?i)\\b(server|data source|database|uid|user id|pwd|password)\\s*=\\s*[^;]+",
|
||||||
|
RegexOptions.Compiled);
|
||||||
|
|
||||||
|
// 6. Postgres Connection Strings
|
||||||
|
static Regex _postgres = new Regex(
|
||||||
|
"(?i)(postgres|mysql|mongodb|redis):\\/\\/[^\\\\s\\\"']+",
|
||||||
|
RegexOptions.Compiled);
|
||||||
|
|
||||||
|
// 7. Azure Connection Strings
|
||||||
|
static Regex _azureConnString = new Regex(
|
||||||
|
"(?i)(DefaultEndpointsProtocol|AccountName|AccountKey)=",
|
||||||
|
RegexOptions.Compiled);
|
||||||
|
|
||||||
|
public static SecretFinding? FindSecret(string line)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// 1. Provider secrets (highest confidence)
|
||||||
|
if (_providerSecrets.IsMatch(line))
|
||||||
|
{
|
||||||
|
return new SecretFinding
|
||||||
|
{
|
||||||
|
Type = FindingType.Token,
|
||||||
|
Severity = Severity.Critical,
|
||||||
|
ValuePreview = Mask(line)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Private keys
|
||||||
|
if (_privateKey.IsMatch(line))
|
||||||
|
{
|
||||||
|
return new SecretFinding
|
||||||
|
{
|
||||||
|
Type = FindingType.ApiKey,
|
||||||
|
Severity = Severity.Critical,
|
||||||
|
ValuePreview = "-----BEGIN **** KEY-----"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Explicit assignment secrets
|
||||||
|
var explicitMatch = _explicitSecret.Match(line);
|
||||||
|
if (explicitMatch.Success)
|
||||||
|
{
|
||||||
|
return new SecretFinding
|
||||||
|
{
|
||||||
|
Type = FindingType.ApiKey | FindingType.Token,
|
||||||
|
Severity = Severity.High,
|
||||||
|
ValuePreview = Mask(explicitMatch.Value)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Connection strings (general)
|
||||||
|
if (_generalConnString.IsMatch(line))
|
||||||
|
{
|
||||||
|
if (IsSafeDevConnection(line))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SecretFinding
|
||||||
|
{
|
||||||
|
Type = FindingType.ConnectionString,
|
||||||
|
Severity = Severity.High,
|
||||||
|
ValuePreview = MaskConnection(line)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Postgres / DB URLs
|
||||||
|
if (_postgres.IsMatch(line))
|
||||||
|
{
|
||||||
|
return new SecretFinding
|
||||||
|
{
|
||||||
|
Type = FindingType.ConnectionString,
|
||||||
|
Severity = Severity.High,
|
||||||
|
ValuePreview = Mask(line)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Azure connection strings
|
||||||
|
if (_azureConnString.IsMatch(line))
|
||||||
|
{
|
||||||
|
return new SecretFinding
|
||||||
|
{
|
||||||
|
Type = FindingType.ConnectionString,
|
||||||
|
Severity = Severity.Critical,
|
||||||
|
ValuePreview = Mask(line)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Fallback entropy check (low confidence)
|
||||||
|
if (_randomBlob.IsMatch(line) && LooksSuspicious(line))
|
||||||
|
{
|
||||||
|
return new SecretFinding
|
||||||
|
{
|
||||||
|
Type = FindingType.SuspiciousBlob,
|
||||||
|
Severity = Severity.Warning,
|
||||||
|
ValuePreview = Mask(line)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string Mask(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
return value;
|
||||||
|
|
||||||
|
if (value.Length <= 10)
|
||||||
|
return "****";
|
||||||
|
|
||||||
|
return value.Substring(0, 4) + "****" + value[^2..];
|
||||||
|
}
|
||||||
|
|
||||||
|
static string MaskConnection(string conn)
|
||||||
|
{
|
||||||
|
return Regex.Replace(conn,
|
||||||
|
"(?i)(password|pwd|user id|uid|key)=([^;]+)",
|
||||||
|
"$1=****");
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool LooksSuspicious(string line)
|
||||||
|
{
|
||||||
|
// reduce false positives
|
||||||
|
return line.Contains("=") ||
|
||||||
|
line.Contains(":") ||
|
||||||
|
line.Contains("token") ||
|
||||||
|
line.Contains("key") ||
|
||||||
|
line.Contains("secret");
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsSafeDevConnection(string line)
|
||||||
|
{
|
||||||
|
line = line.ToLowerInvariant();
|
||||||
|
|
||||||
|
return line.Contains("localhost") ||
|
||||||
|
line.Contains("127.0.0.1") ||
|
||||||
|
line.Contains("::1") ||
|
||||||
|
line.Contains("database=mydb") && line.Contains("trusted_connection=true") ||
|
||||||
|
line.Contains("integrated security=true");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
AWS_KEY=AKIAIOSFODNN7EXAMPLE
|
||||||
|
AWS_SECRET=abcd1234abcd1234abcd1234abcd1234
|
||||||
|
|
||||||
|
GITHUB_TOKEN=ghp_ABCDEF1234567890ABCDEF1234567890AB
|
||||||
|
|
||||||
|
SLACK_TOKEN=xoxb-1234567890-abcdefghij
|
||||||
|
|
||||||
|
DATABASE_URL=postgres://admin:SuperSecret@localhost:5432/prod
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"DefaultConnection": "Server=localhost;Database=GameDb;Trusted_Connection=true;"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"DefaultConnection": "Server=mydb;Database=Prod;User Id=admin;Password=SuperSecret123;"
|
||||||
|
},
|
||||||
|
"Stripe": {
|
||||||
|
"SecretKey": "sk_live_9x8y7z6w5v4u3t2s"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
public class Config
|
||||||
|
{
|
||||||
|
public string ApiKeyName = "access_key";
|
||||||
|
public string TokenDescription = "used for authentication flow";
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
public class Startup
|
||||||
|
{
|
||||||
|
// safe config name
|
||||||
|
public const string ApiKeyName = "access_key";
|
||||||
|
|
||||||
|
// suspicious placeholder
|
||||||
|
private string tokenExample = "your_token_here_1234567890abcdef";
|
||||||
|
|
||||||
|
// real secret
|
||||||
|
private string stripeKey = "sk_live_1234567890abcdef";
|
||||||
|
|
||||||
|
// connection string
|
||||||
|
private string db = "Server=prod;Database=Main;User Id=admin;Password=SuperSecret;";
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"Level": "Debug"
|
||||||
|
},
|
||||||
|
"ApiKeyHint": "use environment variable API_KEY",
|
||||||
|
|
||||||
|
"Stripe": {
|
||||||
|
"Secret": "sk_live_1234567890abcdef"
|
||||||
|
},
|
||||||
|
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"Default": "postgres://user:pass@localhost/db"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
This file contains notes about API keys and tokens.
|
||||||
|
|
||||||
|
DO NOT COMMIT REAL VALUES.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
API_KEY=example_value_1234567890
|
||||||
|
|
||||||
|
Actual production key:
|
||||||
|
ghp_ABCDEF1234567890ABCDEF1234567890AB
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEowIBAAKCAQEA7examplefakekeydataaaaaaaaaaaaaaaaaaaaa
|
||||||
|
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
public class Config
|
||||||
|
{
|
||||||
|
public string ApiKey = "sk_live_1234567890abcdef";
|
||||||
|
public string Secret = "ghp_ABCDEF1234567890ABCDEF1234567890AB";
|
||||||
|
|
||||||
|
string conn = "Server=db;User Id=admin;Password=123456;";
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
public class Settings
|
||||||
|
{
|
||||||
|
public string ApiKeyPlaceholder = "your_api_key_here_123456789";
|
||||||
|
public string TokenExample = "example_token_value_1234567890abcdef";
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# development environment
|
||||||
|
|
||||||
|
DATABASE_URL=postgres://user:password@localhost:5432/db
|
||||||
|
API_KEY=replace_this_value
|
||||||
|
SECRET_TOKEN=example_only_but_long_enough_1234567890
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
User login failed
|
||||||
|
Auth token received: abcdef1234567890abcdef1234567890abcdef
|
||||||
|
Request processed successfully
|
||||||
Reference in New Issue
Block a user