Repository: TSCarterJr/UnsecuredAPIKeys-OpenSource
Branch: main
Commit: 4e15a236ed18
Files: 35
Total size: 134.7 KB
Directory structure:
gitextract_966uldie/
├── .gitattributes
├── .github/
│ └── workflows/
│ ├── claude-code-review.yml
│ └── claude.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── UnsecuredAPIKeys-OpenSource.sln
├── UnsecuredAPIKeys.CLI/
│ ├── Constants.cs
│ ├── Program.cs
│ ├── Services/
│ │ ├── DatabaseService.cs
│ │ ├── ScraperService.cs
│ │ └── VerifierService.cs
│ ├── UnsecuredAPIKeys.CLI.csproj
│ └── appsettings.example.json
├── UnsecuredAPIKeys.Data/
│ ├── Common/
│ │ ├── ApiProviderAttribute.cs
│ │ └── CommonEnums.cs
│ ├── DBContext.cs
│ ├── DesignTimeDbContextFactory.cs
│ ├── Models/
│ │ ├── APIKey.cs
│ │ ├── ApplicationSetting.cs
│ │ ├── RepoReference.cs
│ │ ├── SearchProviderToken.cs
│ │ └── SearchQuery.cs
│ └── UnsecuredAPIKeys.Data.csproj
└── UnsecuredAPIKeys.Providers/
├── AI Providers/
│ ├── AnthropicProvider.cs
│ ├── GoogleProvider.cs
│ └── OpenAIProvider.cs
├── ApiProviderRegistry.cs
├── Common/
│ └── ValidationResult.cs
├── Search Providers/
│ └── GitHubSearchProvider.cs
├── UnsecuredAPIKeys.Providers.csproj
├── _Base/
│ └── BaseApiKeyProvider.cs
└── _Interfaces/
├── IApiKeyProvider.cs
└── ISearchProvider.cs
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain
================================================
FILE: .github/workflows/claude-code-review.yml
================================================
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: |
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number }}
Please review this pull request and provide feedback on:
- Code quality and best practices
- Potential bugs or issues
- Performance considerations
- Security concerns
- Test coverage
Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'
================================================
FILE: .github/workflows/claude.yml
================================================
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'
# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr:*)'
================================================
FILE: .gitignore
================================================
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# 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
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# Claude Code
.claude/
CLAUDE.md
# Environment files
**/.env
**/.env.*
!**/.env.example
# SQLite databases
*.db
*.sqlite
*.sqlite3
# Local config (users will copy from .example)
**/appsettings.json
!**/appsettings.example.json
**/appsettings.Development.json
**/appsettings.Production.json
================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to UnsecuredAPIKeys Open Source.
## [1.0.0] - 2025-12-09 - Lite Version Release
This release transforms the project from a full-featured web platform into a streamlined CLI tool for local use.
### Why This Change?
The original open-source release included the full platform architecture (WebAPI, UI, PostgreSQL, 15+ providers). This created barriers for users who just wanted to:
- Learn about API key security
- Run simple searches locally
- Understand how key discovery works
The lite version removes these barriers while the full platform remains available at [www.UnsecuredAPIKeys.com](https://www.UnsecuredAPIKeys.com).
---
### Added
#### New CLI Application (`UnsecuredAPIKeys.CLI/`)
- **Menu-driven interface** using Spectre.Console for rich terminal UI
- **ScraperService** - Searches GitHub for exposed API keys continuously
- **VerifierService** - Maintains exactly 50 valid keys with automatic re-verification
- **Fallback validation**: Tries multiple providers when assigned provider fails
- **Auto-reclassification**: Updates key's ApiType if different provider validates it
- **DatabaseService** - Handles SQLite initialization, statistics, and exports
- **Constants.cs** - Centralized limits (`MAX_VALID_KEYS = 50`) and app info
- **appsettings.example.json** - Configuration template for self-hosting
#### Documentation
- **CHANGELOG.md** - This file
- **Badges** in README (GitHub Stars, .NET 10, License)
- **Stars thank you** message for community support
- **Self-hosting sections**: Database management, Search Queries, Rate Limiting, Troubleshooting
- **Platform support** documented (Windows, macOS, Linux)
#### Default Search Queries (Auto-seeded)
- OpenAI: `sk-proj-`, `sk-or-v1-`, `OPENAI_API_KEY`, `openai.api_key`
- Anthropic: `sk-ant-api`, `ANTHROPIC_API_KEY`, `anthropic_api_key`
- Google: `AIzaSy`, `GOOGLE_API_KEY`, `gemini_api_key`
---
### Changed
#### Database: PostgreSQL → SQLite
- **Before**: Required PostgreSQL server, connection strings, migrations
- **After**: Single `unsecuredapikeys.db` file, auto-created on first run
- No migrations needed - uses `EnsureCreated()` for simplicity
- Package change: `Npgsql.EntityFrameworkCore.PostgreSQL` → `Microsoft.EntityFrameworkCore.Sqlite`
#### Providers: 15+ → 3
- **Kept**: OpenAI, Anthropic, Google AI
- **Removed**: Cohere, DeepSeek, ElevenLabs, Groq, HuggingFace, MistralAI, OpenRouter, PerplexityAI, Replicate, StabilityAI, TogetherAI
#### Search Providers: 3 → 1
- **Kept**: GitHub (via Octokit)
- **Removed**: GitLab, SourceGraph
#### Architecture: Web Platform → CLI Tool
- **Before**: WebAPI + Next.js UI + Separate Bots + PostgreSQL
- **After**: Single CLI application + SQLite
#### Valid Key Limit
- **Before**: Configurable/unlimited
- **After**: Hard cap of 50 keys (enforced in `LiteLimits.MAX_VALID_KEYS`)
---
### Removed
#### Projects Deleted
| Project | Description |
|---------|-------------|
| `UnsecuredAPIKeys.WebAPI/` | REST API, SignalR hub, controllers |
| `UnsecuredAPIKeys.UI/` | Next.js frontend, React components |
| `UnsecuredAPIKeys.Bots.Scraper/` | Standalone scraper service |
| `UnsecuredAPIKeys.Bots.Verifier/` | Standalone verifier service |
| `UnsecuredAPIKeys.Common/` | Shared utilities (was empty) |
#### AI Providers Removed (11)
- CohereProvider.cs
- DeepSeekProvider.cs
- ElevenLabsProvider.cs
- GroqProvider.cs
- HuggingFaceProvider.cs
- MistralAIProvider.cs
- OpenRouterProvider.cs
- PerplexityAIProvider.cs
- ReplicateProvider.cs
- StabilityAIProvider.cs
- TogetherAIProvider.cs
#### Search Providers Removed (2)
- GitLabSearchProvider.cs
- SourceGraphSearchProvider.cs
#### Services Removed
- GitHubIssueService.cs (auto issue creation)
- SnitchLeaderboardService.cs (community leaderboard)
- UserModerationService.cs (user bans)
#### Database Models Removed (15)
| Model | Purpose |
|-------|---------|
| DiscordUser | Discord OAuth integration |
| UserSession | Session management |
| UserBan | User moderation |
| DonationTracking | PayPal donations |
| DonationSupporter | Donor recognition |
| IssueSubmissionTracking | GitHub issue automation |
| IssueVerification | Issue verification flow |
| SnitchLeaderboard | Community rankings |
| VerificationBatch | Batch job tracking |
| VerificationBatchResult | Batch results |
| KeyInvalidation | Key lifecycle tracking |
| KeyRotation | Key rotation events |
| PatternEffectiveness | Search pattern analytics |
| Proxy | Proxy rotation support |
| RateLimitLog | API rate limit tracking |
#### Database Migrations Removed (30+ files)
All PostgreSQL migrations deleted - SQLite uses runtime schema creation.
#### Configuration Files Removed
- `.dockerignore`
- `.vite/` directories
- `Deploy-VerificationBot.ps1`
- `package.json`, `package-lock.json`
- `analyze-unmatched-keys.ps1`
- `check-unmatched-keys.ps1`
- All `appsettings.json` files (kept `.example` versions)
#### Features Not in Lite Version
- Web UI dashboard
- Real-time SignalR updates
- Discord OAuth login
- PayPal donation integration
- GitHub issue auto-creation
- Snitch leaderboard
- User bans/moderation
- Proxy rotation
- Rate limit tracking tables
- Multi-search-provider support
- Batch verification locking
---
### Security
- **Enforced .gitignore** rules for:
- `*.db`, `*.sqlite`, `*.sqlite3` (database files)
- `.env`, `.env.*` (environment files)
- `.claude/` (AI assistant files)
- `appsettings.json` (local config with tokens)
- **Warning comments** added throughout code about not publishing results publicly
- **Security Warning** section prominent in README
---
### Migration Guide
#### For Users of the Old Version
The lite version is a **complete rewrite**. There is no migration path - it's designed for fresh local use.
If you need the original Web UI + WebAPI architecture:
- Check the [`legacy_ui`](https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/tree/legacy_ui) branch (no longer maintained)
If you need the full platform features:
- Visit [www.UnsecuredAPIKeys.com](https://www.UnsecuredAPIKeys.com)
#### For Contributors
Open PRs against the old architecture are now outdated:
- PR #5 (Docker Compose + Postgres) - No longer applicable
- PR #8 (Next.js dependency bump) - UI removed
- PR #9 (start.ps1) - Empty PR
New contributions should target the CLI architecture.
---
### Lite vs Full Comparison
| Feature | Lite (This Repo) | Full Version |
|---------|------------------|--------------|
| Search Providers | GitHub | GitHub, GitLab, SourceGraph |
| API Providers | 3 (OpenAI, Anthropic, Google) | 15+ |
| Valid Key Cap | 50 | Higher limits |
| Interface | CLI | Web UI + API |
| Database | SQLite (local file) | PostgreSQL |
| Real-time Updates | No | SignalR |
| Community Features | No | Leaderboard, Discord |
| Self-hosted | Yes | Yes (complex) |
| Beginner Friendly | Yes | No |
---
### Technical Details
#### Dependencies (CLI)
```xml
<PackageReference Include="Spectre.Console" Version="0.49.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
<PackageReference Include="Octokit" Version="13.0.1" />
```
#### Rate Limits (Built-in)
| Operation | Delay |
|-----------|-------|
| Between searches | 5,000ms |
| Between verifications | 1,000ms |
| Verification batch size | 10 keys |
#### File Structure
```
UnsecuredAPIKeys-OpenSource/
├── UnsecuredAPIKeys.CLI/
│ ├── Program.cs # Entry point, menu UI
│ ├── Constants.cs # LiteLimits, AppInfo
│ ├── Services/
│ │ ├── ScraperService.cs # GitHub search
│ │ ├── VerifierService.cs # Key validation
│ │ └── DatabaseService.cs # SQLite ops
│ └── appsettings.example.json
├── UnsecuredAPIKeys.Data/
│ ├── DBContext.cs # EF Core context
│ ├── Models/ # 5 essential models
│ └── Common/CommonEnums.cs # Simplified enums
├── UnsecuredAPIKeys.Providers/
│ ├── AI Providers/ # 3 providers
│ ├── Search Providers/ # GitHub only
│ └── _Base/, _Interfaces/ # Framework
├── CHANGELOG.md
├── CLAUDE.md
├── LICENSE
└── README.md
```
---
## [1.0.0] - 2024-07-21 - Initial Open Source Release
Initial release of the full UnsecuredAPIKeys platform as open source.
### Included
- WebAPI with REST endpoints and SignalR
- Next.js frontend with HeroUI components
- PostgreSQL database with EF Core migrations
- 15+ API key validation providers
- 3 search providers (GitHub, GitLab, SourceGraph)
- Discord OAuth integration
- PayPal donation integration
- Automated scraper and verifier bots
- Snitch leaderboard
- User moderation system
---
**Full Version**: [www.UnsecuredAPIKeys.com](https://www.UnsecuredAPIKeys.com)
================================================
FILE: LICENSE
================================================
UnsecuredAPIKeys Open Source License
Based on MIT License with Attribution Requirements
Copyright (c) 2025 TSCarterJr
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:
1. The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
2. MANDATORY UI ATTRIBUTION REQUIREMENT:
Any software, application, or system that incorporates, uses, modifies, or
derives from ANY portion of this Software (including but not limited to APIs,
backend processes, algorithms, data structures, bots, validation logic, or any
other component) and provides a public-facing user interface MUST display a
prominent link to the original project repository:
https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource
This attribution must be:
- Clearly visible in the user interface
- Accessible from the main/home page or footer
- Contain the text "Based on UnsecuredAPIKeys Open Source" or similar
- Link directly to https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource
This requirement applies regardless of the extent of modification, whether the
entire codebase is used or only small portions, and whether the derivative work
is commercial or non-commercial.
3. SCOPE OF APPLICATION:
This attribution requirement applies to any use of the Software, including but
not limited to:
- Using the backend APIs or database schemas
- Implementing the validation algorithms or provider patterns
- Using the bot architecture or scraping logic
- Incorporating the data models or business logic
- Using any code, concepts, or implementations from this project
4. EDUCATIONAL PURPOSE AND USER RESPONSIBILITY:
This Software is provided for EDUCATIONAL and SECURITY RESEARCH purposes only.
By using this Software, you acknowledge and agree that:
- You are solely responsible for your use of this Software
- You will comply with all applicable laws and regulations
- You will not use this Software for unauthorized access to any systems
- You will not use discovered API keys for any malicious or illegal purposes
- You will practice responsible disclosure when discovering exposed credentials
The author(s) and copyright holder(s) expressly disclaim any responsibility for:
- How you choose to use this Software
- Any actions you take based on information obtained through this Software
- Any legal consequences arising from your use or misuse of this Software
- Any damages caused by unauthorized or unethical use of this 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.
USE OF THIS SOFTWARE FOR ANY ILLEGAL, UNAUTHORIZED, OR UNETHICAL PURPOSES IS
STRICTLY PROHIBITED. THE USER ASSUMES ALL RISKS AND FULL RESPONSIBILITY FOR
THEIR ACTIONS.
VIOLATION OF THE ATTRIBUTION REQUIREMENT CONSTITUTES COPYRIGHT INFRINGEMENT
AND BREACH OF LICENSE TERMS, SUBJECT TO LEGAL ACTION AND DAMAGES.
================================================
FILE: README.md
================================================
# UnsecuredAPIKeys Lite
[](https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource)
[](https://dotnet.microsoft.com/download/dotnet/10.0)
[](LICENSE)

> **Thank you to everyone who has starred this project!** Your support helps raise awareness about API key security and encourages responsible disclosure practices.
> **Full Version Available:** [www.UnsecuredAPIKeys.com](https://www.UnsecuredAPIKeys.com)
>
> The full version offers: Web UI, all API providers, community features, and more.
A command-line tool for discovering and validating exposed API keys on GitHub. This lite version focuses on educational and security awareness purposes.
## Lite Version Limits
| Feature | Lite (This Repo) | Full Version |
|---------|------------------|--------------|
| Search Provider | GitHub only | GitHub, GitLab, SourceGraph |
| API Providers | OpenAI, Anthropic, Google | 15+ providers |
| Valid Key Cap | 50 keys | Higher limits |
| Interface | CLI | Web UI + API |
| Database | SQLite (local) | PostgreSQL |
## ⚠️ Educational Purpose Only
This tool is for **educational and security awareness purposes only**.
- **Learn** how API keys get exposed in public repositories
- **Understand** the importance of secret management
- **Report** exposed keys responsibly to repository owners
- **Never** use discovered keys for unauthorized access
**Do NOT publish your database or results publicly.** This would expose working API keys to malicious actors.
## Quick Start
### 1. Download
Download the latest release for your platform from [**Releases**](https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/releases):
| Platform | File |
|----------|------|
| Windows | `unsecuredapikeys-win-x64.exe` |
| Linux | `unsecuredapikeys-linux-x64` |
**No .NET runtime required** - these are self-contained executables.
### 2. Run
**Windows:**
```bash
.\unsecuredapikeys-win-x64.exe
```
**Linux:**
```bash
chmod +x unsecuredapikeys-linux-x64
./unsecuredapikeys-linux-x64
```
### 3. Configure GitHub Token
On first run, go to **Configure Settings** > **Set GitHub Token**.
Create a token at: https://github.com/settings/tokens
Required scope: `public_repo`
### 4. Start Searching
- **Start Scraper**: Searches GitHub for exposed API keys (runs continuously)
- **Start Verifier**: Maintains up to 50 valid keys (re-checks as needed)
- **View Status**: Shows current statistics
- **Export Keys**: Export to JSON or CSV
### Building from Source (Optional)
If you prefer to build from source:
```bash
git clone https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource.git
cd UnsecuredAPIKeys-OpenSource
dotnet build
cd UnsecuredAPIKeys.CLI
dotnet run
```
## How It Works
### Scraper
1. Uses your GitHub token to search for common API key patterns
2. Extracts potential keys using regex patterns for OpenAI, Anthropic, and Google
3. Stores discovered keys in a local SQLite database
### Verifier
1. Validates discovered keys against the actual provider APIs
2. Maintains exactly 50 valid keys (lite limit)
3. Re-checks existing valid keys periodically
4. When a key becomes invalid, verifies new ones until back to 50
## Project Structure
```
UnsecuredAPIKeys-OpenSource/
├── UnsecuredAPIKeys.CLI/ # Main CLI application
├── UnsecuredAPIKeys.Data/ # SQLite database layer
├── UnsecuredAPIKeys.Providers/ # API validation providers
├── unsecuredapikeys.db # SQLite database (auto-created)
└── README.md
```
## Prerequisites
- **.NET 10 SDK** - [Download here](https://dotnet.microsoft.com/download/dotnet/10.0)
- **GitHub Personal Access Token** - [Create here](https://github.com/settings/tokens)
- **Platform**: Windows, macOS, or Linux
## Supported Providers (Lite)
| Provider | Pattern Examples |
|----------|------------------|
| OpenAI | `sk-proj-*`, `sk-or-v1-*` |
| Anthropic | `sk-ant-api*` |
| Google AI | `AIzaSy*` |
## Configuration
Copy `appsettings.example.json` to `appsettings.json` and configure:
```json
{
"GitHub": {
"Token": "ghp_YOUR_TOKEN"
},
"Database": {
"Path": "unsecuredapikeys.db"
}
}
```
Or configure directly via the CLI menu.
## Database
The SQLite database (`unsecuredapikeys.db`) is auto-created on first run in the working directory.
| Action | How |
|--------|-----|
| **Location** | Same folder as the executable |
| **Reset** | Delete `unsecuredapikeys.db` and restart |
| **Backup** | Copy the `.db` file |
| **View data** | Use any SQLite browser (e.g., DB Browser for SQLite) |
## Search Queries
On first run, default search queries are automatically seeded:
- `sk-proj-`, `sk-or-v1-`, `OPENAI_API_KEY` (OpenAI)
- `sk-ant-api`, `ANTHROPIC_API_KEY` (Anthropic)
- `AIzaSy`, `GOOGLE_API_KEY` (Google)
The scraper rotates through these queries automatically.
## Rate Limiting
Built-in delays prevent API abuse:
| Operation | Delay |
|-----------|-------|
| Between searches | 5 seconds |
| Between verifications | 1 second |
| Batch size | 10 keys |
GitHub's API allows ~30 searches/minute with authentication.
## Troubleshooting
| Issue | Solution |
|-------|----------|
| "No GitHub token configured" | Go to Configure Settings > Set GitHub Token |
| "Rate limit exceeded" | Wait 60 seconds, or use a different token |
| Build fails | Ensure .NET 10 SDK is installed: `dotnet --version` |
| No keys found | Check your token has `public_repo` scope |
| Database locked | Close other apps using the .db file |
## Legal & Ethical Use
- **Educational Purpose**: This tool demonstrates API security vulnerabilities
- **Responsible Use**: Only use for legitimate security research
- **No Abuse**: Do not use discovered keys for unauthorized access
- **Compliance**: Follow all applicable laws and terms of service
## License
This project uses a **custom attribution-required license** based on MIT.
### Attribution Required
Any use of this code requires visible attribution:
- Display: "Based on UnsecuredAPIKeys Open Source"
- Link to: https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource
- Must be visible in UI/documentation
See [LICENSE](LICENSE) for full details.
## Legacy UI Version
Looking for the original Web UI + WebAPI architecture? Check the [`legacy_ui`](https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/tree/legacy_ui) branch.
> **Note**: The legacy branch is no longer actively maintained. For the full-featured web experience, use [www.UnsecuredAPIKeys.com](https://www.UnsecuredAPIKeys.com).
## Full Version
For higher limits, more providers, web interface, and community features:
**[www.UnsecuredAPIKeys.com](https://www.UnsecuredAPIKeys.com)**
---
**Remember**: Use responsibly and in accordance with applicable laws.
================================================
FILE: UnsecuredAPIKeys-OpenSource.sln
================================================
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.35931.197
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnsecuredAPIKeys.Data", "UnsecuredAPIKeys.Data\UnsecuredAPIKeys.Data.csproj", "{F4BC4B91-7945-4171-BA6E-BECFEF771A21}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnsecuredAPIKeys.Providers", "UnsecuredAPIKeys.Providers\UnsecuredAPIKeys.Providers.csproj", "{7389D703-A7C1-4853-A857-C1C97C1C6178}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnsecuredAPIKeys.CLI", "UnsecuredAPIKeys.CLI\UnsecuredAPIKeys.CLI.csproj", "{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Debug|x64.ActiveCfg = Debug|Any CPU
{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Debug|x64.Build.0 = Debug|Any CPU
{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Debug|x86.ActiveCfg = Debug|Any CPU
{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Debug|x86.Build.0 = Debug|Any CPU
{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Release|Any CPU.Build.0 = Release|Any CPU
{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Release|x64.ActiveCfg = Release|Any CPU
{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Release|x64.Build.0 = Release|Any CPU
{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Release|x86.ActiveCfg = Release|Any CPU
{F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Release|x86.Build.0 = Release|Any CPU
{7389D703-A7C1-4853-A857-C1C97C1C6178}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7389D703-A7C1-4853-A857-C1C97C1C6178}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7389D703-A7C1-4853-A857-C1C97C1C6178}.Debug|x64.ActiveCfg = Debug|Any CPU
{7389D703-A7C1-4853-A857-C1C97C1C6178}.Debug|x64.Build.0 = Debug|Any CPU
{7389D703-A7C1-4853-A857-C1C97C1C6178}.Debug|x86.ActiveCfg = Debug|Any CPU
{7389D703-A7C1-4853-A857-C1C97C1C6178}.Debug|x86.Build.0 = Debug|Any CPU
{7389D703-A7C1-4853-A857-C1C97C1C6178}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7389D703-A7C1-4853-A857-C1C97C1C6178}.Release|Any CPU.Build.0 = Release|Any CPU
{7389D703-A7C1-4853-A857-C1C97C1C6178}.Release|x64.ActiveCfg = Release|Any CPU
{7389D703-A7C1-4853-A857-C1C97C1C6178}.Release|x64.Build.0 = Release|Any CPU
{7389D703-A7C1-4853-A857-C1C97C1C6178}.Release|x86.ActiveCfg = Release|Any CPU
{7389D703-A7C1-4853-A857-C1C97C1C6178}.Release|x86.Build.0 = Release|Any CPU
{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Debug|x64.ActiveCfg = Debug|Any CPU
{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Debug|x64.Build.0 = Debug|Any CPU
{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Debug|x86.ActiveCfg = Debug|Any CPU
{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Debug|x86.Build.0 = Debug|Any CPU
{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Release|Any CPU.Build.0 = Release|Any CPU
{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Release|x64.ActiveCfg = Release|Any CPU
{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Release|x64.Build.0 = Release|Any CPU
{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Release|x86.ActiveCfg = Release|Any CPU
{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FEDEE8B0-6BD4-48B2-BA35-31BFC542BC0D}
EndGlobalSection
EndGlobal
================================================
FILE: UnsecuredAPIKeys.CLI/Constants.cs
================================================
namespace UnsecuredAPIKeys.CLI;
/// <summary>
/// Constants for the lite version of UnsecuredAPIKeys.
/// Full version available at www.UnsecuredAPIKeys.com
/// </summary>
public static class LiteLimits
{
/// <summary>
/// Maximum valid keys for lite version.
///
/// WARNING: If you modify this limit, do NOT publish your database
/// or results to a public repository. This would expose working API
/// keys to malicious actors who could abuse them.
///
/// For higher limits, use www.UnsecuredAPIKeys.com
/// </summary>
public const int MAX_VALID_KEYS = 50;
/// <summary>
/// Delay between verification batches (milliseconds).
/// </summary>
public const int VERIFICATION_DELAY_MS = 1000;
/// <summary>
/// Delay between GitHub search queries (milliseconds).
/// </summary>
public const int SEARCH_DELAY_MS = 5000;
/// <summary>
/// Number of keys to process per verification batch.
/// </summary>
public const int VERIFICATION_BATCH_SIZE = 10;
}
/// <summary>
/// Application-wide constants.
/// </summary>
public static class AppInfo
{
public const string Name = "UnsecuredAPIKeys Lite";
public const string Version = "1.0.0";
public const string FullVersionUrl = "www.UnsecuredAPIKeys.com";
public const string DatabaseName = "unsecuredapikeys.db";
}
================================================
FILE: UnsecuredAPIKeys.CLI/Program.cs
================================================
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Spectre.Console;
using UnsecuredAPIKeys.CLI;
using UnsecuredAPIKeys.CLI.Services;
using UnsecuredAPIKeys.Data;
// Initialize services
var services = new ServiceCollection();
services.AddLogging(builder => builder
.SetMinimumLevel(LogLevel.Warning)
.AddConsole());
services.AddHttpClient();
await using var serviceProvider = services.BuildServiceProvider();
var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
// Initialize database
var dbService = new DatabaseService(AppInfo.DatabaseName);
DBContext? dbContext = null;
try
{
dbContext = await dbService.InitializeDatabaseAsync();
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]Failed to initialize database: {Markup.Escape(ex.Message)}[/]");
return;
}
// Display banner
DisplayBanner();
// Main menu loop
var running = true;
while (running)
{
var choice = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("[yellow]What would you like to do?[/]")
.PageSize(10)
.AddChoices(new[]
{
"1. Start Scraper (search GitHub for keys)",
"2. Start Verifier (maintain valid keys)",
"3. View Status",
"4. Configure Settings",
"5. Export Keys",
"6. Exit"
}));
AnsiConsole.WriteLine();
switch (choice[0])
{
case '1':
await RunScraperAsync(dbContext, httpClientFactory);
break;
case '2':
await RunVerifierAsync(dbContext, httpClientFactory);
break;
case '3':
await ShowStatusAsync(dbContext, dbService);
break;
case '4':
await ConfigureSettingsAsync(dbContext, dbService);
break;
case '5':
await ExportKeysAsync(dbContext, dbService);
break;
case '6':
running = false;
break;
}
if (running)
{
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine("[dim]Press any key to continue...[/]");
Console.ReadKey(true);
AnsiConsole.Clear();
DisplayBanner();
}
}
AnsiConsole.MarkupLine("[green]Goodbye![/]");
dbContext?.Dispose();
// === Helper Methods ===
void DisplayBanner()
{
AnsiConsole.Write(
new FigletText(AppInfo.Name)
.LeftJustified()
.Color(Color.Cyan1));
AnsiConsole.Write(new Rule("[dim]Lite Version[/]").RuleStyle("grey").LeftJustified());
AnsiConsole.MarkupLine($"[dim]Full version: [link]{Markup.Escape(AppInfo.FullVersionUrl)}[/][/]");
AnsiConsole.MarkupLine($"[dim]Valid key limit: [yellow]{LiteLimits.MAX_VALID_KEYS}[/][/]");
AnsiConsole.WriteLine();
// Educational purpose notice
var warningPanel = new Panel(
"[yellow]This tool is for EDUCATIONAL PURPOSES ONLY.[/]\n\n" +
"If you discover exposed API keys, please help secure them:\n" +
" [green]1.[/] Open an issue on the repository to notify the owner\n" +
" [green]2.[/] Never use keys for unauthorized access\n" +
" [green]3.[/] Do NOT publish your results publicly\n\n" +
"[dim]Help make the internet more secure by reporting, not exploiting.[/]")
.Header("[yellow]Educational Use Only[/]")
.Border(BoxBorder.Rounded)
.BorderColor(Color.Yellow);
AnsiConsole.Write(warningPanel);
AnsiConsole.WriteLine();
}
async Task RunScraperAsync(DBContext db, IHttpClientFactory factory)
{
AnsiConsole.Write(new Rule("[cyan]GitHub Scraper[/]").RuleStyle("cyan"));
AnsiConsole.MarkupLine("[dim]Searches GitHub for exposed API keys. Runs continuously.[/]");
AnsiConsole.MarkupLine("[dim]Press [yellow]Ctrl+C[/] to stop.[/]");
AnsiConsole.WriteLine();
using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (s, e) =>
{
e.Cancel = true;
cts.Cancel();
AnsiConsole.MarkupLine("\n[yellow]Stopping scraper...[/]");
};
var scraper = new ScraperService(db, factory);
await scraper.RunAsync(cts.Token);
}
async Task RunVerifierAsync(DBContext db, IHttpClientFactory factory)
{
AnsiConsole.Write(new Rule("[green]Key Verifier[/]").RuleStyle("green"));
AnsiConsole.MarkupLine($"[dim]Maintains up to [yellow]{LiteLimits.MAX_VALID_KEYS}[/] valid keys.[/]");
AnsiConsole.MarkupLine("[dim]Re-checks valid keys and verifies new ones as needed.[/]");
AnsiConsole.MarkupLine("[dim]Press [yellow]Ctrl+C[/] to stop.[/]");
AnsiConsole.WriteLine();
using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (s, e) =>
{
e.Cancel = true;
cts.Cancel();
AnsiConsole.MarkupLine("\n[yellow]Stopping verifier...[/]");
};
var verifier = new VerifierService(db, factory);
await verifier.RunAsync(cts.Token);
}
async Task ShowStatusAsync(DBContext db, DatabaseService dbService)
{
AnsiConsole.Write(new Rule("[blue]Current Status[/]").RuleStyle("blue"));
var stats = await AnsiConsole.Status()
.Spinner(Spinner.Known.Dots)
.SpinnerStyle(Style.Parse("blue"))
.StartAsync("Loading statistics...", async ctx =>
{
return await dbService.GetStatisticsAsync(db);
});
// Create status table
var table = new Table()
.Border(TableBorder.Rounded)
.BorderColor(Color.Grey)
.AddColumn(new TableColumn("[bold]Metric[/]").LeftAligned())
.AddColumn(new TableColumn("[bold]Value[/]").RightAligned());
table.AddRow("Total Keys Found", stats.TotalKeys.ToString());
table.AddRow("Valid Keys", $"[green]{stats.ValidKeys}[/] / [yellow]{LiteLimits.MAX_VALID_KEYS}[/]");
table.AddRow("Valid (No Credits)", $"[yellow]{stats.ValidNoCreditsKeys}[/]");
table.AddRow("Invalid Keys", $"[red]{stats.InvalidKeys}[/]");
table.AddRow("Pending Verification", $"[blue]{stats.UnverifiedKeys}[/]");
table.AddRow(new Rule().RuleStyle("dim"));
table.AddRow("OpenAI Keys", stats.OpenAIKeys.ToString());
table.AddRow("Anthropic Keys", stats.AnthropicKeys.ToString());
table.AddRow("Google Keys", stats.GoogleKeys.ToString());
table.AddRow(new Rule().RuleStyle("dim"));
table.AddRow("Database", $"[dim]{Markup.Escape(AppInfo.DatabaseName)}[/]");
table.AddRow("GitHub Token", stats.HasGitHubToken ? "[green]Configured[/]" : "[red]Not configured[/]");
AnsiConsole.Write(table);
}
async Task ConfigureSettingsAsync(DBContext db, DatabaseService dbService)
{
AnsiConsole.Write(new Rule("[magenta]Configuration[/]").RuleStyle("magenta"));
var configChoice = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("[yellow]What would you like to configure?[/]")
.AddChoices(new[]
{
"1. Set GitHub Token",
"2. View Current Settings",
"3. Reset Database",
"4. Back to Main Menu"
}));
switch (configChoice[0])
{
case '1':
await SetGitHubTokenAsync(db, dbService);
break;
case '2':
await ShowCurrentSettingsAsync(db, dbService);
break;
case '3':
await ResetDatabaseAsync(dbService);
break;
}
}
async Task SetGitHubTokenAsync(DBContext db, DatabaseService dbService)
{
AnsiConsole.MarkupLine("[dim]Enter your GitHub Personal Access Token.[/]");
AnsiConsole.MarkupLine("[dim]Create one at: https:[[//]]github.com[[/]]settings[[/]]tokens[/]");
AnsiConsole.MarkupLine("[dim]Required scopes: [yellow]public_repo[/] (for searching public repos)[/]");
AnsiConsole.WriteLine();
var token = AnsiConsole.Prompt(
new TextPrompt<string>("[green]GitHub Token:[/]")
.Secret());
if (string.IsNullOrWhiteSpace(token))
{
AnsiConsole.MarkupLine("[red]Token cannot be empty.[/]");
return;
}
// Validate token format
if (!token.StartsWith("ghp_") && !token.StartsWith("github_pat_"))
{
var proceed = AnsiConsole.Confirm(
"[yellow]Token doesn't match expected GitHub token format. Save anyway?[/]",
false);
if (!proceed) return;
}
await dbService.SaveGitHubTokenAsync(db, token);
AnsiConsole.MarkupLine("[green]GitHub token saved successfully![/]");
}
async Task ShowCurrentSettingsAsync(DBContext db, DatabaseService dbService)
{
var stats = await dbService.GetStatisticsAsync(db);
var table = new Table()
.Border(TableBorder.Rounded)
.BorderColor(Color.Grey)
.AddColumn("[bold]Setting[/]")
.AddColumn("[bold]Value[/]");
var dbPath = Path.Combine(Environment.CurrentDirectory, AppInfo.DatabaseName);
table.AddRow("Database Path", Markup.Escape(dbPath));
table.AddRow("GitHub Token", stats.HasGitHubToken ? "[green]Configured[/]" : "[red]Not configured[/]");
table.AddRow("Max Valid Keys", LiteLimits.MAX_VALID_KEYS.ToString());
table.AddRow("Supported Providers", "OpenAI, Anthropic, Google");
AnsiConsole.Write(table);
}
async Task ResetDatabaseAsync(DatabaseService dbService)
{
var confirm = AnsiConsole.Confirm(
"[red]Are you sure you want to reset the database? All data will be lost![/]",
false);
if (!confirm)
{
AnsiConsole.MarkupLine("[dim]Database reset cancelled.[/]");
return;
}
var doubleConfirm = AnsiConsole.Confirm(
"[red]This action is irreversible. Are you absolutely sure?[/]",
false);
if (!doubleConfirm)
{
AnsiConsole.MarkupLine("[dim]Database reset cancelled.[/]");
return;
}
await AnsiConsole.Status()
.Spinner(Spinner.Known.Dots)
.SpinnerStyle(Style.Parse("red"))
.StartAsync("Resetting database...", async ctx =>
{
await dbService.ResetDatabaseAsync();
});
AnsiConsole.MarkupLine("[green]Database reset complete.[/]");
}
async Task ExportKeysAsync(DBContext db, DatabaseService dbService)
{
AnsiConsole.Write(new Rule("[yellow]Export Keys[/]").RuleStyle("yellow"));
var exportChoice = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("[yellow]Export format:[/]")
.AddChoices(new[]
{
"1. JSON",
"2. CSV",
"3. Back to Main Menu"
}));
if (exportChoice[0] == '3') return;
var validOnly = AnsiConsole.Confirm("Export only valid keys?", true);
var format = exportChoice[0] == '1' ? "json" : "csv";
var defaultFileName = exportChoice[0] == '1' ? "keys.json" : "keys.csv";
var fileName = AnsiConsole.Prompt(
new TextPrompt<string>("[green]Output file name:[/]")
.DefaultValue(defaultFileName));
await AnsiConsole.Status()
.Spinner(Spinner.Known.Dots)
.SpinnerStyle(Style.Parse("yellow"))
.StartAsync($"Exporting to {Markup.Escape(fileName)}...", async ctx =>
{
await dbService.ExportKeysAsync(db, fileName, validOnly, format);
});
AnsiConsole.MarkupLine($"[green]Exported to [bold]{Markup.Escape(fileName)}[/][/]");
}
================================================
FILE: UnsecuredAPIKeys.CLI/Services/DatabaseService.cs
================================================
using Microsoft.EntityFrameworkCore;
using Spectre.Console;
using UnsecuredAPIKeys.Data;
using UnsecuredAPIKeys.Data.Common;
using UnsecuredAPIKeys.Data.Models;
namespace UnsecuredAPIKeys.CLI.Services;
/// <summary>
/// Service for database initialization and common operations.
/// </summary>
public class DatabaseService(string dbPath = "unsecuredapikeys.db")
{
public async Task<DBContext> InitializeDatabaseAsync()
{
var dbContext = new DBContext(dbPath);
// Ensure database is created
await dbContext.Database.EnsureCreatedAsync();
// Seed default data if needed
await SeedDefaultDataAsync(dbContext);
return dbContext;
}
private async Task SeedDefaultDataAsync(DBContext dbContext)
{
// Add default search queries if none exist
if (!await dbContext.SearchQueries.AnyAsync())
{
var defaultQueries = new[]
{
// OpenAI patterns
"sk-proj-",
"sk-or-v1-",
"sk-",
"OPENAI_API_KEY",
"openai.api_key",
"chatgpt api key",
"gpt-4 api key",
// Anthropic patterns
"sk-ant-api",
"ANTHROPIC_API_KEY",
"anthropic_api_key",
"claude api key",
// Google AI patterns
"AIzaSy",
"GOOGLE_API_KEY",
"gemini_api_key",
// Other AI providers (patterns only, validation limited to lite providers)
"r8_", // Replicate
"fw_", // Fireworks
"hf_", // HuggingFace
"AI_API_KEY" // Generic
};
foreach (var query in defaultQueries)
{
dbContext.SearchQueries.Add(new SearchQuery
{
Query = query,
IsEnabled = true,
LastSearchUTC = DateTime.UtcNow.AddDays(-1)
});
}
await dbContext.SaveChangesAsync();
AnsiConsole.MarkupLine($"[dim]Added {defaultQueries.Length} default search queries.[/]");
}
}
public async Task<Statistics> GetStatisticsAsync(DBContext dbContext)
{
var stats = new Statistics
{
TotalKeys = await dbContext.APIKeys.CountAsync(),
ValidKeys = await dbContext.APIKeys.CountAsync(k => k.Status == ApiStatusEnum.Valid),
InvalidKeys = await dbContext.APIKeys.CountAsync(k => k.Status == ApiStatusEnum.Invalid),
UnverifiedKeys = await dbContext.APIKeys.CountAsync(k => k.Status == ApiStatusEnum.Unverified),
ValidNoCreditsKeys = await dbContext.APIKeys.CountAsync(k => k.Status == ApiStatusEnum.ValidNoCredits),
OpenAIKeys = await dbContext.APIKeys.CountAsync(k => k.ApiType == ApiTypeEnum.OpenAI),
AnthropicKeys = await dbContext.APIKeys.CountAsync(k => k.ApiType == ApiTypeEnum.AnthropicClaude),
GoogleKeys = await dbContext.APIKeys.CountAsync(k => k.ApiType == ApiTypeEnum.GoogleAI),
HasGitHubToken = await dbContext.SearchProviderTokens
.AnyAsync(t => t.IsEnabled && t.SearchProvider == SearchProviderEnum.GitHub)
};
return stats;
}
public async Task SaveGitHubTokenAsync(DBContext dbContext, string token)
{
var existing = await dbContext.SearchProviderTokens
.FirstOrDefaultAsync(t => t.SearchProvider == SearchProviderEnum.GitHub);
if (existing != null)
{
existing.Token = token;
existing.IsEnabled = true;
}
else
{
dbContext.SearchProviderTokens.Add(new SearchProviderToken
{
Token = token,
SearchProvider = SearchProviderEnum.GitHub,
IsEnabled = true
});
}
await dbContext.SaveChangesAsync();
}
public async Task ResetDatabaseAsync()
{
if (File.Exists(dbPath))
{
File.Delete(dbPath);
}
// Reinitialize
await InitializeDatabaseAsync();
}
public async Task ExportKeysAsync(DBContext dbContext, string filePath, bool validOnly, string format)
{
var query = dbContext.APIKeys.AsQueryable();
if (validOnly)
{
query = query.Where(k => k.Status == ApiStatusEnum.Valid || k.Status == ApiStatusEnum.ValidNoCredits);
}
var keys = await query
.Include(k => k.References)
.ToListAsync();
if (format.ToLower() == "json")
{
await ExportAsJsonAsync(keys, filePath);
}
else
{
await ExportAsCsvAsync(keys, filePath);
}
}
private async Task ExportAsJsonAsync(List<APIKey> keys, string filePath)
{
var exportData = keys.Select(k => new
{
k.Id,
k.ApiKey,
Type = k.ApiType.ToString(),
Status = k.Status.ToString(),
k.FirstFoundUTC,
k.LastCheckedUTC,
Sources = k.References.Select(r => new
{
r.RepoURL,
r.RepoOwner,
r.RepoName,
r.FilePath,
r.FoundUTC
})
});
var json = System.Text.Json.JsonSerializer.Serialize(exportData, new System.Text.Json.JsonSerializerOptions
{
WriteIndented = true
});
await File.WriteAllTextAsync(filePath, json);
}
private async Task ExportAsCsvAsync(List<APIKey> keys, string filePath)
{
var lines = new List<string>
{
"Id,ApiKey,Type,Status,FirstFoundUTC,LastCheckedUTC,RepoURL"
};
foreach (var key in keys)
{
var repoUrl = key.References.FirstOrDefault()?.RepoURL ?? "";
lines.Add($"{key.Id},\"{key.ApiKey}\",{key.ApiType},{key.Status},{key.FirstFoundUTC:O},{key.LastCheckedUTC:O},\"{repoUrl}\"");
}
await File.WriteAllLinesAsync(filePath, lines);
}
}
public class Statistics
{
public int TotalKeys { get; set; }
public int ValidKeys { get; set; }
public int InvalidKeys { get; set; }
public int UnverifiedKeys { get; set; }
public int ValidNoCreditsKeys { get; set; }
public int OpenAIKeys { get; set; }
public int AnthropicKeys { get; set; }
public int GoogleKeys { get; set; }
public bool HasGitHubToken { get; set; }
}
================================================
FILE: UnsecuredAPIKeys.CLI/Services/ScraperService.cs
================================================
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Spectre.Console;
using UnsecuredAPIKeys.Data;
using UnsecuredAPIKeys.Data.Common;
using UnsecuredAPIKeys.Data.Models;
using UnsecuredAPIKeys.Providers;
using UnsecuredAPIKeys.Providers._Interfaces;
using UnsecuredAPIKeys.Providers.Search_Providers;
namespace UnsecuredAPIKeys.CLI.Services;
/// <summary>
/// Scraper service for finding API keys on GitHub.
/// Lite version: GitHub only, 3 AI providers.
/// Full version: www.UnsecuredAPIKeys.com
/// </summary>
public class ScraperService
{
private readonly DBContext _dbContext;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<ScraperService>? _logger;
private readonly IReadOnlyList<IApiKeyProvider> _providers;
private CancellationTokenSource? _cancellationTokenSource;
private int _newKeysFound;
private int _duplicateKeysFound;
public ScraperService(DBContext dbContext, IHttpClientFactory httpClientFactory, ILogger<ScraperService>? logger = null)
{
_dbContext = dbContext;
_httpClientFactory = httpClientFactory;
_logger = logger;
_providers = ApiProviderRegistry.ScraperProviders;
}
public async Task RunAsync(CancellationToken cancellationToken)
{
_cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
AnsiConsole.MarkupLine("[cyan]Starting GitHub scraper...[/]");
AnsiConsole.MarkupLine($"[dim]Loaded {_providers.Count} API key providers[/]");
foreach (var provider in _providers)
{
AnsiConsole.MarkupLine($" [dim]- {Markup.Escape(provider.ProviderName)}[/]");
}
// Get GitHub token
var token = await _dbContext.SearchProviderTokens
.Where(t => t.IsEnabled && t.SearchProvider == SearchProviderEnum.GitHub)
.FirstOrDefaultAsync(cancellationToken);
if (token == null)
{
AnsiConsole.MarkupLine("[red]No GitHub token configured. Use 'Configure Settings' to add one.[/]");
return;
}
// Run continuously
while (!_cancellationTokenSource.Token.IsCancellationRequested)
{
try
{
await RunScrapingCycleAsync(token);
if (_cancellationTokenSource.Token.IsCancellationRequested)
break;
// Wait before next cycle
AnsiConsole.MarkupLine($"[dim]Waiting {LiteLimits.SEARCH_DELAY_MS / 1000}s before next search...[/]");
await Task.Delay(LiteLimits.SEARCH_DELAY_MS, _cancellationTokenSource.Token);
// Reset counters
_newKeysFound = 0;
_duplicateKeysFound = 0;
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]Error during scraping: {Markup.Escape(ex.Message)}[/]");
_logger?.LogError(ex, "Scraping cycle error");
await Task.Delay(5000, _cancellationTokenSource.Token);
}
}
AnsiConsole.MarkupLine("[green]Scraper stopped.[/]");
}
private async Task RunScrapingCycleAsync(SearchProviderToken token)
{
// Get next query to process
var query = await _dbContext.SearchQueries
.Where(x => x.IsEnabled && x.LastSearchUTC < DateTime.UtcNow.AddHours(-1))
.OrderBy(x => x.LastSearchUTC)
.FirstOrDefaultAsync(_cancellationTokenSource!.Token);
if (query == null)
{
AnsiConsole.MarkupLine("[dim]No queries due for search. Waiting...[/]");
return;
}
AnsiConsole.MarkupLine($"[cyan]Searching: {Markup.Escape(query.Query)}[/]");
// Update last search time
query.LastSearchUTC = DateTime.UtcNow;
await _dbContext.SaveChangesAsync(_cancellationTokenSource.Token);
// Search GitHub
var searchProvider = new GitHubSearchProvider(_dbContext);
IEnumerable<RepoReference>? results;
try
{
results = await searchProvider.SearchAsync(query, token);
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]Search error: {Markup.Escape(ex.Message)}[/]");
return;
}
if (results == null)
{
AnsiConsole.MarkupLine("[yellow]No results from search.[/]");
return;
}
var resultsList = results.ToList();
AnsiConsole.MarkupLine($"[dim]Found {resultsList.Count} potential matches[/]");
// Process each result
await AnsiConsole.Progress()
.StartAsync(async ctx =>
{
var task = ctx.AddTask($"[cyan]Processing results[/]", maxValue: resultsList.Count);
foreach (var repoRef in resultsList)
{
if (_cancellationTokenSource!.Token.IsCancellationRequested)
break;
await ProcessResultAsync(repoRef, token, query);
task.Increment(1);
}
});
// Summary
var table = new Table()
.Border(TableBorder.Rounded)
.AddColumn("[bold]Metric[/]")
.AddColumn("[bold]Value[/]");
table.AddRow("Query", Markup.Escape(query.Query));
table.AddRow("Results Processed", resultsList.Count.ToString());
table.AddRow("New Keys", $"[green]{_newKeysFound}[/]");
table.AddRow("Duplicates", $"[dim]{_duplicateKeysFound}[/]");
AnsiConsole.Write(table);
}
private async Task ProcessResultAsync(RepoReference repoRef, SearchProviderToken token, SearchQuery query)
{
try
{
// Get file content
var content = await FetchFileContentAsync(repoRef, token);
if (string.IsNullOrEmpty(content))
return;
// Search for API keys using all provider patterns
foreach (var provider in _providers)
{
foreach (var pattern in provider.RegexPatterns)
{
var regex = new System.Text.RegularExpressions.Regex(pattern);
var matches = regex.Matches(content);
foreach (System.Text.RegularExpressions.Match match in matches)
{
var apiKey = match.Value;
// Check if already exists
var exists = await _dbContext.APIKeys
.AnyAsync(k => k.ApiKey == apiKey, _cancellationTokenSource!.Token);
if (exists)
{
Interlocked.Increment(ref _duplicateKeysFound);
continue;
}
// Add new key
var newKey = new APIKey
{
ApiKey = apiKey,
ApiType = provider.ApiType,
Status = ApiStatusEnum.Unverified,
SearchProvider = SearchProviderEnum.GitHub,
FirstFoundUTC = DateTime.UtcNow,
LastFoundUTC = DateTime.UtcNow
};
// Add repo reference
repoRef.SearchQueryId = query.Id;
repoRef.FoundUTC = DateTime.UtcNow;
repoRef.Provider = "GitHub";
newKey.References.Add(repoRef);
_dbContext.APIKeys.Add(newKey);
await _dbContext.SaveChangesAsync(_cancellationTokenSource!.Token);
Interlocked.Increment(ref _newKeysFound);
AnsiConsole.MarkupLine($"[green]+ New {Markup.Escape(provider.ProviderName)} key found![/]");
}
}
}
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "Error processing result: {Url}", repoRef.FileURL);
}
}
private async Task<string?> FetchFileContentAsync(RepoReference repoRef, SearchProviderToken token)
{
try
{
using var client = _httpClientFactory.CreateClient();
client.DefaultRequestHeaders.UserAgent.ParseAdd("UnsecuredAPIKeys-Lite/1.0");
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token.Token);
// Build raw content URL from repo info
// Format: https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path}
string? url = null;
if (!string.IsNullOrEmpty(repoRef.RepoOwner) &&
!string.IsNullOrEmpty(repoRef.RepoName) &&
!string.IsNullOrEmpty(repoRef.FilePath))
{
var branch = repoRef.Branch ?? "main";
url = $"https://raw.githubusercontent.com/{repoRef.RepoOwner}/{repoRef.RepoName}/{branch}/{repoRef.FilePath}";
}
if (string.IsNullOrEmpty(url))
return null;
var response = await client.GetAsync(url, _cancellationTokenSource!.Token);
// Try 'master' if 'main' fails
if (!response.IsSuccessStatusCode && repoRef.Branch == null)
{
url = $"https://raw.githubusercontent.com/{repoRef.RepoOwner}/{repoRef.RepoName}/master/{repoRef.FilePath}";
response = await client.GetAsync(url, _cancellationTokenSource!.Token);
}
if (!response.IsSuccessStatusCode)
return null;
return await response.Content.ReadAsStringAsync(_cancellationTokenSource.Token);
}
catch
{
return null;
}
}
}
================================================
FILE: UnsecuredAPIKeys.CLI/Services/VerifierService.cs
================================================
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Spectre.Console;
using UnsecuredAPIKeys.Data;
using UnsecuredAPIKeys.Data.Common;
using UnsecuredAPIKeys.Data.Models;
using UnsecuredAPIKeys.Providers;
using UnsecuredAPIKeys.Providers._Interfaces;
namespace UnsecuredAPIKeys.CLI.Services;
/// <summary>
/// Verifier service that maintains up to 50 valid API keys.
/// When a key becomes invalid, verifies new keys to maintain the limit.
/// Lite version: 50 key cap.
/// Full version: www.UnsecuredAPIKeys.com
/// </summary>
public class VerifierService(
DBContext dbContext,
IHttpClientFactory httpClientFactory,
ILogger<VerifierService>? logger = null)
{
private readonly IReadOnlyList<IApiKeyProvider> _providers = ApiProviderRegistry.VerifierProviders;
private CancellationTokenSource? _cancellationTokenSource;
private int _validCount;
private int _invalidCount;
private int _verifiedCount;
public async Task RunAsync(CancellationToken cancellationToken)
{
_cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
AnsiConsole.MarkupLine("[green]Starting verifier service...[/]");
AnsiConsole.MarkupLine($"[dim]Target valid keys: [yellow]{LiteLimits.MAX_VALID_KEYS}[/][/]");
AnsiConsole.MarkupLine($"[dim]Loaded {_providers.Count} verification providers[/]");
foreach (var provider in _providers)
{
AnsiConsole.MarkupLine($" [dim]- {Markup.Escape(provider.ProviderName)}[/]");
}
// Run continuously
while (!_cancellationTokenSource.Token.IsCancellationRequested)
{
try
{
await RunVerificationCycleAsync();
if (_cancellationTokenSource.Token.IsCancellationRequested)
break;
// Wait before next cycle
AnsiConsole.MarkupLine($"[dim]Waiting {LiteLimits.VERIFICATION_DELAY_MS / 1000}s before next verification cycle...[/]");
await Task.Delay(LiteLimits.VERIFICATION_DELAY_MS, _cancellationTokenSource.Token);
// Reset counters
_validCount = 0;
_invalidCount = 0;
_verifiedCount = 0;
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]Error during verification: {Markup.Escape(ex.Message)}[/]");
logger?.LogError(ex, "Verification cycle error");
await Task.Delay(5000, _cancellationTokenSource.Token);
}
}
AnsiConsole.MarkupLine("[green]Verifier stopped.[/]");
}
private async Task RunVerificationCycleAsync()
{
// Count current valid keys
var currentValidCount = await dbContext.APIKeys
.CountAsync(k => k.Status == ApiStatusEnum.Valid, _cancellationTokenSource!.Token);
AnsiConsole.MarkupLine($"[dim]Current valid keys: [yellow]{currentValidCount}[/] / [yellow]{LiteLimits.MAX_VALID_KEYS}[/][/]");
if (currentValidCount >= LiteLimits.MAX_VALID_KEYS)
{
// Re-verify existing valid keys to ensure they're still valid
await ReVerifyExistingKeysAsync();
}
else
{
// Verify unverified keys until we reach the limit
await VerifyNewKeysAsync(LiteLimits.MAX_VALID_KEYS - currentValidCount);
}
// Display summary
var table = new Table()
.Border(TableBorder.Rounded)
.AddColumn("[bold]Metric[/]")
.AddColumn("[bold]Value[/]");
table.AddRow("Keys Verified", _verifiedCount.ToString());
table.AddRow("Now Valid", $"[green]{_validCount}[/]");
table.AddRow("Now Invalid", $"[red]{_invalidCount}[/]");
var newValidCount = await dbContext.APIKeys
.CountAsync(k => k.Status == ApiStatusEnum.Valid, _cancellationTokenSource!.Token);
table.AddRow("Total Valid", $"[yellow]{newValidCount}[/] / [yellow]{LiteLimits.MAX_VALID_KEYS}[/]");
AnsiConsole.Write(table);
}
private async Task ReVerifyExistingKeysAsync()
{
AnsiConsole.MarkupLine("[dim]Re-verifying existing valid keys...[/]");
// Get oldest verified keys first
var keysToReVerify = await dbContext.APIKeys
.Where(k => k.Status == ApiStatusEnum.Valid)
.OrderBy(k => k.LastCheckedUTC)
.Take(LiteLimits.VERIFICATION_BATCH_SIZE)
.ToListAsync(_cancellationTokenSource!.Token);
await AnsiConsole.Progress()
.StartAsync(async ctx =>
{
var task = ctx.AddTask("[green]Re-verifying keys[/]", maxValue: keysToReVerify.Count);
foreach (var key in keysToReVerify)
{
if (_cancellationTokenSource.Token.IsCancellationRequested)
break;
await VerifyKeyAsync(key);
task.Increment(1);
}
});
await dbContext.SaveChangesAsync(_cancellationTokenSource.Token);
}
private async Task VerifyNewKeysAsync(int neededCount)
{
AnsiConsole.MarkupLine($"[dim]Verifying unverified keys (need {neededCount} more valid)...[/]");
// Get unverified keys
var keysToVerify = await dbContext.APIKeys
.Where(k => k.Status == ApiStatusEnum.Unverified)
.OrderBy(k => k.FirstFoundUTC)
.Take(Math.Max(neededCount * 2, LiteLimits.VERIFICATION_BATCH_SIZE))
.ToListAsync(_cancellationTokenSource!.Token);
if (keysToVerify.Count == 0)
{
AnsiConsole.MarkupLine("[yellow]No unverified keys available.[/]");
return;
}
await AnsiConsole.Progress()
.StartAsync(async ctx =>
{
var task = ctx.AddTask("[green]Verifying new keys[/]", maxValue: keysToVerify.Count);
var validFound = 0;
foreach (var key in keysToVerify)
{
if (_cancellationTokenSource.Token.IsCancellationRequested)
break;
// Stop if we've reached our target
if (validFound >= neededCount)
{
task.Value = keysToVerify.Count;
break;
}
var wasValid = await VerifyKeyAsync(key);
if (wasValid)
validFound++;
task.Increment(1);
}
});
await dbContext.SaveChangesAsync(_cancellationTokenSource.Token);
}
private async Task<bool> VerifyKeyAsync(APIKey key)
{
Interlocked.Increment(ref _verifiedCount);
// Build list of providers to try, starting with the assigned one
var providersToTry = GetProvidersToTry(key);
if (providersToTry.Count == 0)
{
key.Status = ApiStatusEnum.Error;
key.LastCheckedUTC = DateTime.UtcNow;
AnsiConsole.MarkupLine($"[yellow]No matching providers for key[/]");
return false;
}
// Try each matching provider until one succeeds
foreach (var provider in providersToTry)
{
try
{
var result = await provider.ValidateKeyAsync(key.ApiKey, httpClientFactory);
key.LastCheckedUTC = DateTime.UtcNow;
switch (result.Status)
{
case Providers.Common.ValidationAttemptStatus.Valid:
// Update the key's API type if a different provider validated it
if (key.ApiType != provider.ApiType)
{
AnsiConsole.MarkupLine($"[dim]Reclassified from {key.ApiType} to {provider.ApiType}[/]");
key.ApiType = provider.ApiType;
}
key.Status = ApiStatusEnum.Valid;
key.ErrorCount = 0;
Interlocked.Increment(ref _validCount);
AnsiConsole.MarkupLine($"[green]Valid: {Markup.Escape(provider.ProviderName)} key[/]");
return true;
case Providers.Common.ValidationAttemptStatus.HttpError:
// Check if it's a quota/credits issue based on detail
if (result.Detail?.Contains("quota", StringComparison.OrdinalIgnoreCase) == true ||
result.Detail?.Contains("credit", StringComparison.OrdinalIgnoreCase) == true ||
result.Detail?.Contains("billing", StringComparison.OrdinalIgnoreCase) == true)
{
// Update the key's API type if a different provider validated it
if (key.ApiType != provider.ApiType)
{
AnsiConsole.MarkupLine($"[dim]Reclassified from {key.ApiType} to {provider.ApiType}[/]");
key.ApiType = provider.ApiType;
}
key.Status = ApiStatusEnum.ValidNoCredits;
key.ErrorCount = 0;
Interlocked.Increment(ref _validCount);
AnsiConsole.MarkupLine($"[yellow]Valid [[no credits]]: {Markup.Escape(provider.ProviderName)} key[/]");
return true;
}
// HTTP error but not quota - try next provider
continue;
case Providers.Common.ValidationAttemptStatus.Unauthorized:
// This provider explicitly rejected it - try next provider
continue;
case Providers.Common.ValidationAttemptStatus.NetworkError:
// Network error - don't try other providers, just increment error count
key.ErrorCount++;
if (key.ErrorCount >= 3)
{
key.Status = ApiStatusEnum.Error;
}
return false;
default:
// Provider-specific error - try next provider
continue;
}
}
catch (Exception ex)
{
logger?.LogWarning(ex, "Error verifying key {KeyId} with provider {Provider}", key.Id, provider.ProviderName);
// Continue to next provider on exception
continue;
}
}
// All providers failed - mark as invalid
key.Status = ApiStatusEnum.Invalid;
Interlocked.Increment(ref _invalidCount);
return false;
}
/// <summary>
/// Gets providers to try for a key, ordered by: assigned provider first, then other matching providers.
/// </summary>
private List<IApiKeyProvider> GetProvidersToTry(APIKey key)
{
var result = new List<IApiKeyProvider>();
// First, add the assigned provider (if it exists)
var assignedProvider = _providers.FirstOrDefault(p => p.ApiType == key.ApiType);
if (assignedProvider != null)
{
result.Add(assignedProvider);
}
// Then add other providers whose patterns match this key
foreach (var provider in _providers)
{
// Skip the already-added assigned provider
if (provider.ApiType == key.ApiType)
continue;
// Check if any of this provider's patterns match the key
foreach (var pattern in provider.RegexPatterns)
{
try
{
var regex = new System.Text.RegularExpressions.Regex(pattern);
if (regex.IsMatch(key.ApiKey))
{
result.Add(provider);
break; // One match is enough for this provider
}
}
catch
{
// Invalid regex pattern - skip it
}
}
}
return result;
}
}
================================================
FILE: UnsecuredAPIKeys.CLI/UnsecuredAPIKeys.CLI.csproj
================================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>unsecuredapikeys</AssemblyName>
<RootNamespace>UnsecuredAPIKeys.CLI</RootNamespace>
<Product>UnsecuredAPIKeys - Open Source</Product>
<Authors>https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/</Authors>
<Company>https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/</Company>
<PackageProjectUrl>https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/</PackageProjectUrl>
<RepositoryUrl>https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild>
<AnalysisLevel>latest-recommended</AnalysisLevel>
<Version>1.0.0</Version>
<!-- Single-file publishing settings -->
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Spectre.Console" Version="0.54.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\UnsecuredAPIKeys.Data\UnsecuredAPIKeys.Data.csproj" />
<ProjectReference Include="..\UnsecuredAPIKeys.Providers\UnsecuredAPIKeys.Providers.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.example.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
================================================
FILE: UnsecuredAPIKeys.CLI/appsettings.example.json
================================================
{
"// README": "Copy this file to appsettings.json and configure your GitHub token",
"// IMPORTANT": "Do NOT commit appsettings.json to version control",
"GitHub": {
"Token": "ghp_YOUR_GITHUB_TOKEN_HERE",
"// TokenInfo": "Create at https://github.com/settings/tokens - requires 'public_repo' scope"
},
"Database": {
"Path": "unsecuredapikeys.db",
"// PathInfo": "SQLite database file path. Can be absolute or relative."
},
"Limits": {
"MaxValidKeys": 50,
"// MaxValidKeysInfo": "Lite version limit. For higher limits, use www.UnsecuredAPIKeys.com"
}
}
================================================
FILE: UnsecuredAPIKeys.Data/Common/ApiProviderAttribute.cs
================================================
namespace UnsecuredAPIKeys.Data.Common;
[AttributeUsage(AttributeTargets.Class)]
public class ApiProviderAttribute : Attribute
{
/// <summary>
/// Whether this provider should be used by the Scraper bot
/// </summary>
public bool ScraperUse { get; set; } = true;
/// <summary>
/// Whether this provider should be used by the Verifier bot
/// </summary>
public bool VerificationUse { get; set; } = true;
/// <summary>
/// Creates an ApiProvider attribute with default usage (enabled for both scraper and verifier)
/// </summary>
public ApiProviderAttribute()
{
}
/// <summary>
/// Creates an ApiProvider attribute with specific usage flags
/// </summary>
/// <param name="scraperUse">Enable for scraper bot</param>
/// <param name="verificationUse">Enable for verifier bot</param>
public ApiProviderAttribute(bool scraperUse, bool verificationUse)
{
ScraperUse = scraperUse;
VerificationUse = verificationUse;
}
}
================================================
FILE: UnsecuredAPIKeys.Data/Common/CommonEnums.cs
================================================
namespace UnsecuredAPIKeys.Data.Common
{
/// <summary>
/// Search provider for finding API keys.
/// Lite version: GitHub only.
/// Full version: www.UnsecuredAPIKeys.com
/// </summary>
public enum SearchProviderEnum
{
Unknown = -99,
GitHub = 1
}
/// <summary>
/// Status of an API key in the system.
/// </summary>
public enum ApiStatusEnum
{
/// <summary>The key was found but not yet checked for validity.</summary>
Unverified = -99,
/// <summary>The key was checked and is valid/working.</summary>
Valid = 1,
/// <summary>The key was checked and is not working (invalid, expired, revoked, etc.).</summary>
Invalid = 0,
/// <summary>The key is valid but has no credits/quota.</summary>
ValidNoCredits = 7,
/// <summary>The key was checked and is erroring out for some reason.</summary>
Error = 6
}
/// <summary>
/// Type of API provider.
/// Lite version: OpenAI, Anthropic, Google only.
/// Full version with all providers: www.UnsecuredAPIKeys.com
/// </summary>
public enum ApiTypeEnum
{
Unknown = -99,
// AI Services - Lite version only supports these 3
OpenAI = 100,
AnthropicClaude = 120,
GoogleAI = 130
}
}
================================================
FILE: UnsecuredAPIKeys.Data/DBContext.cs
================================================
using Microsoft.EntityFrameworkCore;
using UnsecuredAPIKeys.Data.Models;
namespace UnsecuredAPIKeys.Data
{
/// <summary>
/// SQLite database context for UnsecuredAPIKeys Lite.
/// Full version with PostgreSQL: www.UnsecuredAPIKeys.com
/// </summary>
public class DBContext : DbContext
{
private readonly string _dbPath;
public DBContext(DbContextOptions<DBContext> options) : base(options)
{
_dbPath = "unsecuredapikeys.db";
}
public DBContext(string dbPath = "unsecuredapikeys.db")
{
_dbPath = dbPath;
}
// Core entities
public DbSet<APIKey> APIKeys { get; set; } = null!;
public DbSet<RepoReference> RepoReferences { get; set; } = null!;
public DbSet<SearchQuery> SearchQueries { get; set; } = null!;
public DbSet<SearchProviderToken> SearchProviderTokens { get; set; } = null!;
public DbSet<ApplicationSetting> ApplicationSettings { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseSqlite($"Data Source={_dbPath}");
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// APIKey indexes for performance
modelBuilder.Entity<APIKey>()
.HasIndex(k => k.ApiKey)
.IsUnique()
.HasDatabaseName("IX_APIKeys_ApiKey");
modelBuilder.Entity<APIKey>()
.HasIndex(k => new { k.Status, k.ApiType })
.HasDatabaseName("IX_APIKeys_Status_ApiType");
modelBuilder.Entity<APIKey>()
.HasIndex(k => k.LastCheckedUTC)
.HasDatabaseName("IX_APIKeys_LastCheckedUTC");
modelBuilder.Entity<APIKey>()
.HasIndex(k => k.Status)
.HasDatabaseName("IX_APIKeys_Status");
// RepoReference indexes
modelBuilder.Entity<RepoReference>()
.HasIndex(r => r.APIKeyId)
.HasDatabaseName("IX_RepoReferences_ApiKeyId");
// SearchQuery indexes
modelBuilder.Entity<SearchQuery>()
.HasIndex(q => new { q.IsEnabled, q.LastSearchUTC })
.HasDatabaseName("IX_SearchQueries_IsEnabled_LastSearchUTC");
// SearchProviderToken indexes
modelBuilder.Entity<SearchProviderToken>()
.HasIndex(t => t.SearchProvider)
.HasDatabaseName("IX_SearchProviderTokens_SearchProvider");
// Relationships
modelBuilder.Entity<RepoReference>()
.HasOne(r => r.APIKey)
.WithMany(k => k.References)
.HasForeignKey(r => r.APIKeyId)
.OnDelete(DeleteBehavior.Cascade);
}
}
}
================================================
FILE: UnsecuredAPIKeys.Data/DesignTimeDbContextFactory.cs
================================================
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace UnsecuredAPIKeys.Data
{
/// <summary>
/// Factory for creating DBContext during EF Core design-time operations (migrations).
/// Uses SQLite for the lite version.
/// </summary>
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<DBContext>
{
public DBContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<DBContext>();
// Use SQLite for the lite version
optionsBuilder.UseSqlite("Data Source=unsecuredapikeys.db");
return new DBContext(optionsBuilder.Options);
}
}
}
================================================
FILE: UnsecuredAPIKeys.Data/Models/APIKey.cs
================================================
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using UnsecuredAPIKeys.Data.Common;
using System.Text.Json.Serialization; // <-- Add this using directive
namespace UnsecuredAPIKeys.Data.Models
{
public class APIKey
{
[Key]
public long Id { get; set; }
[Required]
public required string ApiKey { get; set; }
[JsonConverter(typeof(JsonStringEnumConverter))]
public ApiStatusEnum Status { get; set; }
[JsonConverter(typeof(JsonStringEnumConverter))]
public ApiTypeEnum ApiType { get; set; } = ApiTypeEnum.Unknown;
public SearchProviderEnum SearchProvider { get; set; }
public DateTime? LastCheckedUTC { get; set; }
public DateTime FirstFoundUTC { get; set; }
public DateTime LastFoundUTC { get; set; }
public int TimesDisplayed { get; set; }
// Error tracking for verification failures
public int ErrorCount { get; set; } = 0;
// Navigation property to where this key was found
public virtual ICollection<RepoReference> References { get; set; } = [];
}
}
================================================
FILE: UnsecuredAPIKeys.Data/Models/ApplicationSetting.cs
================================================
using System.ComponentModel.DataAnnotations;
namespace UnsecuredAPIKeys.Data.Models
{
public class ApplicationSetting
{
[Key] public required string Key { get; init; }
public required string Value { get; init; }
public string? Description { get; init; }
}
}
================================================
FILE: UnsecuredAPIKeys.Data/Models/RepoReference.cs
================================================
using System.ComponentModel.DataAnnotations;
namespace UnsecuredAPIKeys.Data.Models
{
public class RepoReference
{
[Key]
public long Id { get; set; }
public long APIKeyId { get; set; } // Foreign key to APIKey
public virtual APIKey? APIKey { get; set; } // Navigation property
// Repository information
[Required]
public string? RepoURL { get; set; } // Just base repo URL
public string? RepoOwner { get; set; } // Owner username/organization
public string? RepoName { get; set; } // Repository name
public string? RepoDescription { get; set; }
public long RepoId { get; set; } // GitHub's repo ID
// File information
[Required]
public string? FileURL { get; set; } // Full path with commit hash
public string? FileName { get; set; }
public string? FilePath { get; set; } // Path within repo
public string? FileSHA { get; set; }
public string? ApiContentUrl { get; set; } // URL to fetch raw content via API
// Context information
public string? CodeContext { get; set; } // Surrounding code
public int LineNumber { get; set; }
// Discovery metadata
public long SearchQueryId { get; set; } // Which query found this
public DateTime FoundUTC { get; set; }
public string? Provider { get; set; } // e.g., GitHub, GitLab
public string? Branch { get; set; } // Branch where the file was found
}
}
================================================
FILE: UnsecuredAPIKeys.Data/Models/SearchProviderToken.cs
================================================
using System.ComponentModel.DataAnnotations;
using UnsecuredAPIKeys.Data.Common;
namespace UnsecuredAPIKeys.Data.Models
{
public class SearchProviderToken
{
[Key] public int Id { get; set; }
public string Token { get; set; } = string.Empty;
public SearchProviderEnum SearchProvider { get; set; } = SearchProviderEnum.Unknown;
public bool IsEnabled { get; set; }
// Make it nullable so we can identify never-used tokens
public DateTime? LastUsedUTC { get; set; }
}
}
================================================
FILE: UnsecuredAPIKeys.Data/Models/SearchQuery.cs
================================================
using System.ComponentModel.DataAnnotations;
namespace UnsecuredAPIKeys.Data.Models
{
public class SearchQuery
{
[Key] public long Id { get; set; }
public string Query { get; set; } = string.Empty;
public bool IsEnabled { get; set; }
public int SearchResultsCount { get; set; }
public DateTime LastSearchUTC { get; set; }
}
}
================================================
FILE: UnsecuredAPIKeys.Data/UnsecuredAPIKeys.Data.csproj
================================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
================================================
FILE: UnsecuredAPIKeys.Providers/AI Providers/AnthropicProvider.cs
================================================
using System.Net;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using UnsecuredAPIKeys.Data.Common;
using UnsecuredAPIKeys.Providers._Base;
using UnsecuredAPIKeys.Providers.Common;
namespace UnsecuredAPIKeys.Providers.AI_Providers
{
/// <summary>
/// Provider implementation for handling Anthropic (Claude) API keys with enhanced validation.
/// </summary>
[ApiProvider]
public class AnthropicProvider : BaseApiKeyProvider
{
private const string API_ENDPOINT = "https://api.anthropic.com/v1/messages";
private const string ANTHROPIC_VERSION = "2023-06-01";
private const string DEFAULT_MODEL = "claude-sonnet-4-20250514";
private const int MAX_RETRIES = 3;
private const int TIMEOUT_SECONDS = 30;
// Anthropic-specific response keywords (additional to base class)
private static readonly HashSet<string> InvalidKeyIndicators = new(StringComparer.OrdinalIgnoreCase)
{
"invalid_api_key",
"authentication_error",
"invalid x-api-key",
"unauthorized"
};
public override string ProviderName => "Anthropic";
public override ApiTypeEnum ApiType => ApiTypeEnum.AnthropicClaude;
// Enhanced regex patterns with compiled regex for better performance
public override IEnumerable<string> RegexPatterns =>
[
@"sk-ant-api\d{0,2}-[a-zA-Z0-9\-_]{40,120}",
@"sk-ant-[a-zA-Z0-9\-_]{40,95}",
@"sk-ant-v\d+-[a-zA-Z0-9\-_]{40,95}",
@"sk-ant-[a-zA-Z0-9]+-[a-zA-Z0-9\-_]{20,120}",
@"sk-ant-[a-zA-Z0-9]{40,64}",
@"\bsk-ant-[a-zA-Z0-9\-_]{20,120}\b"
];
public AnthropicProvider() : base()
{
}
public AnthropicProvider(ILogger<AnthropicProvider>? logger) : base(logger)
{
}
protected override async Task<ValidationResult> ValidateKeyWithHttpClientAsync(string apiKey, HttpClient httpClient)
{
using var request = CreateValidationRequest(apiKey);
var response = await httpClient.SendAsync(request);
var responseBody = await response.Content.ReadAsStringAsync();
_logger?.LogDebug("Anthropic API response: Status={StatusCode}, Body={Body}",
response.StatusCode, responseBody.Length > 200 ? responseBody.Substring(0, 200) + "..." : responseBody);
return InterpretResponse(response.StatusCode, responseBody);
}
private HttpRequestMessage CreateValidationRequest(string apiKey)
{
var request = new HttpRequestMessage(HttpMethod.Post, API_ENDPOINT);
// Set headers
request.Headers.Add("x-api-key", apiKey);
request.Headers.Add("anthropic-version", ANTHROPIC_VERSION);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// Ultra-minimal payload for lowest cost
var payload = new
{
model = DEFAULT_MODEL,
max_tokens = 1,
messages = new[]
{
new { role = "user", content = "1" }
},
temperature = 0,
stop_sequences = new[] { "1", "2", "3", "4", "5" }
};
var jsonContent = JsonSerializer.Serialize(payload);
request.Content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
return request;
}
private ValidationResult InterpretResponse(HttpStatusCode statusCode, string responseBody)
{
// Success cases
if (IsSuccessStatusCode(statusCode))
{
return ValidationResult.Success(statusCode);
}
var bodyLower = responseBody.ToLowerInvariant();
switch (statusCode)
{
case HttpStatusCode.Unauthorized: // 401
if (ContainsAny(bodyLower, InvalidKeyIndicators))
{
return ValidationResult.IsUnauthorized(statusCode);
}
return ValidationResult.IsUnauthorized(statusCode);
case HttpStatusCode.Forbidden: // 403
if (ContainsAny(bodyLower, PermissionIndicators))
{
_logger?.LogInformation("API key has permission restrictions but is valid");
return ValidationResult.Success(statusCode);
}
return ValidationResult.HasHttpError(statusCode, $"Forbidden: {TruncateResponse(responseBody)}");
case HttpStatusCode.BadRequest: // 400
if (ContainsAny(bodyLower, QuotaIndicators))
{
_logger?.LogInformation("API key is valid but has quota/billing issues");
return ValidationResult.Success(statusCode);
}
return ValidationResult.HasHttpError(statusCode, $"Bad request: {TruncateResponse(responseBody)}");
case HttpStatusCode.PaymentRequired: // 402
case HttpStatusCode.TooManyRequests: // 429
return ValidationResult.Success(statusCode);
case HttpStatusCode.ServiceUnavailable: // 503
case HttpStatusCode.GatewayTimeout: // 504
return ValidationResult.HasNetworkError($"Service unavailable: {statusCode}");
default:
if (ContainsAny(bodyLower, QuotaIndicators))
{
return ValidationResult.Success(statusCode);
}
return ValidationResult.HasHttpError(statusCode,
$"API request failed with status {statusCode}. Response: {TruncateResponse(responseBody)}");
}
}
protected override bool IsValidKeyFormat(string apiKey)
{
if (string.IsNullOrWhiteSpace(apiKey) || apiKey.Length < 20)
return false;
if (!apiKey.StartsWith("sk-ant-", StringComparison.Ordinal))
return false;
return apiKey.All(c => char.IsLetterOrDigit(c) || c == '-' || c == '_');
}
}
}
================================================
FILE: UnsecuredAPIKeys.Providers/AI Providers/GoogleProvider.cs
================================================
using System.Net;
using System.Net.Http.Headers;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using UnsecuredAPIKeys.Data.Common;
using UnsecuredAPIKeys.Providers._Base;
using UnsecuredAPIKeys.Providers.Common;
namespace UnsecuredAPIKeys.Providers.AI_Providers
{
/// <summary>
/// Provider implementation for handling Google AI API keys.
/// </summary>
[ApiProvider]
public class GoogleProvider : BaseApiKeyProvider
{
public override string ProviderName => "Google";
public override ApiTypeEnum ApiType => ApiTypeEnum.GoogleAI;
// Regex patterns specific to Google AI keys (from Scraper_Program.cs)
public override IEnumerable<string> RegexPatterns =>
[
@"AIza[0-9A-Za-z\-_]{35}", // Standard length is exactly 39 characters total
@"AIza[0-9A-Za-z\-_]{35,40}" // Allow for some variation in newer keys
];
public GoogleProvider() : base()
{
}
public GoogleProvider(ILogger<GoogleProvider>? logger) : base(logger)
{
}
protected override async Task<ValidationResult> ValidateKeyWithHttpClientAsync(string apiKey, HttpClient httpClient)
{
// Use Google's models endpoint for lightweight validation
using var modelRequest = new HttpRequestMessage(HttpMethod.Get, "https://generativelanguage.googleapis.com/v1beta/models");
// Google uses x-goog-api-key header
modelRequest.Headers.Add("x-goog-api-key", apiKey);
var modelResponse = await httpClient.SendAsync(modelRequest);
string responseBody = await modelResponse.Content.ReadAsStringAsync();
_logger?.LogDebug("Google AI models API response: Status={StatusCode}, Body={Body}",
modelResponse.StatusCode, TruncateResponse(responseBody));
if (IsSuccessStatusCode(modelResponse.StatusCode))
{
// Parse the models from the response
var models = ParseGoogleModels(responseBody);
return ValidationResult.Success(modelResponse.StatusCode, models);
}
else if (modelResponse.StatusCode == HttpStatusCode.Unauthorized ||
modelResponse.StatusCode == HttpStatusCode.Forbidden)
{
// Google often uses 403 for invalid keys
if (ContainsAny(responseBody, UnauthorizedIndicators))
{
return ValidationResult.IsUnauthorized(modelResponse.StatusCode);
}
return ValidationResult.IsUnauthorized(modelResponse.StatusCode);
}
else if (modelResponse.StatusCode == HttpStatusCode.BadRequest)
{
// Check specific error types
if (ContainsAny(responseBody, UnauthorizedIndicators))
{
return ValidationResult.IsUnauthorized(modelResponse.StatusCode);
}
return ValidationResult.HasHttpError(modelResponse.StatusCode, $"Bad request. Response: {TruncateResponse(responseBody)}");
}
else if ((int)modelResponse.StatusCode == 429)
{
// Rate limited means the key is valid
return ValidationResult.Success(modelResponse.StatusCode);
}
else
{
// Check for quota/billing issues
if (ContainsAny(responseBody, QuotaIndicators))
{
return ValidationResult.Success(modelResponse.StatusCode);
}
return ValidationResult.HasHttpError(modelResponse.StatusCode,
$"API request failed with status {modelResponse.StatusCode}. Response: {TruncateResponse(responseBody)}");
}
}
protected override bool IsValidKeyFormat(string apiKey)
{
return !string.IsNullOrWhiteSpace(apiKey) &&
apiKey.StartsWith("AIza") &&
apiKey.Length >= 39; // AIza + 35 chars
}
private List<ModelInfo>? ParseGoogleModels(string jsonResponse)
{
try
{
using var doc = JsonDocument.Parse(jsonResponse);
if (!doc.RootElement.TryGetProperty("models", out var modelsArray))
{
return null;
}
var models = new List<ModelInfo>();
foreach (var modelElement in modelsArray.EnumerateArray())
{
var model = new ModelInfo
{
ModelId = modelElement.GetProperty("name").GetString() ?? "",
DisplayName = modelElement.TryGetProperty("displayName", out var displayName) ? displayName.GetString() : null,
Description = modelElement.TryGetProperty("description", out var description) ? description.GetString() : null,
Version = modelElement.TryGetProperty("version", out var version) ? version.GetString() : null,
InputTokenLimit = modelElement.TryGetProperty("inputTokenLimit", out var inputLimit) ? inputLimit.GetInt64() : null,
OutputTokenLimit = modelElement.TryGetProperty("outputTokenLimit", out var outputLimit) ? outputLimit.GetInt64() : null,
Temperature = modelElement.TryGetProperty("temperature", out var temp) ? (float?)temp.GetDouble() : null,
TopP = modelElement.TryGetProperty("topP", out var topP) ? (float?)topP.GetDouble() : null,
TopK = modelElement.TryGetProperty("topK", out var topK) ? topK.GetInt32() : null,
MaxTemperature = modelElement.TryGetProperty("maxTemperature", out var maxTemp) ? (float?)maxTemp.GetDouble() : null
};
// Parse supported methods
if (modelElement.TryGetProperty("supportedGenerationMethods", out var methods))
{
model.SupportedMethods = new List<string>();
foreach (var method in methods.EnumerateArray())
{
if (method.GetString() is string methodStr)
{
model.SupportedMethods.Add(methodStr);
}
}
}
// Extract model group from the display name
if (model.DisplayName != null)
{
// Extract model family (e.g., "Gemini 1.5", "Gemini 2.0", etc.)
if (model.DisplayName.Contains("Gemini"))
{
var parts = model.DisplayName.Split(' ');
if (parts.Length >= 2)
{
model.ModelGroup = $"{parts[0]} {parts[1]}";
}
}
}
models.Add(model);
}
return models;
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error parsing Google models response");
return null;
}
}
}
}
================================================
FILE: UnsecuredAPIKeys.Providers/AI Providers/OpenAIProvider.cs
================================================
using System.Net;
using System.Net.Http.Headers;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using UnsecuredAPIKeys.Data.Common;
using UnsecuredAPIKeys.Providers._Base;
using UnsecuredAPIKeys.Providers.Common;
namespace UnsecuredAPIKeys.Providers.AI_Providers
{
/// <summary>
/// Provider implementation for handling OpenAI API keys.
/// </summary>
[ApiProvider]
public class OpenAIProvider : BaseApiKeyProvider
{
public override string ProviderName => "OpenAI";
public override ApiTypeEnum ApiType => ApiTypeEnum.OpenAI;
// Enhanced regex patterns for OpenAI keys
public override IEnumerable<string> RegexPatterns =>
[
@"sk-[A-Za-z0-9\-]{20,}",
@"sk-proj-[A-Za-z0-9\-]{20,}",
@"sk-svcacct-[A-Za-z0-9\-]{20,}",
@"sk-[A-Za-z0-9]{48}", // Standard format
@"Bearer sk-[A-Za-z0-9\-]{20,}" // Keys in auth headers
];
public OpenAIProvider() : base()
{
}
public OpenAIProvider(ILogger<OpenAIProvider>? logger) : base(logger)
{
}
protected override async Task<ValidationResult> ValidateKeyWithHttpClientAsync(string apiKey, HttpClient httpClient)
{
// First, try a lightweight model listing endpoint
using var modelRequest = new HttpRequestMessage(HttpMethod.Get, "https://api.openai.com/v1/models");
modelRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
var modelResponse = await httpClient.SendAsync(modelRequest);
string responseBody = await modelResponse.Content.ReadAsStringAsync();
_logger?.LogDebug("OpenAI models API response: Status={StatusCode}, Body={Body}",
modelResponse.StatusCode, TruncateResponse(responseBody));
if (IsSuccessStatusCode(modelResponse.StatusCode))
{
// Parse the models from the response
var models = ParseOpenAIModels(responseBody);
return ValidationResult.Success(modelResponse.StatusCode, models);
}
else if (modelResponse.StatusCode == HttpStatusCode.Unauthorized)
{
return ValidationResult.IsUnauthorized(modelResponse.StatusCode);
}
else if ((int)modelResponse.StatusCode == 429)
{
// Rate limited means the key is valid
return ValidationResult.Success(modelResponse.StatusCode);
}
else if (modelResponse.StatusCode == HttpStatusCode.PaymentRequired)
{
// Payment required means valid key but no credits
return ValidationResult.Success(modelResponse.StatusCode);
}
else
{
// Check response body for quota/billing issues
if (ContainsAny(responseBody, QuotaIndicators))
{
return ValidationResult.Success(modelResponse.StatusCode);
}
return ValidationResult.HasHttpError(modelResponse.StatusCode,
$"API request failed with status {modelResponse.StatusCode}. Response: {TruncateResponse(responseBody)}");
}
}
protected override bool IsValidKeyFormat(string apiKey)
{
return !string.IsNullOrWhiteSpace(apiKey) &&
apiKey.StartsWith("sk-") &&
apiKey.Length >= 23; // sk- + at least 20 chars
}
private List<ModelInfo>? ParseOpenAIModels(string jsonResponse)
{
try
{
using var doc = JsonDocument.Parse(jsonResponse);
if (!doc.RootElement.TryGetProperty("data", out var dataArray))
{
return null;
}
var models = new List<ModelInfo>();
foreach (var modelElement in dataArray.EnumerateArray())
{
var model = new ModelInfo
{
ModelId = modelElement.GetProperty("id").GetString() ?? "",
DisplayName = modelElement.GetProperty("id").GetString() ?? "", // OpenAI uses id as display name
Description = modelElement.TryGetProperty("description", out var desc) ? desc.GetString() : null
};
// Extract model group from the ID
if (!string.IsNullOrEmpty(model.ModelId))
{
// Group models by family (e.g., "gpt-4", "gpt-3.5", "text-embedding")
if (model.ModelId.StartsWith("gpt-4"))
{
model.ModelGroup = "GPT-4";
// Check for specific capabilities
if (model.ModelId.Contains("turbo"))
{
model.Description = "GPT-4 Turbo model with enhanced capabilities";
}
else if (model.ModelId.Contains("vision"))
{
model.Description = "GPT-4 model with vision capabilities";
}
}
else if (model.ModelId.StartsWith("gpt-3.5"))
{
model.ModelGroup = "GPT-3.5";
}
else if (model.ModelId.StartsWith("o1"))
{
model.ModelGroup = "O1";
model.Description = "OpenAI's reasoning model";
}
else if (model.ModelId.StartsWith("text-embedding"))
{
model.ModelGroup = "Embeddings";
model.Description = "Text embedding model";
}
else if (model.ModelId.StartsWith("dall-e"))
{
model.ModelGroup = "DALL-E";
model.Description = "Image generation model";
}
else if (model.ModelId.StartsWith("whisper"))
{
model.ModelGroup = "Whisper";
model.Description = "Speech recognition model";
}
else if (model.ModelId.StartsWith("tts"))
{
model.ModelGroup = "TTS";
model.Description = "Text-to-speech model";
}
else
{
model.ModelGroup = "Other";
}
}
models.Add(model);
}
return models;
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error parsing OpenAI models response");
return null;
}
}
}
}
================================================
FILE: UnsecuredAPIKeys.Providers/ApiProviderRegistry.cs
================================================
using System.Reflection;
using UnsecuredAPIKeys.Data.Common;
using UnsecuredAPIKeys.Providers._Interfaces;
namespace UnsecuredAPIKeys.Providers
{
public static class ApiProviderRegistry
{
private static readonly Lazy<List<IApiKeyProvider>> _allProviders = new(() =>
{
return [.. Assembly.GetExecutingAssembly()
.GetTypes()
.Where(type => type.GetCustomAttribute<ApiProviderAttribute>() != null
&& typeof(IApiKeyProvider).IsAssignableFrom(type)
&& !type.IsInterface
&& !type.IsAbstract)
.Select(type => (IApiKeyProvider)Activator.CreateInstance(type)!)];
});
private static readonly Lazy<List<IApiKeyProvider>> _scraperProviders = new(() =>
{
return [.. Assembly.GetExecutingAssembly()
.GetTypes()
.Where(type => {
var attr = type.GetCustomAttribute<ApiProviderAttribute>();
return attr != null
&& attr.ScraperUse
&& typeof(IApiKeyProvider).IsAssignableFrom(type)
&& !type.IsInterface
&& !type.IsAbstract;
})
.Select(type => (IApiKeyProvider)Activator.CreateInstance(type)!)];
});
private static readonly Lazy<List<IApiKeyProvider>> _verifierProviders = new(() =>
{
return [.. Assembly.GetExecutingAssembly()
.GetTypes()
.Where(type => {
var attr = type.GetCustomAttribute<ApiProviderAttribute>();
return attr != null
&& attr.VerificationUse
&& typeof(IApiKeyProvider).IsAssignableFrom(type)
&& !type.IsInterface
&& !type.IsAbstract;
})
.Select(type => (IApiKeyProvider)Activator.CreateInstance(type)!)];
});
/// <summary>
/// Gets all providers with ApiProvider attribute (backward compatibility)
/// </summary>
public static IReadOnlyList<IApiKeyProvider> Providers => _allProviders.Value;
/// <summary>
/// Gets providers that are enabled for scraper use
/// </summary>
public static IReadOnlyList<IApiKeyProvider> ScraperProviders => _scraperProviders.Value;
/// <summary>
/// Gets providers that are enabled for verifier use
/// </summary>
public static IReadOnlyList<IApiKeyProvider> VerifierProviders => _verifierProviders.Value;
/// <summary>
/// Gets providers for a specific bot type
/// </summary>
/// <param name="botType">The type of bot (Scraper or Verifier)</param>
/// <returns>List of providers enabled for the specified bot type</returns>
public static IReadOnlyList<IApiKeyProvider> GetProvidersForBot(BotType botType)
{
return botType switch
{
BotType.Scraper => ScraperProviders,
BotType.Verifier => VerifierProviders,
_ => Providers
};
}
}
/// <summary>
/// Enumeration of bot types for provider filtering
/// </summary>
public enum BotType
{
Scraper,
Verifier
}
}
================================================
FILE: UnsecuredAPIKeys.Providers/Common/ValidationResult.cs
================================================
using System.Net;
namespace UnsecuredAPIKeys.Providers.Common
{
public enum ValidationAttemptStatus
{
Valid, // Key is valid and working.
Unauthorized, // Key is explicitly unauthorized (e.g., HTTP 401).
HttpError, // An HTTP error occurred (e.g., 403, 404, 429, 5xx).
NetworkError, // A network-level error occurred (e.g., DNS, timeout, connection refused).
ProviderSpecificError // An unexpected error within the provider's logic.
}
public class ModelInfo
{
public string ModelId { get; set; } = string.Empty;
public string? DisplayName { get; set; }
public string? Description { get; set; }
public string? Version { get; set; }
public long? InputTokenLimit { get; set; }
public long? OutputTokenLimit { get; set; }
public List<string>? SupportedMethods { get; set; }
public float? Temperature { get; set; }
public float? TopP { get; set; }
public int? TopK { get; set; }
public float? MaxTemperature { get; set; }
public string? ModelGroup { get; set; } // For grouping similar models
}
public class ValidationResult
{
public ValidationAttemptStatus Status { get; set; }
public HttpStatusCode? HttpStatusCode { get; set; } // Null if not an HTTP-related error.
public string? Detail { get; set; } = string.Empty; // Optional error message or detail.
// Model information discovered during validation
public List<ModelInfo>? AvailableModels { get; set; }
// Helper factory methods for convenience
public static ValidationResult Success(HttpStatusCode statusCode, List<ModelInfo>? models = null) =>
new() { Status = ValidationAttemptStatus.Valid, HttpStatusCode = statusCode, AvailableModels = models };
public static ValidationResult IsUnauthorized(HttpStatusCode statusCode, string? detail = null) =>
new() { Status = ValidationAttemptStatus.Unauthorized, HttpStatusCode = statusCode, Detail = detail };
public static ValidationResult HasHttpError(HttpStatusCode statusCode, string? detail = null) =>
new() { Status = ValidationAttemptStatus.HttpError, HttpStatusCode = statusCode, Detail = detail };
public static ValidationResult HasNetworkError(string detail) =>
new() { Status = ValidationAttemptStatus.NetworkError, Detail = detail };
public static ValidationResult HasProviderSpecificError(string detail) =>
new() { Status = ValidationAttemptStatus.ProviderSpecificError, Detail = detail };
}
}
================================================
FILE: UnsecuredAPIKeys.Providers/Search Providers/GitHubSearchProvider.cs
================================================
using Microsoft.Extensions.Logging;
using Octokit;
using UnsecuredAPIKeys.Data;
using UnsecuredAPIKeys.Data.Models;
using UnsecuredAPIKeys.Providers._Interfaces;
// Assuming logging might be needed later
namespace UnsecuredAPIKeys.Providers.Search_Providers
{
/// <summary>
/// Implements the ISearchProvider interface for searching code on GitHub.
/// </summary>
public class GitHubSearchProvider(DBContext dbContext, ILogger<GitHubSearchProvider>? logger = null) : ISearchProvider
{
/// <inheritdoc />
public string ProviderName => "GitHub";
/// <inheritdoc />
public async Task<IEnumerable<RepoReference>> SearchAsync(SearchQuery query, SearchProviderToken? token)
{
if (token == null || string.IsNullOrWhiteSpace(token.Token))
{
logger?.LogError("GitHub token is missing or invalid."); // Use _logger field
throw new ArgumentNullException(nameof(token), "A valid GitHub token is required.");
}
if (query == null || string.IsNullOrWhiteSpace(query.Query))
{
logger?.LogError("Search query is missing or invalid."); // Use _logger field
throw new ArgumentNullException(nameof(query), "A valid search query is required.");
}
var client = new GitHubClient(new ProductHeaderValue("UnsecuredAPIKeys-Scraper"))
{
Credentials = new Credentials(token.Token)
};
var results = new List<RepoReference>();
int page = 1;
const int perPage = 100; // Max allowed by GitHub API
try
{
logger?.LogInformation("Starting GitHub search for query: {Query}", query.Query); // Use _logger field
while (true) // Loop to handle pagination
{
var request = new SearchCodeRequest(query.Query)
{
// Consider adding filters like language, user, repo if needed
Page = page,
PerPage = perPage
// Order = SortDirection.Descending
};
SearchCodeResult searchResult;
try
{
searchResult = await client.Search.SearchCode(request);
if (page == 1)
{
query.SearchResultsCount = searchResult.TotalCount;
dbContext.SearchQueries.Update(query);
await dbContext.SaveChangesAsync();
}
}
catch (RateLimitExceededException ex)
{
logger?.LogWarning("GitHub API rate limit exceeded. Waiting until {ResetTime}.", ex.Reset.ToString("o")); // Use _logger field
// Wait until the rate limit resets
var delay = ex.Reset - DateTimeOffset.UtcNow;
if (delay > TimeSpan.Zero)
{
if (delay.TotalMinutes > 1)
{
Environment.Exit(200);
}
await Task.Delay(delay);
}
continue; // Retry the same page
}
catch (ApiException apiEx)
{
logger?.LogError(apiEx, "GitHub API error during search on page {Page}. Status: {StatusCode}", page, apiEx.StatusCode); // Use _logger field
// Decide how to handle API errors (e.g., stop, retry after delay)
break; // Stop searching on API error for now
}
if (searchResult?.Items == null || !searchResult.Items.Any())
{
logger?.LogInformation("No more results found for query '{Query}' on page {Page}.", query.Query, page); // Use _logger field
break; // No more results
}
logger?.LogDebug("Found {Count} results on page {Page} for query '{Query}'.", searchResult.Items.Count, page, query.Query); // Use _logger field
foreach (var item in searchResult.Items)
{
results.Add(new RepoReference
{
SearchQueryId = query.Id,
Provider = ProviderName,
RepoOwner = item.Repository?.Owner?.Login, // Corrected field name
RepoName = item.Repository?.Name, // Corrected field name
FilePath = item.Path,
FileURL = item.HtmlUrl, // HTML URL for viewing in browser
ApiContentUrl = item.Url, // API URL for fetching content
Branch = item.Repository?.DefaultBranch, // Assuming default branch, might need refinement
FileSHA = item.Sha, // Corrected field name (SHA of the file blob)
FoundUTC = DateTime.UtcNow, // Corrected field name (Record when this specific reference was found)
RepoURL = item.Repository?.HtmlUrl,
RepoDescription = item.Repository?.Description,
FileName = item.Name
});
}
// Basic check to prevent infinite loops if API behaves unexpectedly
if (searchResult.Items.Count < perPage || results.Count >= 1000) // GitHub limits code search results to 1000
{
logger?.LogInformation("Finished GitHub search for query '{Query}'. Total results processed: {TotalCount}. Reached page limit or result cap.", query.Query, results.Count); // Use _logger field
break;
}
page++; // Move to the next page
await Task.Delay(TimeSpan.FromSeconds(2)); // Add a small delay to be polite to the API
}
}
catch (Exception ex)
{
logger?.LogError(ex, "An unexpected error occurred during GitHub search for query: {Query}", query.Query); // Use _logger field
}
logger?.LogInformation("Completed GitHub search for query '{Query}'. Found {Count} potential references.", query.Query, results.Count); // Use _logger field
return results;
}
}
}
================================================
FILE: UnsecuredAPIKeys.Providers/UnsecuredAPIKeys.Providers.csproj
================================================
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\UnsecuredAPIKeys.Data\UnsecuredAPIKeys.Data.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
<PackageReference Include="Octokit" Version="14.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.1" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
================================================
FILE: UnsecuredAPIKeys.Providers/_Base/BaseApiKeyProvider.cs
================================================
using System.Net;
using Microsoft.Extensions.Logging;
using UnsecuredAPIKeys.Data.Common;
using UnsecuredAPIKeys.Providers._Interfaces;
using UnsecuredAPIKeys.Providers.Common;
namespace UnsecuredAPIKeys.Providers._Base
{
/// <summary>
/// Base class for API key providers with common functionality and retry logic.
/// Lite version: OpenAI, Anthropic, Google only.
/// Full version with all providers: www.UnsecuredAPIKeys.com
/// </summary>
public abstract class BaseApiKeyProvider(ILogger? logger = null) : IApiKeyProvider
{
protected const int DEFAULT_MAX_RETRIES = 3;
protected const int DEFAULT_TIMEOUT_SECONDS = 30;
protected readonly ILogger? _logger = logger;
public abstract string ProviderName { get; }
public abstract ApiTypeEnum ApiType { get; }
public abstract IEnumerable<string> RegexPatterns { get; }
/// <summary>
/// Validates an API key with retry logic and proper resource management.
/// </summary>
public async Task<ValidationResult> ValidateKeyAsync(string apiKey, IHttpClientFactory httpClientFactory)
{
if (string.IsNullOrWhiteSpace(apiKey))
{
return ValidationResult.HasProviderSpecificError("API key is null or whitespace.");
}
// Clean the API key
apiKey = CleanApiKey(apiKey);
// Validate format if implemented
if (!IsValidKeyFormat(apiKey))
{
return ValidationResult.HasProviderSpecificError("API key format is invalid.");
}
Exception? lastException = null;
for (int retry = 0; retry < GetMaxRetries(); retry++)
{
if (retry > 0)
{
var delay = TimeSpan.FromSeconds(Math.Pow(2, retry - 1));
_logger?.LogDebug("Retrying {Provider} validation after {Delay}ms (attempt {Retry}/{MaxRetries})",
ProviderName, delay.TotalMilliseconds, retry + 1, GetMaxRetries());
await Task.Delay(delay);
}
try
{
using var httpClient = CreateHttpClient(httpClientFactory);
var result = await ValidateKeyWithHttpClientAsync(apiKey, httpClient);
if (result.Status != ValidationAttemptStatus.NetworkError)
{
return result;
}
// Continue retrying on network errors
lastException = new Exception(result.Detail);
}
catch (HttpRequestException ex)
{
lastException = ex;
_logger?.LogWarning(ex, "HTTP request failed on attempt {Retry}/{MaxRetries} for {Provider}",
retry + 1, GetMaxRetries(), ProviderName);
if (retry == GetMaxRetries() - 1)
{
return ValidationResult.HasNetworkError($"HTTP request failed after {GetMaxRetries()} retries: {ex.Message}");
}
}
catch (TaskCanceledException ex)
{
lastException = ex;
_logger?.LogWarning(ex, "Request timeout on attempt {Retry}/{MaxRetries} for {Provider}",
retry + 1, GetMaxRetries(), ProviderName);
if (retry == GetMaxRetries() - 1)
{
return ValidationResult.HasNetworkError($"Request timeout after {GetMaxRetries()} retries: {ex.Message}");
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "Unexpected error during {Provider} key validation", ProviderName);
return ValidationResult.HasProviderSpecificError($"Unexpected error: {ex.Message}");
}
}
return ValidationResult.HasNetworkError($"Failed after {GetMaxRetries()} retries. Last error: {lastException?.Message ?? "Unknown error"}");
}
/// <summary>
/// Abstract method for provider-specific validation logic.
/// </summary>
protected abstract Task<ValidationResult> ValidateKeyWithHttpClientAsync(string apiKey, HttpClient httpClient);
/// <summary>
/// Creates an HttpClient with proper configuration.
/// </summary>
protected virtual HttpClient CreateHttpClient(IHttpClientFactory httpClientFactory)
{
try
{
var client = httpClientFactory.CreateClient(ProviderName.ToLowerInvariant().Replace(" ", ""));
client.Timeout = TimeSpan.FromSeconds(GetTimeoutSeconds());
return client;
}
catch
{
// Fall back to manual creation if factory fails
return new HttpClient
{
Timeout = TimeSpan.FromSeconds(GetTimeoutSeconds())
};
}
}
/// <summary>
/// Cleans the API key by removing common prefixes and whitespace.
/// </summary>
protected virtual string CleanApiKey(string apiKey)
{
apiKey = apiKey.Trim();
// Remove common prefixes
if (apiKey.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
apiKey = apiKey.Substring(7).Trim();
}
else if (apiKey.StartsWith("x-api-key:", StringComparison.OrdinalIgnoreCase))
{
apiKey = apiKey.Substring(10).Trim();
}
return apiKey;
}
/// <summary>
/// Validates the API key format. Override in derived classes for specific validation.
/// </summary>
protected virtual bool IsValidKeyFormat(string apiKey)
{
return !string.IsNullOrWhiteSpace(apiKey) && apiKey.Length >= 10;
}
/// <summary>
/// Gets the maximum number of retries. Override in derived classes if needed.
/// </summary>
protected virtual int GetMaxRetries() => DEFAULT_MAX_RETRIES;
/// <summary>
/// Gets the timeout in seconds. Override in derived classes if needed.
/// </summary>
protected virtual int GetTimeoutSeconds() => DEFAULT_TIMEOUT_SECONDS;
/// <summary>
/// Common method to check if response body contains any of the specified indicators.
/// </summary>
protected static bool ContainsAny(string text, HashSet<string> indicators)
{
return indicators.Any(indicator => text.Contains(indicator, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Truncates response text for logging purposes.
/// </summary>
protected static string TruncateResponse(string response, int maxLength = 200)
{
if (string.IsNullOrEmpty(response))
return string.Empty;
return response.Length > maxLength
? response.Substring(0, maxLength) + "..."
: response;
}
/// <summary>
/// Checks if the status code indicates success.
/// </summary>
protected static bool IsSuccessStatusCode(HttpStatusCode statusCode)
{
return (int)statusCode >= 200 && (int)statusCode < 300;
}
/// <summary>
/// Common quota/billing indicators across providers.
/// </summary>
protected static readonly HashSet<string> QuotaIndicators = new(StringComparer.OrdinalIgnoreCase)
{
"credit", "quota", "billing", "insufficient_funds", "payment", "exceeded", "balance", "limit",
"insufficient_quota", "exceeded_quota", "rate_limit", "rate_limit_exceeded", "RESOURCE_EXHAUSTED"
};
/// <summary>
/// Common unauthorized indicators across providers.
/// </summary>
protected static readonly HashSet<string> UnauthorizedIndicators = new(StringComparer.OrdinalIgnoreCase)
{
"invalid_api_key", "authentication_error", "unauthorized", "invalid x-api-key", "API_KEY_INVALID",
"API key not valid", "API key expired", "invalid token", "authentication failed"
};
/// <summary>
/// Common permission indicators across providers.
/// </summary>
protected static readonly HashSet<string> PermissionIndicators = new(StringComparer.OrdinalIgnoreCase)
{
"permission", "access", "not_authorized_for_model", "forbidden", "read-only", "Pro service"
};
}
}
================================================
FILE: UnsecuredAPIKeys.Providers/_Interfaces/IApiKeyProvider.cs
================================================
using UnsecuredAPIKeys.Data.Common;
using UnsecuredAPIKeys.Providers.Common;
namespace UnsecuredAPIKeys.Providers._Interfaces
{
/// <summary>
/// Defines the contract for an API key provider, responsible for
/// identifying and validating keys for a specific service.
/// Lite version: OpenAI, Anthropic, Google only.
/// Full version: www.UnsecuredAPIKeys.com
/// </summary>
public interface IApiKeyProvider
{
/// <summary>
/// Gets the unique name of the provider (e.g., "OpenAI", "Anthropic").
/// </summary>
string ProviderName { get; }
/// <summary>
/// Gets the corresponding ApiTypeEnum value for this provider.
/// </summary>
ApiTypeEnum ApiType { get; }
/// <summary>
/// Gets the list of regex patterns used to identify potential keys for this provider.
/// </summary>
IEnumerable<string> RegexPatterns { get; }
/// <summary>
/// Asynchronously validates the given API key against the provider's service.
/// </summary>
/// <param name="apiKey">The API key string to validate.</param>
/// <param name="httpClientFactory">The IHttpClientFactory for creating HttpClient instances.</param>
/// <returns>A ValidationResult indicating the outcome of the validation attempt.</returns>
Task<ValidationResult> ValidateKeyAsync(string apiKey, IHttpClientFactory httpClientFactory);
}
}
================================================
FILE: UnsecuredAPIKeys.Providers/_Interfaces/ISearchProvider.cs
================================================
using UnsecuredAPIKeys.Data.Models;
namespace UnsecuredAPIKeys.Providers._Interfaces
{
/// <summary>
/// Defines the contract for a search provider used to find potential API keys.
/// </summary>
public interface ISearchProvider
{
/// <summary>
/// Gets the name of the search provider (e.g., "GitHub", "GitLab").
/// </summary>
string ProviderName { get; }
/// <summary>
/// Executes a search based on the provided query.
/// </summary>
/// <param name="query">The search query details.</param>
/// <param name="token">The API token to use for the search.</param>
/// <returns>A collection of RepoReference objects representing potential findings.</returns>
Task<IEnumerable<RepoReference>> SearchAsync(SearchQuery query, SearchProviderToken? token);
}
}
gitextract_966uldie/
├── .gitattributes
├── .github/
│ └── workflows/
│ ├── claude-code-review.yml
│ └── claude.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── UnsecuredAPIKeys-OpenSource.sln
├── UnsecuredAPIKeys.CLI/
│ ├── Constants.cs
│ ├── Program.cs
│ ├── Services/
│ │ ├── DatabaseService.cs
│ │ ├── ScraperService.cs
│ │ └── VerifierService.cs
│ ├── UnsecuredAPIKeys.CLI.csproj
│ └── appsettings.example.json
├── UnsecuredAPIKeys.Data/
│ ├── Common/
│ │ ├── ApiProviderAttribute.cs
│ │ └── CommonEnums.cs
│ ├── DBContext.cs
│ ├── DesignTimeDbContextFactory.cs
│ ├── Models/
│ │ ├── APIKey.cs
│ │ ├── ApplicationSetting.cs
│ │ ├── RepoReference.cs
│ │ ├── SearchProviderToken.cs
│ │ └── SearchQuery.cs
│ └── UnsecuredAPIKeys.Data.csproj
└── UnsecuredAPIKeys.Providers/
├── AI Providers/
│ ├── AnthropicProvider.cs
│ ├── GoogleProvider.cs
│ └── OpenAIProvider.cs
├── ApiProviderRegistry.cs
├── Common/
│ └── ValidationResult.cs
├── Search Providers/
│ └── GitHubSearchProvider.cs
├── UnsecuredAPIKeys.Providers.csproj
├── _Base/
│ └── BaseApiKeyProvider.cs
└── _Interfaces/
├── IApiKeyProvider.cs
└── ISearchProvider.cs
SYMBOL INDEX (90 symbols across 22 files)
FILE: UnsecuredAPIKeys.CLI/Constants.cs
class LiteLimits (line 7) | public static class LiteLimits
class AppInfo (line 39) | public static class AppInfo
FILE: UnsecuredAPIKeys.CLI/Services/DatabaseService.cs
class DatabaseService (line 12) | public class DatabaseService(string dbPath = "unsecuredapikeys.db")
method InitializeDatabaseAsync (line 14) | public async Task<DBContext> InitializeDatabaseAsync()
method SeedDefaultDataAsync (line 27) | private async Task SeedDefaultDataAsync(DBContext dbContext)
method GetStatisticsAsync (line 76) | public async Task<Statistics> GetStatisticsAsync(DBContext dbContext)
method SaveGitHubTokenAsync (line 95) | public async Task SaveGitHubTokenAsync(DBContext dbContext, string token)
method ResetDatabaseAsync (line 118) | public async Task ResetDatabaseAsync()
method ExportKeysAsync (line 129) | public async Task ExportKeysAsync(DBContext dbContext, string filePath...
method ExportAsJsonAsync (line 152) | private async Task ExportAsJsonAsync(List<APIKey> keys, string filePath)
method ExportAsCsvAsync (line 180) | private async Task ExportAsCsvAsync(List<APIKey> keys, string filePath)
class Statistics (line 197) | public class Statistics
FILE: UnsecuredAPIKeys.CLI/Services/ScraperService.cs
class ScraperService (line 18) | public class ScraperService
method ScraperService (line 29) | public ScraperService(DBContext dbContext, IHttpClientFactory httpClie...
method RunAsync (line 37) | public async Task RunAsync(CancellationToken cancellationToken)
method RunScrapingCycleAsync (line 93) | private async Task RunScrapingCycleAsync(SearchProviderToken token)
method ProcessResultAsync (line 166) | private async Task ProcessResultAsync(RepoReference repoRef, SearchPro...
method FetchFileContentAsync (line 229) | private async Task<string?> FetchFileContentAsync(RepoReference repoRe...
FILE: UnsecuredAPIKeys.CLI/Services/VerifierService.cs
class VerifierService (line 18) | public class VerifierService(
method RunAsync (line 30) | public async Task RunAsync(CancellationToken cancellationToken)
method RunVerificationCycleAsync (line 77) | private async Task RunVerificationCycleAsync()
method ReVerifyExistingKeysAsync (line 113) | private async Task ReVerifyExistingKeysAsync()
method VerifyNewKeysAsync (line 142) | private async Task VerifyNewKeysAsync(int neededCount)
method VerifyKeyAsync (line 188) | private async Task<bool> VerifyKeyAsync(APIKey key)
method GetProvidersToTry (line 282) | private List<IApiKeyProvider> GetProvidersToTry(APIKey key)
FILE: UnsecuredAPIKeys.Data/Common/ApiProviderAttribute.cs
class ApiProviderAttribute (line 3) | [AttributeUsage(AttributeTargets.Class)]
method ApiProviderAttribute (line 19) | public ApiProviderAttribute()
method ApiProviderAttribute (line 28) | public ApiProviderAttribute(bool scraperUse, bool verificationUse)
FILE: UnsecuredAPIKeys.Data/Common/CommonEnums.cs
type SearchProviderEnum (line 8) | public enum SearchProviderEnum
type ApiStatusEnum (line 17) | public enum ApiStatusEnum
type ApiTypeEnum (line 40) | public enum ApiTypeEnum
FILE: UnsecuredAPIKeys.Data/DBContext.cs
class DBContext (line 10) | public class DBContext : DbContext
method DBContext (line 14) | public DBContext(DbContextOptions<DBContext> options) : base(options)
method DBContext (line 19) | public DBContext(string dbPath = "unsecuredapikeys.db")
method OnConfiguring (line 31) | protected override void OnConfiguring(DbContextOptionsBuilder optionsB...
method OnModelCreating (line 39) | protected override void OnModelCreating(ModelBuilder modelBuilder)
FILE: UnsecuredAPIKeys.Data/DesignTimeDbContextFactory.cs
class DesignTimeDbContextFactory (line 10) | public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<DB...
method CreateDbContext (line 12) | public DBContext CreateDbContext(string[] args)
FILE: UnsecuredAPIKeys.Data/Models/APIKey.cs
class APIKey (line 8) | public class APIKey
FILE: UnsecuredAPIKeys.Data/Models/ApplicationSetting.cs
class ApplicationSetting (line 5) | public class ApplicationSetting
FILE: UnsecuredAPIKeys.Data/Models/RepoReference.cs
class RepoReference (line 5) | public class RepoReference
FILE: UnsecuredAPIKeys.Data/Models/SearchProviderToken.cs
class SearchProviderToken (line 6) | public class SearchProviderToken
FILE: UnsecuredAPIKeys.Data/Models/SearchQuery.cs
class SearchQuery (line 5) | public class SearchQuery
FILE: UnsecuredAPIKeys.Providers/AI Providers/AnthropicProvider.cs
class AnthropicProvider (line 17) | [ApiProvider]
method AnthropicProvider (line 50) | public AnthropicProvider() : base()
method AnthropicProvider (line 54) | public AnthropicProvider(ILogger<AnthropicProvider>? logger) : base(lo...
method ValidateKeyWithHttpClientAsync (line 58) | protected override async Task<ValidationResult> ValidateKeyWithHttpCli...
method CreateValidationRequest (line 71) | private HttpRequestMessage CreateValidationRequest(string apiKey)
method InterpretResponse (line 99) | private ValidationResult InterpretResponse(HttpStatusCode statusCode, ...
method IsValidKeyFormat (line 153) | protected override bool IsValidKeyFormat(string apiKey)
FILE: UnsecuredAPIKeys.Providers/AI Providers/GoogleProvider.cs
class GoogleProvider (line 14) | [ApiProvider]
method GoogleProvider (line 27) | public GoogleProvider() : base()
method GoogleProvider (line 31) | public GoogleProvider(ILogger<GoogleProvider>? logger) : base(logger)
method ValidateKeyWithHttpClientAsync (line 35) | protected override async Task<ValidationResult> ValidateKeyWithHttpCli...
method IsValidKeyFormat (line 91) | protected override bool IsValidKeyFormat(string apiKey)
method ParseGoogleModels (line 98) | private List<ModelInfo>? ParseGoogleModels(string jsonResponse)
FILE: UnsecuredAPIKeys.Providers/AI Providers/OpenAIProvider.cs
class OpenAIProvider (line 14) | [ApiProvider]
method OpenAIProvider (line 30) | public OpenAIProvider() : base()
method OpenAIProvider (line 34) | public OpenAIProvider(ILogger<OpenAIProvider>? logger) : base(logger)
method ValidateKeyWithHttpClientAsync (line 38) | protected override async Task<ValidationResult> ValidateKeyWithHttpCli...
method IsValidKeyFormat (line 83) | protected override bool IsValidKeyFormat(string apiKey)
method ParseOpenAIModels (line 90) | private List<ModelInfo>? ParseOpenAIModels(string jsonResponse)
FILE: UnsecuredAPIKeys.Providers/ApiProviderRegistry.cs
class ApiProviderRegistry (line 7) | public static class ApiProviderRegistry
method GetProvidersForBot (line 70) | public static IReadOnlyList<IApiKeyProvider> GetProvidersForBot(BotTyp...
type BotType (line 84) | public enum BotType
FILE: UnsecuredAPIKeys.Providers/Common/ValidationResult.cs
type ValidationAttemptStatus (line 5) | public enum ValidationAttemptStatus
class ModelInfo (line 14) | public class ModelInfo
class ValidationResult (line 30) | public class ValidationResult
method Success (line 40) | public static ValidationResult Success(HttpStatusCode statusCode, List...
method IsUnauthorized (line 43) | public static ValidationResult IsUnauthorized(HttpStatusCode statusCod...
method HasHttpError (line 46) | public static ValidationResult HasHttpError(HttpStatusCode statusCode,...
method HasNetworkError (line 49) | public static ValidationResult HasNetworkError(string detail) =>
method HasProviderSpecificError (line 52) | public static ValidationResult HasProviderSpecificError(string detail) =>
FILE: UnsecuredAPIKeys.Providers/Search Providers/GitHubSearchProvider.cs
class GitHubSearchProvider (line 13) | public class GitHubSearchProvider(DBContext dbContext, ILogger<GitHubSea...
method SearchAsync (line 19) | public async Task<IEnumerable<RepoReference>> SearchAsync(SearchQuery ...
FILE: UnsecuredAPIKeys.Providers/_Base/BaseApiKeyProvider.cs
class BaseApiKeyProvider (line 14) | public abstract class BaseApiKeyProvider(ILogger? logger = null) : IApiK...
method ValidateKeyAsync (line 28) | public async Task<ValidationResult> ValidateKeyAsync(string apiKey, IH...
method ValidateKeyWithHttpClientAsync (line 104) | protected abstract Task<ValidationResult> ValidateKeyWithHttpClientAsy...
method CreateHttpClient (line 109) | protected virtual HttpClient CreateHttpClient(IHttpClientFactory httpC...
method CleanApiKey (line 130) | protected virtual string CleanApiKey(string apiKey)
method IsValidKeyFormat (line 150) | protected virtual bool IsValidKeyFormat(string apiKey)
method GetMaxRetries (line 158) | protected virtual int GetMaxRetries() => DEFAULT_MAX_RETRIES;
method GetTimeoutSeconds (line 163) | protected virtual int GetTimeoutSeconds() => DEFAULT_TIMEOUT_SECONDS;
method ContainsAny (line 168) | protected static bool ContainsAny(string text, HashSet<string> indicat...
method TruncateResponse (line 176) | protected static string TruncateResponse(string response, int maxLengt...
method IsSuccessStatusCode (line 189) | protected static bool IsSuccessStatusCode(HttpStatusCode statusCode)
FILE: UnsecuredAPIKeys.Providers/_Interfaces/IApiKeyProvider.cs
type IApiKeyProvider (line 12) | public interface IApiKeyProvider
method ValidateKeyAsync (line 35) | Task<ValidationResult> ValidateKeyAsync(string apiKey, IHttpClientFact...
FILE: UnsecuredAPIKeys.Providers/_Interfaces/ISearchProvider.cs
type ISearchProvider (line 8) | public interface ISearchProvider
method SearchAsync (line 21) | Task<IEnumerable<RepoReference>> SearchAsync(SearchQuery query, Search...
Condensed preview — 35 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (146K chars).
[
{
"path": ".gitattributes",
"chars": 2518,
"preview": "###############################################################################\n# Set default behavior to automatically "
},
{
"path": ".github/workflows/claude-code-review.yml",
"chars": 1952,
"preview": "name: Claude Code Review\n\non:\n pull_request:\n types: [opened, synchronize]\n # Optional: Only run on specific file"
},
{
"path": ".github/workflows/claude.yml",
"chars": 1886,
"preview": "name: Claude Code\n\non:\n issue_comment:\n types: [created]\n pull_request_review_comment:\n types: [created]\n issue"
},
{
"path": ".gitignore",
"chars": 6517,
"preview": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## G"
},
{
"path": "CHANGELOG.md",
"chars": 8688,
"preview": "# Changelog\n\nAll notable changes to UnsecuredAPIKeys Open Source.\n\n## [1.0.0] - 2025-12-09 - Lite Version Release\n\nThis "
},
{
"path": "LICENSE",
"chars": 3745,
"preview": "UnsecuredAPIKeys Open Source License\nBased on MIT License with Attribution Requirements\n\nCopyright (c) 2025 TSCarterJr\n\n"
},
{
"path": "README.md",
"chars": 7197,
"preview": "# UnsecuredAPIKeys Lite\n\n[]\npublic class ApiProviderAttribute : A"
},
{
"path": "UnsecuredAPIKeys.Data/Common/CommonEnums.cs",
"chars": 1348,
"preview": "namespace UnsecuredAPIKeys.Data.Common\n{\n /// <summary>\n /// Search provider for finding API keys.\n /// Lite ve"
},
{
"path": "UnsecuredAPIKeys.Data/DBContext.cs",
"chars": 2997,
"preview": "using Microsoft.EntityFrameworkCore;\nusing UnsecuredAPIKeys.Data.Models;\n\nnamespace UnsecuredAPIKeys.Data\n{\n /// <sum"
},
{
"path": "UnsecuredAPIKeys.Data/DesignTimeDbContextFactory.cs",
"chars": 716,
"preview": "using Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Design;\n\nnamespace UnsecuredAPIKeys.Data\n{\n "
},
{
"path": "UnsecuredAPIKeys.Data/Models/APIKey.cs",
"chars": 1167,
"preview": "using System.ComponentModel.DataAnnotations;\nusing System.ComponentModel.DataAnnotations.Schema;\nusing UnsecuredAPIKeys"
},
{
"path": "UnsecuredAPIKeys.Data/Models/ApplicationSetting.cs",
"chars": 296,
"preview": "using System.ComponentModel.DataAnnotations;\n\nnamespace UnsecuredAPIKeys.Data.Models\n{\n public class ApplicationSett"
},
{
"path": "UnsecuredAPIKeys.Data/Models/RepoReference.cs",
"chars": 1528,
"preview": "using System.ComponentModel.DataAnnotations;\n\nnamespace UnsecuredAPIKeys.Data.Models\n{\n public class RepoReference\n "
},
{
"path": "UnsecuredAPIKeys.Data/Models/SearchProviderToken.cs",
"chars": 531,
"preview": "using System.ComponentModel.DataAnnotations;\nusing UnsecuredAPIKeys.Data.Common;\n\nnamespace UnsecuredAPIKeys.Data.Model"
},
{
"path": "UnsecuredAPIKeys.Data/Models/SearchQuery.cs",
"chars": 381,
"preview": "using System.ComponentModel.DataAnnotations;\n\nnamespace UnsecuredAPIKeys.Data.Models\n{\n public class SearchQuery\n "
},
{
"path": "UnsecuredAPIKeys.Data/UnsecuredAPIKeys.Data.csproj",
"chars": 663,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n <PropertyGroup>\n <TargetFramework>net10.0</TargetFramework>\n <ImplicitUsings"
},
{
"path": "UnsecuredAPIKeys.Providers/AI Providers/AnthropicProvider.cs",
"chars": 6422,
"preview": "using System.Net;\nusing System.Net.Http.Headers;\nusing System.Text;\nusing System.Text.Json;\n\nusing Microsoft.Extensions."
},
{
"path": "UnsecuredAPIKeys.Providers/AI Providers/GoogleProvider.cs",
"chars": 7479,
"preview": "using System.Net;\nusing System.Net.Http.Headers;\nusing System.Text.Json;\nusing Microsoft.Extensions.Logging;\nusing Unsec"
},
{
"path": "UnsecuredAPIKeys.Providers/AI Providers/OpenAIProvider.cs",
"chars": 7328,
"preview": "using System.Net;\nusing System.Net.Http.Headers;\nusing System.Text.Json;\nusing Microsoft.Extensions.Logging;\nusing Unsec"
},
{
"path": "UnsecuredAPIKeys.Providers/ApiProviderRegistry.cs",
"chars": 3464,
"preview": "using System.Reflection;\nusing UnsecuredAPIKeys.Data.Common;\nusing UnsecuredAPIKeys.Providers._Interfaces;\n\nnamespace U"
},
{
"path": "UnsecuredAPIKeys.Providers/Common/ValidationResult.cs",
"chars": 2696,
"preview": "using System.Net;\n\nnamespace UnsecuredAPIKeys.Providers.Common\n{\n public enum ValidationAttemptStatus\n {\n V"
},
{
"path": "UnsecuredAPIKeys.Providers/Search Providers/GitHubSearchProvider.cs",
"chars": 6785,
"preview": "using Microsoft.Extensions.Logging;\nusing Octokit;\nusing UnsecuredAPIKeys.Data;\nusing UnsecuredAPIKeys.Data.Models;\nusin"
},
{
"path": "UnsecuredAPIKeys.Providers/UnsecuredAPIKeys.Providers.csproj",
"chars": 588,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n <ItemGroup>\n <ProjectReference Include=\"..\\UnsecuredAPIKeys.Data\\UnsecuredAPIKe"
},
{
"path": "UnsecuredAPIKeys.Providers/_Base/BaseApiKeyProvider.cs",
"chars": 8819,
"preview": "using System.Net;\nusing Microsoft.Extensions.Logging;\nusing UnsecuredAPIKeys.Data.Common;\nusing UnsecuredAPIKeys.Provide"
},
{
"path": "UnsecuredAPIKeys.Providers/_Interfaces/IApiKeyProvider.cs",
"chars": 1479,
"preview": "using UnsecuredAPIKeys.Data.Common;\nusing UnsecuredAPIKeys.Providers.Common;\n\nnamespace UnsecuredAPIKeys.Providers._Inte"
},
{
"path": "UnsecuredAPIKeys.Providers/_Interfaces/ISearchProvider.cs",
"chars": 870,
"preview": "using UnsecuredAPIKeys.Data.Models;\n\nnamespace UnsecuredAPIKeys.Providers._Interfaces\n{\n /// <summary>\n /// Define"
}
]
About this extraction
This page contains the full source code of the TSCarterJr/UnsecuredAPIKeys-OpenSource GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 35 files (134.7 KB), approximately 31.7k tokens, and a symbol index with 90 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.