├── UnsecuredAPIKeys.Data ├── Models │ ├── ApplicationSetting.cs │ ├── SearchQuery.cs │ ├── SearchProviderToken.cs │ ├── APIKey.cs │ └── RepoReference.cs ├── UnsecuredAPIKeys.Data.csproj ├── DesignTimeDbContextFactory.cs ├── Common │ ├── ApiProviderAttribute.cs │ └── CommonEnums.cs └── DBContext.cs ├── UnsecuredAPIKeys.CLI ├── appsettings.example.json ├── Constants.cs ├── UnsecuredAPIKeys.CLI.csproj ├── Services │ ├── DatabaseService.cs │ ├── ScraperService.cs │ └── VerifierService.cs └── Program.cs ├── UnsecuredAPIKeys.Providers ├── UnsecuredAPIKeys.Providers.csproj ├── _Interfaces │ ├── ISearchProvider.cs │ └── IApiKeyProvider.cs ├── Common │ └── ValidationResult.cs ├── ApiProviderRegistry.cs ├── AI Providers │ ├── AnthropicProvider.cs │ ├── OpenAIProvider.cs │ └── GoogleProvider.cs ├── Search Providers │ └── GitHubSearchProvider.cs └── _Base │ └── BaseApiKeyProvider.cs ├── .github └── workflows │ ├── claude.yml │ └── claude-code-review.yml ├── .gitattributes ├── LICENSE ├── UnsecuredAPIKeys-OpenSource.sln ├── README.md ├── .gitignore └── CHANGELOG.md /UnsecuredAPIKeys.Data/Models/ApplicationSetting.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace UnsecuredAPIKeys.Data.Models 4 | { 5 | public class ApplicationSetting 6 | { 7 | [Key] public required string Key { get; init; } 8 | public required string Value { get; init; } 9 | public string? Description { get; init; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /UnsecuredAPIKeys.Data/Models/SearchQuery.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace UnsecuredAPIKeys.Data.Models 4 | { 5 | public class SearchQuery 6 | { 7 | [Key] public long Id { get; set; } 8 | public string Query { get; set; } = string.Empty; 9 | public bool IsEnabled { get; set; } 10 | public int SearchResultsCount { get; set; } 11 | 12 | public DateTime LastSearchUTC { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /UnsecuredAPIKeys.Data/Models/SearchProviderToken.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using UnsecuredAPIKeys.Data.Common; 3 | 4 | namespace UnsecuredAPIKeys.Data.Models 5 | { 6 | public class SearchProviderToken 7 | { 8 | [Key] public int Id { get; set; } 9 | public string Token { get; set; } = string.Empty; 10 | public SearchProviderEnum SearchProvider { get; set; } = SearchProviderEnum.Unknown; 11 | public bool IsEnabled { get; set; } 12 | 13 | // Make it nullable so we can identify never-used tokens 14 | public DateTime? LastUsedUTC { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /UnsecuredAPIKeys.CLI/appsettings.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "// README": "Copy this file to appsettings.json and configure your GitHub token", 3 | "// IMPORTANT": "Do NOT commit appsettings.json to version control", 4 | 5 | "GitHub": { 6 | "Token": "ghp_YOUR_GITHUB_TOKEN_HERE", 7 | "// TokenInfo": "Create at https://github.com/settings/tokens - requires 'public_repo' scope" 8 | }, 9 | 10 | "Database": { 11 | "Path": "unsecuredapikeys.db", 12 | "// PathInfo": "SQLite database file path. Can be absolute or relative." 13 | }, 14 | 15 | "Limits": { 16 | "MaxValidKeys": 50, 17 | "// MaxValidKeysInfo": "Lite version limit. For higher limits, use www.UnsecuredAPIKeys.com" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /UnsecuredAPIKeys.Providers/UnsecuredAPIKeys.Providers.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | net10.0 15 | enable 16 | enable 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /UnsecuredAPIKeys.Data/UnsecuredAPIKeys.Data.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /UnsecuredAPIKeys.Data/DesignTimeDbContextFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Design; 3 | 4 | namespace UnsecuredAPIKeys.Data 5 | { 6 | /// 7 | /// Factory for creating DBContext during EF Core design-time operations (migrations). 8 | /// Uses SQLite for the lite version. 9 | /// 10 | public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory 11 | { 12 | public DBContext CreateDbContext(string[] args) 13 | { 14 | var optionsBuilder = new DbContextOptionsBuilder(); 15 | 16 | // Use SQLite for the lite version 17 | optionsBuilder.UseSqlite("Data Source=unsecuredapikeys.db"); 18 | 19 | return new DBContext(optionsBuilder.Options); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /UnsecuredAPIKeys.Providers/_Interfaces/ISearchProvider.cs: -------------------------------------------------------------------------------- 1 | using UnsecuredAPIKeys.Data.Models; 2 | 3 | namespace UnsecuredAPIKeys.Providers._Interfaces 4 | { 5 | /// 6 | /// Defines the contract for a search provider used to find potential API keys. 7 | /// 8 | public interface ISearchProvider 9 | { 10 | /// 11 | /// Gets the name of the search provider (e.g., "GitHub", "GitLab"). 12 | /// 13 | string ProviderName { get; } 14 | 15 | /// 16 | /// Executes a search based on the provided query. 17 | /// 18 | /// The search query details. 19 | /// The API token to use for the search. 20 | /// A collection of RepoReference objects representing potential findings. 21 | Task> SearchAsync(SearchQuery query, SearchProviderToken? token); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /UnsecuredAPIKeys.Data/Common/ApiProviderAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace UnsecuredAPIKeys.Data.Common; 2 | 3 | [AttributeUsage(AttributeTargets.Class)] 4 | public class ApiProviderAttribute : Attribute 5 | { 6 | /// 7 | /// Whether this provider should be used by the Scraper bot 8 | /// 9 | public bool ScraperUse { get; set; } = true; 10 | 11 | /// 12 | /// Whether this provider should be used by the Verifier bot 13 | /// 14 | public bool VerificationUse { get; set; } = true; 15 | 16 | /// 17 | /// Creates an ApiProvider attribute with default usage (enabled for both scraper and verifier) 18 | /// 19 | public ApiProviderAttribute() 20 | { 21 | } 22 | 23 | /// 24 | /// Creates an ApiProvider attribute with specific usage flags 25 | /// 26 | /// Enable for scraper bot 27 | /// Enable for verifier bot 28 | public ApiProviderAttribute(bool scraperUse, bool verificationUse) 29 | { 30 | ScraperUse = scraperUse; 31 | VerificationUse = verificationUse; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /UnsecuredAPIKeys.Data/Models/APIKey.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using UnsecuredAPIKeys.Data.Common; 4 | using System.Text.Json.Serialization; // <-- Add this using directive 5 | 6 | namespace UnsecuredAPIKeys.Data.Models 7 | { 8 | public class APIKey 9 | { 10 | [Key] 11 | public long Id { get; set; } 12 | 13 | [Required] 14 | public required string ApiKey { get; set; } 15 | 16 | [JsonConverter(typeof(JsonStringEnumConverter))] 17 | public ApiStatusEnum Status { get; set; } 18 | 19 | [JsonConverter(typeof(JsonStringEnumConverter))] 20 | public ApiTypeEnum ApiType { get; set; } = ApiTypeEnum.Unknown; 21 | 22 | public SearchProviderEnum SearchProvider { get; set; } 23 | 24 | public DateTime? LastCheckedUTC { get; set; } 25 | public DateTime FirstFoundUTC { get; set; } 26 | public DateTime LastFoundUTC { get; set; } 27 | 28 | public int TimesDisplayed { get; set; } 29 | 30 | // Error tracking for verification failures 31 | public int ErrorCount { get; set; } = 0; 32 | 33 | // Navigation property to where this key was found 34 | public virtual ICollection References { get; set; } = []; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /UnsecuredAPIKeys.CLI/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace UnsecuredAPIKeys.CLI; 2 | 3 | /// 4 | /// Constants for the lite version of UnsecuredAPIKeys. 5 | /// Full version available at www.UnsecuredAPIKeys.com 6 | /// 7 | public static class LiteLimits 8 | { 9 | /// 10 | /// Maximum valid keys for lite version. 11 | /// 12 | /// WARNING: If you modify this limit, do NOT publish your database 13 | /// or results to a public repository. This would expose working API 14 | /// keys to malicious actors who could abuse them. 15 | /// 16 | /// For higher limits, use www.UnsecuredAPIKeys.com 17 | /// 18 | public const int MAX_VALID_KEYS = 50; 19 | 20 | /// 21 | /// Delay between verification batches (milliseconds). 22 | /// 23 | public const int VERIFICATION_DELAY_MS = 1000; 24 | 25 | /// 26 | /// Delay between GitHub search queries (milliseconds). 27 | /// 28 | public const int SEARCH_DELAY_MS = 5000; 29 | 30 | /// 31 | /// Number of keys to process per verification batch. 32 | /// 33 | public const int VERIFICATION_BATCH_SIZE = 10; 34 | } 35 | 36 | /// 37 | /// Application-wide constants. 38 | /// 39 | public static class AppInfo 40 | { 41 | public const string Name = "UnsecuredAPIKeys Lite"; 42 | public const string Version = "1.0.0"; 43 | public const string FullVersionUrl = "www.UnsecuredAPIKeys.com"; 44 | public const string DatabaseName = "unsecuredapikeys.db"; 45 | } 46 | -------------------------------------------------------------------------------- /UnsecuredAPIKeys.Data/Common/CommonEnums.cs: -------------------------------------------------------------------------------- 1 | namespace UnsecuredAPIKeys.Data.Common 2 | { 3 | /// 4 | /// Search provider for finding API keys. 5 | /// Lite version: GitHub only. 6 | /// Full version: www.UnsecuredAPIKeys.com 7 | /// 8 | public enum SearchProviderEnum 9 | { 10 | Unknown = -99, 11 | GitHub = 1 12 | } 13 | 14 | /// 15 | /// Status of an API key in the system. 16 | /// 17 | public enum ApiStatusEnum 18 | { 19 | /// The key was found but not yet checked for validity. 20 | Unverified = -99, 21 | 22 | /// The key was checked and is valid/working. 23 | Valid = 1, 24 | 25 | /// The key was checked and is not working (invalid, expired, revoked, etc.). 26 | Invalid = 0, 27 | 28 | /// The key is valid but has no credits/quota. 29 | ValidNoCredits = 7, 30 | 31 | /// The key was checked and is erroring out for some reason. 32 | Error = 6 33 | } 34 | 35 | /// 36 | /// Type of API provider. 37 | /// Lite version: OpenAI, Anthropic, Google only. 38 | /// Full version with all providers: www.UnsecuredAPIKeys.com 39 | /// 40 | public enum ApiTypeEnum 41 | { 42 | Unknown = -99, 43 | 44 | // AI Services - Lite version only supports these 3 45 | OpenAI = 100, 46 | AnthropicClaude = 120, 47 | GoogleAI = 130 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /UnsecuredAPIKeys.Providers/_Interfaces/IApiKeyProvider.cs: -------------------------------------------------------------------------------- 1 | using UnsecuredAPIKeys.Data.Common; 2 | using UnsecuredAPIKeys.Providers.Common; 3 | 4 | namespace UnsecuredAPIKeys.Providers._Interfaces 5 | { 6 | /// 7 | /// Defines the contract for an API key provider, responsible for 8 | /// identifying and validating keys for a specific service. 9 | /// Lite version: OpenAI, Anthropic, Google only. 10 | /// Full version: www.UnsecuredAPIKeys.com 11 | /// 12 | public interface IApiKeyProvider 13 | { 14 | /// 15 | /// Gets the unique name of the provider (e.g., "OpenAI", "Anthropic"). 16 | /// 17 | string ProviderName { get; } 18 | 19 | /// 20 | /// Gets the corresponding ApiTypeEnum value for this provider. 21 | /// 22 | ApiTypeEnum ApiType { get; } 23 | 24 | /// 25 | /// Gets the list of regex patterns used to identify potential keys for this provider. 26 | /// 27 | IEnumerable RegexPatterns { get; } 28 | 29 | /// 30 | /// Asynchronously validates the given API key against the provider's service. 31 | /// 32 | /// The API key string to validate. 33 | /// The IHttpClientFactory for creating HttpClient instances. 34 | /// A ValidationResult indicating the outcome of the validation attempt. 35 | Task ValidateKeyAsync(string apiKey, IHttpClientFactory httpClientFactory); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /UnsecuredAPIKeys.Data/Models/RepoReference.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace UnsecuredAPIKeys.Data.Models 4 | { 5 | public class RepoReference 6 | { 7 | [Key] 8 | public long Id { get; set; } 9 | public long APIKeyId { get; set; } // Foreign key to APIKey 10 | public virtual APIKey? APIKey { get; set; } // Navigation property 11 | 12 | // Repository information 13 | [Required] 14 | public string? RepoURL { get; set; } // Just base repo URL 15 | public string? RepoOwner { get; set; } // Owner username/organization 16 | public string? RepoName { get; set; } // Repository name 17 | public string? RepoDescription { get; set; } 18 | public long RepoId { get; set; } // GitHub's repo ID 19 | 20 | // File information 21 | [Required] 22 | public string? FileURL { get; set; } // Full path with commit hash 23 | public string? FileName { get; set; } 24 | public string? FilePath { get; set; } // Path within repo 25 | public string? FileSHA { get; set; } 26 | public string? ApiContentUrl { get; set; } // URL to fetch raw content via API 27 | 28 | // Context information 29 | public string? CodeContext { get; set; } // Surrounding code 30 | public int LineNumber { get; set; } 31 | 32 | // Discovery metadata 33 | public long SearchQueryId { get; set; } // Which query found this 34 | public DateTime FoundUTC { get; set; } 35 | public string? Provider { get; set; } // e.g., GitHub, GitLab 36 | public string? Branch { get; set; } // Branch where the file was found 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/claude.yml: -------------------------------------------------------------------------------- 1 | name: Claude Code 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | pull_request_review_comment: 7 | types: [created] 8 | issues: 9 | types: [opened, assigned] 10 | pull_request_review: 11 | types: [submitted] 12 | 13 | jobs: 14 | claude: 15 | if: | 16 | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || 17 | (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || 18 | (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || 19 | (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) 20 | runs-on: ubuntu-latest 21 | permissions: 22 | contents: read 23 | pull-requests: read 24 | issues: read 25 | id-token: write 26 | actions: read # Required for Claude to read CI results on PRs 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 1 32 | 33 | - name: Run Claude Code 34 | id: claude 35 | uses: anthropics/claude-code-action@v1 36 | with: 37 | claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} 38 | 39 | # This is an optional setting that allows Claude to read CI results on PRs 40 | additional_permissions: | 41 | actions: read 42 | 43 | # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. 44 | # prompt: 'Update the pull request description to include a summary of changes.' 45 | 46 | # Optional: Add claude_args to customize behavior and configuration 47 | # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md 48 | # or https://code.claude.com/docs/en/cli-reference for available options 49 | # claude_args: '--allowed-tools Bash(gh pr:*)' 50 | 51 | -------------------------------------------------------------------------------- /.github/workflows/claude-code-review.yml: -------------------------------------------------------------------------------- 1 | name: Claude Code Review 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize] 6 | # Optional: Only run on specific file changes 7 | # paths: 8 | # - "src/**/*.ts" 9 | # - "src/**/*.tsx" 10 | # - "src/**/*.js" 11 | # - "src/**/*.jsx" 12 | 13 | jobs: 14 | claude-review: 15 | # Optional: Filter by PR author 16 | # if: | 17 | # github.event.pull_request.user.login == 'external-contributor' || 18 | # github.event.pull_request.user.login == 'new-developer' || 19 | # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' 20 | 21 | runs-on: ubuntu-latest 22 | permissions: 23 | contents: read 24 | pull-requests: read 25 | issues: read 26 | id-token: write 27 | 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v4 31 | with: 32 | fetch-depth: 1 33 | 34 | - name: Run Claude Code Review 35 | id: claude-review 36 | uses: anthropics/claude-code-action@v1 37 | with: 38 | claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} 39 | prompt: | 40 | REPO: ${{ github.repository }} 41 | PR NUMBER: ${{ github.event.pull_request.number }} 42 | 43 | Please review this pull request and provide feedback on: 44 | - Code quality and best practices 45 | - Potential bugs or issues 46 | - Performance considerations 47 | - Security concerns 48 | - Test coverage 49 | 50 | Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback. 51 | 52 | Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR. 53 | 54 | # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md 55 | # or https://code.claude.com/docs/en/cli-reference for available options 56 | 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:*)"' 57 | 58 | -------------------------------------------------------------------------------- /UnsecuredAPIKeys.CLI/UnsecuredAPIKeys.CLI.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net10.0 6 | enable 7 | enable 8 | unsecuredapikeys 9 | UnsecuredAPIKeys.CLI 10 | UnsecuredAPIKeys - Open Source 11 | https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/ 12 | https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/ 13 | https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/ 14 | https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/ 15 | git 16 | True 17 | latest-recommended 18 | 1.0.0 19 | 20 | 21 | true 22 | true 23 | true 24 | true 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | PreserveNewest 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /UnsecuredAPIKeys.Providers/Common/ValidationResult.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace UnsecuredAPIKeys.Providers.Common 4 | { 5 | public enum ValidationAttemptStatus 6 | { 7 | Valid, // Key is valid and working. 8 | Unauthorized, // Key is explicitly unauthorized (e.g., HTTP 401). 9 | HttpError, // An HTTP error occurred (e.g., 403, 404, 429, 5xx). 10 | NetworkError, // A network-level error occurred (e.g., DNS, timeout, connection refused). 11 | ProviderSpecificError // An unexpected error within the provider's logic. 12 | } 13 | 14 | public class ModelInfo 15 | { 16 | public string ModelId { get; set; } = string.Empty; 17 | public string? DisplayName { get; set; } 18 | public string? Description { get; set; } 19 | public string? Version { get; set; } 20 | public long? InputTokenLimit { get; set; } 21 | public long? OutputTokenLimit { get; set; } 22 | public List? SupportedMethods { get; set; } 23 | public float? Temperature { get; set; } 24 | public float? TopP { get; set; } 25 | public int? TopK { get; set; } 26 | public float? MaxTemperature { get; set; } 27 | public string? ModelGroup { get; set; } // For grouping similar models 28 | } 29 | 30 | public class ValidationResult 31 | { 32 | public ValidationAttemptStatus Status { get; set; } 33 | public HttpStatusCode? HttpStatusCode { get; set; } // Null if not an HTTP-related error. 34 | public string? Detail { get; set; } = string.Empty; // Optional error message or detail. 35 | 36 | // Model information discovered during validation 37 | public List? AvailableModels { get; set; } 38 | 39 | // Helper factory methods for convenience 40 | public static ValidationResult Success(HttpStatusCode statusCode, List? models = null) => 41 | new() { Status = ValidationAttemptStatus.Valid, HttpStatusCode = statusCode, AvailableModels = models }; 42 | 43 | public static ValidationResult IsUnauthorized(HttpStatusCode statusCode, string? detail = null) => 44 | new() { Status = ValidationAttemptStatus.Unauthorized, HttpStatusCode = statusCode, Detail = detail }; 45 | 46 | public static ValidationResult HasHttpError(HttpStatusCode statusCode, string? detail = null) => 47 | new() { Status = ValidationAttemptStatus.HttpError, HttpStatusCode = statusCode, Detail = detail }; 48 | 49 | public static ValidationResult HasNetworkError(string detail) => 50 | new() { Status = ValidationAttemptStatus.NetworkError, Detail = detail }; 51 | 52 | public static ValidationResult HasProviderSpecificError(string detail) => 53 | new() { Status = ValidationAttemptStatus.ProviderSpecificError, Detail = detail }; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /UnsecuredAPIKeys.Data/DBContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using UnsecuredAPIKeys.Data.Models; 3 | 4 | namespace UnsecuredAPIKeys.Data 5 | { 6 | /// 7 | /// SQLite database context for UnsecuredAPIKeys Lite. 8 | /// Full version with PostgreSQL: www.UnsecuredAPIKeys.com 9 | /// 10 | public class DBContext : DbContext 11 | { 12 | private readonly string _dbPath; 13 | 14 | public DBContext(DbContextOptions options) : base(options) 15 | { 16 | _dbPath = "unsecuredapikeys.db"; 17 | } 18 | 19 | public DBContext(string dbPath = "unsecuredapikeys.db") 20 | { 21 | _dbPath = dbPath; 22 | } 23 | 24 | // Core entities 25 | public DbSet APIKeys { get; set; } = null!; 26 | public DbSet RepoReferences { get; set; } = null!; 27 | public DbSet SearchQueries { get; set; } = null!; 28 | public DbSet SearchProviderTokens { get; set; } = null!; 29 | public DbSet ApplicationSettings { get; set; } = null!; 30 | 31 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 32 | { 33 | if (!optionsBuilder.IsConfigured) 34 | { 35 | optionsBuilder.UseSqlite($"Data Source={_dbPath}"); 36 | } 37 | } 38 | 39 | protected override void OnModelCreating(ModelBuilder modelBuilder) 40 | { 41 | base.OnModelCreating(modelBuilder); 42 | 43 | // APIKey indexes for performance 44 | modelBuilder.Entity() 45 | .HasIndex(k => k.ApiKey) 46 | .IsUnique() 47 | .HasDatabaseName("IX_APIKeys_ApiKey"); 48 | 49 | modelBuilder.Entity() 50 | .HasIndex(k => new { k.Status, k.ApiType }) 51 | .HasDatabaseName("IX_APIKeys_Status_ApiType"); 52 | 53 | modelBuilder.Entity() 54 | .HasIndex(k => k.LastCheckedUTC) 55 | .HasDatabaseName("IX_APIKeys_LastCheckedUTC"); 56 | 57 | modelBuilder.Entity() 58 | .HasIndex(k => k.Status) 59 | .HasDatabaseName("IX_APIKeys_Status"); 60 | 61 | // RepoReference indexes 62 | modelBuilder.Entity() 63 | .HasIndex(r => r.APIKeyId) 64 | .HasDatabaseName("IX_RepoReferences_ApiKeyId"); 65 | 66 | // SearchQuery indexes 67 | modelBuilder.Entity() 68 | .HasIndex(q => new { q.IsEnabled, q.LastSearchUTC }) 69 | .HasDatabaseName("IX_SearchQueries_IsEnabled_LastSearchUTC"); 70 | 71 | // SearchProviderToken indexes 72 | modelBuilder.Entity() 73 | .HasIndex(t => t.SearchProvider) 74 | .HasDatabaseName("IX_SearchProviderTokens_SearchProvider"); 75 | 76 | // Relationships 77 | modelBuilder.Entity() 78 | .HasOne(r => r.APIKey) 79 | .WithMany(k => k.References) 80 | .HasForeignKey(r => r.APIKeyId) 81 | .OnDelete(DeleteBehavior.Cascade); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /UnsecuredAPIKeys.Providers/ApiProviderRegistry.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using UnsecuredAPIKeys.Data.Common; 3 | using UnsecuredAPIKeys.Providers._Interfaces; 4 | 5 | namespace UnsecuredAPIKeys.Providers 6 | { 7 | public static class ApiProviderRegistry 8 | { 9 | private static readonly Lazy> _allProviders = new(() => 10 | { 11 | return [.. Assembly.GetExecutingAssembly() 12 | .GetTypes() 13 | .Where(type => type.GetCustomAttribute() != null 14 | && typeof(IApiKeyProvider).IsAssignableFrom(type) 15 | && !type.IsInterface 16 | && !type.IsAbstract) 17 | .Select(type => (IApiKeyProvider)Activator.CreateInstance(type)!)]; 18 | }); 19 | 20 | private static readonly Lazy> _scraperProviders = new(() => 21 | { 22 | return [.. Assembly.GetExecutingAssembly() 23 | .GetTypes() 24 | .Where(type => { 25 | var attr = type.GetCustomAttribute(); 26 | return attr != null 27 | && attr.ScraperUse 28 | && typeof(IApiKeyProvider).IsAssignableFrom(type) 29 | && !type.IsInterface 30 | && !type.IsAbstract; 31 | }) 32 | .Select(type => (IApiKeyProvider)Activator.CreateInstance(type)!)]; 33 | }); 34 | 35 | private static readonly Lazy> _verifierProviders = new(() => 36 | { 37 | return [.. Assembly.GetExecutingAssembly() 38 | .GetTypes() 39 | .Where(type => { 40 | var attr = type.GetCustomAttribute(); 41 | return attr != null 42 | && attr.VerificationUse 43 | && typeof(IApiKeyProvider).IsAssignableFrom(type) 44 | && !type.IsInterface 45 | && !type.IsAbstract; 46 | }) 47 | .Select(type => (IApiKeyProvider)Activator.CreateInstance(type)!)]; 48 | }); 49 | 50 | /// 51 | /// Gets all providers with ApiProvider attribute (backward compatibility) 52 | /// 53 | public static IReadOnlyList Providers => _allProviders.Value; 54 | 55 | /// 56 | /// Gets providers that are enabled for scraper use 57 | /// 58 | public static IReadOnlyList ScraperProviders => _scraperProviders.Value; 59 | 60 | /// 61 | /// Gets providers that are enabled for verifier use 62 | /// 63 | public static IReadOnlyList VerifierProviders => _verifierProviders.Value; 64 | 65 | /// 66 | /// Gets providers for a specific bot type 67 | /// 68 | /// The type of bot (Scraper or Verifier) 69 | /// List of providers enabled for the specified bot type 70 | public static IReadOnlyList GetProvidersForBot(BotType botType) 71 | { 72 | return botType switch 73 | { 74 | BotType.Scraper => ScraperProviders, 75 | BotType.Verifier => VerifierProviders, 76 | _ => Providers 77 | }; 78 | } 79 | } 80 | 81 | /// 82 | /// Enumeration of bot types for provider filtering 83 | /// 84 | public enum BotType 85 | { 86 | Scraper, 87 | Verifier 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | UnsecuredAPIKeys Open Source License 2 | Based on MIT License with Attribution Requirements 3 | 4 | Copyright (c) 2025 TSCarterJr 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | 1. The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | 2. MANDATORY UI ATTRIBUTION REQUIREMENT: 17 | Any software, application, or system that incorporates, uses, modifies, or 18 | derives from ANY portion of this Software (including but not limited to APIs, 19 | backend processes, algorithms, data structures, bots, validation logic, or any 20 | other component) and provides a public-facing user interface MUST display a 21 | prominent link to the original project repository: 22 | https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource 23 | 24 | This attribution must be: 25 | - Clearly visible in the user interface 26 | - Accessible from the main/home page or footer 27 | - Contain the text "Based on UnsecuredAPIKeys Open Source" or similar 28 | - Link directly to https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource 29 | 30 | This requirement applies regardless of the extent of modification, whether the 31 | entire codebase is used or only small portions, and whether the derivative work 32 | is commercial or non-commercial. 33 | 34 | 3. SCOPE OF APPLICATION: 35 | This attribution requirement applies to any use of the Software, including but 36 | not limited to: 37 | - Using the backend APIs or database schemas 38 | - Implementing the validation algorithms or provider patterns 39 | - Using the bot architecture or scraping logic 40 | - Incorporating the data models or business logic 41 | - Using any code, concepts, or implementations from this project 42 | 43 | 4. EDUCATIONAL PURPOSE AND USER RESPONSIBILITY: 44 | This Software is provided for EDUCATIONAL and SECURITY RESEARCH purposes only. 45 | 46 | By using this Software, you acknowledge and agree that: 47 | - You are solely responsible for your use of this Software 48 | - You will comply with all applicable laws and regulations 49 | - You will not use this Software for unauthorized access to any systems 50 | - You will not use discovered API keys for any malicious or illegal purposes 51 | - You will practice responsible disclosure when discovering exposed credentials 52 | 53 | The author(s) and copyright holder(s) expressly disclaim any responsibility for: 54 | - How you choose to use this Software 55 | - Any actions you take based on information obtained through this Software 56 | - Any legal consequences arising from your use or misuse of this Software 57 | - Any damages caused by unauthorized or unethical use of this Software 58 | 59 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 60 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 61 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 62 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 63 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 64 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 65 | SOFTWARE. 66 | 67 | USE OF THIS SOFTWARE FOR ANY ILLEGAL, UNAUTHORIZED, OR UNETHICAL PURPOSES IS 68 | STRICTLY PROHIBITED. THE USER ASSUMES ALL RISKS AND FULL RESPONSIBILITY FOR 69 | THEIR ACTIONS. 70 | 71 | VIOLATION OF THE ATTRIBUTION REQUIREMENT CONSTITUTES COPYRIGHT INFRINGEMENT 72 | AND BREACH OF LICENSE TERMS, SUBJECT TO LEGAL ACTION AND DAMAGES. 73 | -------------------------------------------------------------------------------- /UnsecuredAPIKeys-OpenSource.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.13.35931.197 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnsecuredAPIKeys.Data", "UnsecuredAPIKeys.Data\UnsecuredAPIKeys.Data.csproj", "{F4BC4B91-7945-4171-BA6E-BECFEF771A21}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnsecuredAPIKeys.Providers", "UnsecuredAPIKeys.Providers\UnsecuredAPIKeys.Providers.csproj", "{7389D703-A7C1-4853-A857-C1C97C1C6178}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnsecuredAPIKeys.CLI", "UnsecuredAPIKeys.CLI\UnsecuredAPIKeys.CLI.csproj", "{1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Debug|x64 = Debug|x64 16 | Debug|x86 = Debug|x86 17 | Release|Any CPU = Release|Any CPU 18 | Release|x64 = Release|x64 19 | Release|x86 = Release|x86 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Debug|x64.ActiveCfg = Debug|Any CPU 25 | {F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Debug|x64.Build.0 = Debug|Any CPU 26 | {F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Debug|x86.ActiveCfg = Debug|Any CPU 27 | {F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Debug|x86.Build.0 = Debug|Any CPU 28 | {F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Release|x64.ActiveCfg = Release|Any CPU 31 | {F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Release|x64.Build.0 = Release|Any CPU 32 | {F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Release|x86.ActiveCfg = Release|Any CPU 33 | {F4BC4B91-7945-4171-BA6E-BECFEF771A21}.Release|x86.Build.0 = Release|Any CPU 34 | {7389D703-A7C1-4853-A857-C1C97C1C6178}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {7389D703-A7C1-4853-A857-C1C97C1C6178}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {7389D703-A7C1-4853-A857-C1C97C1C6178}.Debug|x64.ActiveCfg = Debug|Any CPU 37 | {7389D703-A7C1-4853-A857-C1C97C1C6178}.Debug|x64.Build.0 = Debug|Any CPU 38 | {7389D703-A7C1-4853-A857-C1C97C1C6178}.Debug|x86.ActiveCfg = Debug|Any CPU 39 | {7389D703-A7C1-4853-A857-C1C97C1C6178}.Debug|x86.Build.0 = Debug|Any CPU 40 | {7389D703-A7C1-4853-A857-C1C97C1C6178}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {7389D703-A7C1-4853-A857-C1C97C1C6178}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {7389D703-A7C1-4853-A857-C1C97C1C6178}.Release|x64.ActiveCfg = Release|Any CPU 43 | {7389D703-A7C1-4853-A857-C1C97C1C6178}.Release|x64.Build.0 = Release|Any CPU 44 | {7389D703-A7C1-4853-A857-C1C97C1C6178}.Release|x86.ActiveCfg = Release|Any CPU 45 | {7389D703-A7C1-4853-A857-C1C97C1C6178}.Release|x86.Build.0 = Release|Any CPU 46 | {1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Debug|x64.ActiveCfg = Debug|Any CPU 49 | {1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Debug|x64.Build.0 = Debug|Any CPU 50 | {1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Debug|x86.ActiveCfg = Debug|Any CPU 51 | {1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Debug|x86.Build.0 = Debug|Any CPU 52 | {1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Release|x64.ActiveCfg = Release|Any CPU 55 | {1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Release|x64.Build.0 = Release|Any CPU 56 | {1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Release|x86.ActiveCfg = Release|Any CPU 57 | {1F7908B2-B234-4D58-A0B8-8D4F2AA130A4}.Release|x86.Build.0 = Release|Any CPU 58 | EndGlobalSection 59 | GlobalSection(SolutionProperties) = preSolution 60 | HideSolutionNode = FALSE 61 | EndGlobalSection 62 | GlobalSection(ExtensibilityGlobals) = postSolution 63 | SolutionGuid = {FEDEE8B0-6BD4-48B2-BA35-31BFC542BC0D} 64 | EndGlobalSection 65 | EndGlobal 66 | -------------------------------------------------------------------------------- /UnsecuredAPIKeys.Providers/AI Providers/AnthropicProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http.Headers; 3 | using System.Text; 4 | using System.Text.Json; 5 | 6 | using Microsoft.Extensions.Logging; 7 | 8 | using UnsecuredAPIKeys.Data.Common; 9 | using UnsecuredAPIKeys.Providers._Base; 10 | using UnsecuredAPIKeys.Providers.Common; 11 | 12 | namespace UnsecuredAPIKeys.Providers.AI_Providers 13 | { 14 | /// 15 | /// Provider implementation for handling Anthropic (Claude) API keys with enhanced validation. 16 | /// 17 | [ApiProvider] 18 | public class AnthropicProvider : BaseApiKeyProvider 19 | { 20 | private const string API_ENDPOINT = "https://api.anthropic.com/v1/messages"; 21 | private const string ANTHROPIC_VERSION = "2023-06-01"; 22 | private const string DEFAULT_MODEL = "claude-sonnet-4-20250514"; 23 | private const int MAX_RETRIES = 3; 24 | private const int TIMEOUT_SECONDS = 30; 25 | 26 | // Anthropic-specific response keywords (additional to base class) 27 | private static readonly HashSet InvalidKeyIndicators = new(StringComparer.OrdinalIgnoreCase) 28 | { 29 | "invalid_api_key", 30 | "authentication_error", 31 | "invalid x-api-key", 32 | "unauthorized" 33 | }; 34 | 35 | public override string ProviderName => "Anthropic"; 36 | 37 | public override ApiTypeEnum ApiType => ApiTypeEnum.AnthropicClaude; 38 | 39 | // Enhanced regex patterns with compiled regex for better performance 40 | public override IEnumerable RegexPatterns => 41 | [ 42 | @"sk-ant-api\d{0,2}-[a-zA-Z0-9\-_]{40,120}", 43 | @"sk-ant-[a-zA-Z0-9\-_]{40,95}", 44 | @"sk-ant-v\d+-[a-zA-Z0-9\-_]{40,95}", 45 | @"sk-ant-[a-zA-Z0-9]+-[a-zA-Z0-9\-_]{20,120}", 46 | @"sk-ant-[a-zA-Z0-9]{40,64}", 47 | @"\bsk-ant-[a-zA-Z0-9\-_]{20,120}\b" 48 | ]; 49 | 50 | public AnthropicProvider() : base() 51 | { 52 | } 53 | 54 | public AnthropicProvider(ILogger? logger) : base(logger) 55 | { 56 | } 57 | 58 | protected override async Task ValidateKeyWithHttpClientAsync(string apiKey, HttpClient httpClient) 59 | { 60 | using var request = CreateValidationRequest(apiKey); 61 | 62 | var response = await httpClient.SendAsync(request); 63 | var responseBody = await response.Content.ReadAsStringAsync(); 64 | 65 | _logger?.LogDebug("Anthropic API response: Status={StatusCode}, Body={Body}", 66 | response.StatusCode, responseBody.Length > 200 ? responseBody.Substring(0, 200) + "..." : responseBody); 67 | 68 | return InterpretResponse(response.StatusCode, responseBody); 69 | } 70 | 71 | private HttpRequestMessage CreateValidationRequest(string apiKey) 72 | { 73 | var request = new HttpRequestMessage(HttpMethod.Post, API_ENDPOINT); 74 | 75 | // Set headers 76 | request.Headers.Add("x-api-key", apiKey); 77 | request.Headers.Add("anthropic-version", ANTHROPIC_VERSION); 78 | request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 79 | 80 | // Ultra-minimal payload for lowest cost 81 | var payload = new 82 | { 83 | model = DEFAULT_MODEL, 84 | max_tokens = 1, 85 | messages = new[] 86 | { 87 | new { role = "user", content = "1" } 88 | }, 89 | temperature = 0, 90 | stop_sequences = new[] { "1", "2", "3", "4", "5" } 91 | }; 92 | 93 | var jsonContent = JsonSerializer.Serialize(payload); 94 | request.Content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); 95 | 96 | return request; 97 | } 98 | 99 | private ValidationResult InterpretResponse(HttpStatusCode statusCode, string responseBody) 100 | { 101 | // Success cases 102 | if (IsSuccessStatusCode(statusCode)) 103 | { 104 | return ValidationResult.Success(statusCode); 105 | } 106 | 107 | var bodyLower = responseBody.ToLowerInvariant(); 108 | 109 | switch (statusCode) 110 | { 111 | case HttpStatusCode.Unauthorized: // 401 112 | if (ContainsAny(bodyLower, InvalidKeyIndicators)) 113 | { 114 | return ValidationResult.IsUnauthorized(statusCode); 115 | } 116 | return ValidationResult.IsUnauthorized(statusCode); 117 | 118 | case HttpStatusCode.Forbidden: // 403 119 | if (ContainsAny(bodyLower, PermissionIndicators)) 120 | { 121 | _logger?.LogInformation("API key has permission restrictions but is valid"); 122 | return ValidationResult.Success(statusCode); 123 | } 124 | return ValidationResult.HasHttpError(statusCode, $"Forbidden: {TruncateResponse(responseBody)}"); 125 | 126 | case HttpStatusCode.BadRequest: // 400 127 | if (ContainsAny(bodyLower, QuotaIndicators)) 128 | { 129 | _logger?.LogInformation("API key is valid but has quota/billing issues"); 130 | return ValidationResult.Success(statusCode); 131 | } 132 | return ValidationResult.HasHttpError(statusCode, $"Bad request: {TruncateResponse(responseBody)}"); 133 | 134 | case HttpStatusCode.PaymentRequired: // 402 135 | case HttpStatusCode.TooManyRequests: // 429 136 | return ValidationResult.Success(statusCode); 137 | 138 | case HttpStatusCode.ServiceUnavailable: // 503 139 | case HttpStatusCode.GatewayTimeout: // 504 140 | return ValidationResult.HasNetworkError($"Service unavailable: {statusCode}"); 141 | 142 | default: 143 | if (ContainsAny(bodyLower, QuotaIndicators)) 144 | { 145 | return ValidationResult.Success(statusCode); 146 | } 147 | 148 | return ValidationResult.HasHttpError(statusCode, 149 | $"API request failed with status {statusCode}. Response: {TruncateResponse(responseBody)}"); 150 | } 151 | } 152 | 153 | protected override bool IsValidKeyFormat(string apiKey) 154 | { 155 | if (string.IsNullOrWhiteSpace(apiKey) || apiKey.Length < 20) 156 | return false; 157 | 158 | if (!apiKey.StartsWith("sk-ant-", StringComparison.Ordinal)) 159 | return false; 160 | 161 | return apiKey.All(c => char.IsLetterOrDigit(c) || c == '-' || c == '_'); 162 | } 163 | } 164 | } -------------------------------------------------------------------------------- /UnsecuredAPIKeys.Providers/Search Providers/GitHubSearchProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Octokit; 3 | using UnsecuredAPIKeys.Data; 4 | using UnsecuredAPIKeys.Data.Models; 5 | using UnsecuredAPIKeys.Providers._Interfaces; 6 | // Assuming logging might be needed later 7 | 8 | namespace UnsecuredAPIKeys.Providers.Search_Providers 9 | { 10 | /// 11 | /// Implements the ISearchProvider interface for searching code on GitHub. 12 | /// 13 | public class GitHubSearchProvider(DBContext dbContext, ILogger? logger = null) : ISearchProvider 14 | { 15 | /// 16 | public string ProviderName => "GitHub"; 17 | 18 | /// 19 | public async Task> SearchAsync(SearchQuery query, SearchProviderToken? token) 20 | { 21 | if (token == null || string.IsNullOrWhiteSpace(token.Token)) 22 | { 23 | logger?.LogError("GitHub token is missing or invalid."); // Use _logger field 24 | throw new ArgumentNullException(nameof(token), "A valid GitHub token is required."); 25 | } 26 | 27 | if (query == null || string.IsNullOrWhiteSpace(query.Query)) 28 | { 29 | logger?.LogError("Search query is missing or invalid."); // Use _logger field 30 | throw new ArgumentNullException(nameof(query), "A valid search query is required."); 31 | } 32 | 33 | var client = new GitHubClient(new ProductHeaderValue("UnsecuredAPIKeys-Scraper")) 34 | { 35 | Credentials = new Credentials(token.Token) 36 | }; 37 | 38 | var results = new List(); 39 | int page = 1; 40 | const int perPage = 100; // Max allowed by GitHub API 41 | 42 | try 43 | { 44 | logger?.LogInformation("Starting GitHub search for query: {Query}", query.Query); // Use _logger field 45 | 46 | while (true) // Loop to handle pagination 47 | { 48 | var request = new SearchCodeRequest(query.Query) 49 | { 50 | // Consider adding filters like language, user, repo if needed 51 | Page = page, 52 | PerPage = perPage 53 | // Order = SortDirection.Descending 54 | }; 55 | 56 | SearchCodeResult searchResult; 57 | try 58 | { 59 | searchResult = await client.Search.SearchCode(request); 60 | 61 | if (page == 1) 62 | { 63 | query.SearchResultsCount = searchResult.TotalCount; 64 | dbContext.SearchQueries.Update(query); 65 | await dbContext.SaveChangesAsync(); 66 | } 67 | } 68 | catch (RateLimitExceededException ex) 69 | { 70 | logger?.LogWarning("GitHub API rate limit exceeded. Waiting until {ResetTime}.", ex.Reset.ToString("o")); // Use _logger field 71 | 72 | // Wait until the rate limit resets 73 | var delay = ex.Reset - DateTimeOffset.UtcNow; 74 | if (delay > TimeSpan.Zero) 75 | { 76 | if (delay.TotalMinutes > 1) 77 | { 78 | Environment.Exit(200); 79 | } 80 | 81 | await Task.Delay(delay); 82 | } 83 | continue; // Retry the same page 84 | } 85 | catch (ApiException apiEx) 86 | { 87 | logger?.LogError(apiEx, "GitHub API error during search on page {Page}. Status: {StatusCode}", page, apiEx.StatusCode); // Use _logger field 88 | // Decide how to handle API errors (e.g., stop, retry after delay) 89 | break; // Stop searching on API error for now 90 | } 91 | 92 | if (searchResult?.Items == null || !searchResult.Items.Any()) 93 | { 94 | logger?.LogInformation("No more results found for query '{Query}' on page {Page}.", query.Query, page); // Use _logger field 95 | break; // No more results 96 | } 97 | 98 | logger?.LogDebug("Found {Count} results on page {Page} for query '{Query}'.", searchResult.Items.Count, page, query.Query); // Use _logger field 99 | 100 | foreach (var item in searchResult.Items) 101 | { 102 | results.Add(new RepoReference 103 | { 104 | SearchQueryId = query.Id, 105 | Provider = ProviderName, 106 | RepoOwner = item.Repository?.Owner?.Login, // Corrected field name 107 | RepoName = item.Repository?.Name, // Corrected field name 108 | FilePath = item.Path, 109 | FileURL = item.HtmlUrl, // HTML URL for viewing in browser 110 | ApiContentUrl = item.Url, // API URL for fetching content 111 | Branch = item.Repository?.DefaultBranch, // Assuming default branch, might need refinement 112 | FileSHA = item.Sha, // Corrected field name (SHA of the file blob) 113 | FoundUTC = DateTime.UtcNow, // Corrected field name (Record when this specific reference was found) 114 | RepoURL = item.Repository?.HtmlUrl, 115 | RepoDescription = item.Repository?.Description, 116 | FileName = item.Name 117 | }); 118 | } 119 | 120 | // Basic check to prevent infinite loops if API behaves unexpectedly 121 | if (searchResult.Items.Count < perPage || results.Count >= 1000) // GitHub limits code search results to 1000 122 | { 123 | 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 124 | break; 125 | } 126 | 127 | page++; // Move to the next page 128 | await Task.Delay(TimeSpan.FromSeconds(2)); // Add a small delay to be polite to the API 129 | } 130 | } 131 | catch (Exception ex) 132 | { 133 | logger?.LogError(ex, "An unexpected error occurred during GitHub search for query: {Query}", query.Query); // Use _logger field 134 | } 135 | 136 | logger?.LogInformation("Completed GitHub search for query '{Query}'. Found {Count} potential references.", query.Query, results.Count); // Use _logger field 137 | return results; 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /UnsecuredAPIKeys.CLI/Services/DatabaseService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Spectre.Console; 3 | using UnsecuredAPIKeys.Data; 4 | using UnsecuredAPIKeys.Data.Common; 5 | using UnsecuredAPIKeys.Data.Models; 6 | 7 | namespace UnsecuredAPIKeys.CLI.Services; 8 | 9 | /// 10 | /// Service for database initialization and common operations. 11 | /// 12 | public class DatabaseService(string dbPath = "unsecuredapikeys.db") 13 | { 14 | public async Task InitializeDatabaseAsync() 15 | { 16 | var dbContext = new DBContext(dbPath); 17 | 18 | // Ensure database is created 19 | await dbContext.Database.EnsureCreatedAsync(); 20 | 21 | // Seed default data if needed 22 | await SeedDefaultDataAsync(dbContext); 23 | 24 | return dbContext; 25 | } 26 | 27 | private async Task SeedDefaultDataAsync(DBContext dbContext) 28 | { 29 | // Add default search queries if none exist 30 | if (!await dbContext.SearchQueries.AnyAsync()) 31 | { 32 | var defaultQueries = new[] 33 | { 34 | // OpenAI patterns 35 | "sk-proj-", 36 | "sk-or-v1-", 37 | "sk-", 38 | "OPENAI_API_KEY", 39 | "openai.api_key", 40 | "chatgpt api key", 41 | "gpt-4 api key", 42 | 43 | // Anthropic patterns 44 | "sk-ant-api", 45 | "ANTHROPIC_API_KEY", 46 | "anthropic_api_key", 47 | "claude api key", 48 | 49 | // Google AI patterns 50 | "AIzaSy", 51 | "GOOGLE_API_KEY", 52 | "gemini_api_key", 53 | 54 | // Other AI providers (patterns only, validation limited to lite providers) 55 | "r8_", // Replicate 56 | "fw_", // Fireworks 57 | "hf_", // HuggingFace 58 | "AI_API_KEY" // Generic 59 | }; 60 | 61 | foreach (var query in defaultQueries) 62 | { 63 | dbContext.SearchQueries.Add(new SearchQuery 64 | { 65 | Query = query, 66 | IsEnabled = true, 67 | LastSearchUTC = DateTime.UtcNow.AddDays(-1) 68 | }); 69 | } 70 | 71 | await dbContext.SaveChangesAsync(); 72 | AnsiConsole.MarkupLine($"[dim]Added {defaultQueries.Length} default search queries.[/]"); 73 | } 74 | } 75 | 76 | public async Task GetStatisticsAsync(DBContext dbContext) 77 | { 78 | var stats = new Statistics 79 | { 80 | TotalKeys = await dbContext.APIKeys.CountAsync(), 81 | ValidKeys = await dbContext.APIKeys.CountAsync(k => k.Status == ApiStatusEnum.Valid), 82 | InvalidKeys = await dbContext.APIKeys.CountAsync(k => k.Status == ApiStatusEnum.Invalid), 83 | UnverifiedKeys = await dbContext.APIKeys.CountAsync(k => k.Status == ApiStatusEnum.Unverified), 84 | ValidNoCreditsKeys = await dbContext.APIKeys.CountAsync(k => k.Status == ApiStatusEnum.ValidNoCredits), 85 | OpenAIKeys = await dbContext.APIKeys.CountAsync(k => k.ApiType == ApiTypeEnum.OpenAI), 86 | AnthropicKeys = await dbContext.APIKeys.CountAsync(k => k.ApiType == ApiTypeEnum.AnthropicClaude), 87 | GoogleKeys = await dbContext.APIKeys.CountAsync(k => k.ApiType == ApiTypeEnum.GoogleAI), 88 | HasGitHubToken = await dbContext.SearchProviderTokens 89 | .AnyAsync(t => t.IsEnabled && t.SearchProvider == SearchProviderEnum.GitHub) 90 | }; 91 | 92 | return stats; 93 | } 94 | 95 | public async Task SaveGitHubTokenAsync(DBContext dbContext, string token) 96 | { 97 | var existing = await dbContext.SearchProviderTokens 98 | .FirstOrDefaultAsync(t => t.SearchProvider == SearchProviderEnum.GitHub); 99 | 100 | if (existing != null) 101 | { 102 | existing.Token = token; 103 | existing.IsEnabled = true; 104 | } 105 | else 106 | { 107 | dbContext.SearchProviderTokens.Add(new SearchProviderToken 108 | { 109 | Token = token, 110 | SearchProvider = SearchProviderEnum.GitHub, 111 | IsEnabled = true 112 | }); 113 | } 114 | 115 | await dbContext.SaveChangesAsync(); 116 | } 117 | 118 | public async Task ResetDatabaseAsync() 119 | { 120 | if (File.Exists(dbPath)) 121 | { 122 | File.Delete(dbPath); 123 | } 124 | 125 | // Reinitialize 126 | await InitializeDatabaseAsync(); 127 | } 128 | 129 | public async Task ExportKeysAsync(DBContext dbContext, string filePath, bool validOnly, string format) 130 | { 131 | var query = dbContext.APIKeys.AsQueryable(); 132 | 133 | if (validOnly) 134 | { 135 | query = query.Where(k => k.Status == ApiStatusEnum.Valid || k.Status == ApiStatusEnum.ValidNoCredits); 136 | } 137 | 138 | var keys = await query 139 | .Include(k => k.References) 140 | .ToListAsync(); 141 | 142 | if (format.ToLower() == "json") 143 | { 144 | await ExportAsJsonAsync(keys, filePath); 145 | } 146 | else 147 | { 148 | await ExportAsCsvAsync(keys, filePath); 149 | } 150 | } 151 | 152 | private async Task ExportAsJsonAsync(List keys, string filePath) 153 | { 154 | var exportData = keys.Select(k => new 155 | { 156 | k.Id, 157 | k.ApiKey, 158 | Type = k.ApiType.ToString(), 159 | Status = k.Status.ToString(), 160 | k.FirstFoundUTC, 161 | k.LastCheckedUTC, 162 | Sources = k.References.Select(r => new 163 | { 164 | r.RepoURL, 165 | r.RepoOwner, 166 | r.RepoName, 167 | r.FilePath, 168 | r.FoundUTC 169 | }) 170 | }); 171 | 172 | var json = System.Text.Json.JsonSerializer.Serialize(exportData, new System.Text.Json.JsonSerializerOptions 173 | { 174 | WriteIndented = true 175 | }); 176 | 177 | await File.WriteAllTextAsync(filePath, json); 178 | } 179 | 180 | private async Task ExportAsCsvAsync(List keys, string filePath) 181 | { 182 | var lines = new List 183 | { 184 | "Id,ApiKey,Type,Status,FirstFoundUTC,LastCheckedUTC,RepoURL" 185 | }; 186 | 187 | foreach (var key in keys) 188 | { 189 | var repoUrl = key.References.FirstOrDefault()?.RepoURL ?? ""; 190 | lines.Add($"{key.Id},\"{key.ApiKey}\",{key.ApiType},{key.Status},{key.FirstFoundUTC:O},{key.LastCheckedUTC:O},\"{repoUrl}\""); 191 | } 192 | 193 | await File.WriteAllLinesAsync(filePath, lines); 194 | } 195 | } 196 | 197 | public class Statistics 198 | { 199 | public int TotalKeys { get; set; } 200 | public int ValidKeys { get; set; } 201 | public int InvalidKeys { get; set; } 202 | public int UnverifiedKeys { get; set; } 203 | public int ValidNoCreditsKeys { get; set; } 204 | public int OpenAIKeys { get; set; } 205 | public int AnthropicKeys { get; set; } 206 | public int GoogleKeys { get; set; } 207 | public bool HasGitHubToken { get; set; } 208 | } 209 | -------------------------------------------------------------------------------- /UnsecuredAPIKeys.Providers/AI Providers/OpenAIProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http.Headers; 3 | using System.Text.Json; 4 | using Microsoft.Extensions.Logging; 5 | using UnsecuredAPIKeys.Data.Common; 6 | using UnsecuredAPIKeys.Providers._Base; 7 | using UnsecuredAPIKeys.Providers.Common; 8 | 9 | namespace UnsecuredAPIKeys.Providers.AI_Providers 10 | { 11 | /// 12 | /// Provider implementation for handling OpenAI API keys. 13 | /// 14 | [ApiProvider] 15 | public class OpenAIProvider : BaseApiKeyProvider 16 | { 17 | public override string ProviderName => "OpenAI"; 18 | public override ApiTypeEnum ApiType => ApiTypeEnum.OpenAI; 19 | 20 | // Enhanced regex patterns for OpenAI keys 21 | public override IEnumerable RegexPatterns => 22 | [ 23 | @"sk-[A-Za-z0-9\-]{20,}", 24 | @"sk-proj-[A-Za-z0-9\-]{20,}", 25 | @"sk-svcacct-[A-Za-z0-9\-]{20,}", 26 | @"sk-[A-Za-z0-9]{48}", // Standard format 27 | @"Bearer sk-[A-Za-z0-9\-]{20,}" // Keys in auth headers 28 | ]; 29 | 30 | public OpenAIProvider() : base() 31 | { 32 | } 33 | 34 | public OpenAIProvider(ILogger? logger) : base(logger) 35 | { 36 | } 37 | 38 | protected override async Task ValidateKeyWithHttpClientAsync(string apiKey, HttpClient httpClient) 39 | { 40 | // First, try a lightweight model listing endpoint 41 | using var modelRequest = new HttpRequestMessage(HttpMethod.Get, "https://api.openai.com/v1/models"); 42 | modelRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); 43 | 44 | var modelResponse = await httpClient.SendAsync(modelRequest); 45 | string responseBody = await modelResponse.Content.ReadAsStringAsync(); 46 | 47 | _logger?.LogDebug("OpenAI models API response: Status={StatusCode}, Body={Body}", 48 | modelResponse.StatusCode, TruncateResponse(responseBody)); 49 | 50 | if (IsSuccessStatusCode(modelResponse.StatusCode)) 51 | { 52 | // Parse the models from the response 53 | var models = ParseOpenAIModels(responseBody); 54 | return ValidationResult.Success(modelResponse.StatusCode, models); 55 | } 56 | else if (modelResponse.StatusCode == HttpStatusCode.Unauthorized) 57 | { 58 | return ValidationResult.IsUnauthorized(modelResponse.StatusCode); 59 | } 60 | else if ((int)modelResponse.StatusCode == 429) 61 | { 62 | // Rate limited means the key is valid 63 | return ValidationResult.Success(modelResponse.StatusCode); 64 | } 65 | else if (modelResponse.StatusCode == HttpStatusCode.PaymentRequired) 66 | { 67 | // Payment required means valid key but no credits 68 | return ValidationResult.Success(modelResponse.StatusCode); 69 | } 70 | else 71 | { 72 | // Check response body for quota/billing issues 73 | if (ContainsAny(responseBody, QuotaIndicators)) 74 | { 75 | return ValidationResult.Success(modelResponse.StatusCode); 76 | } 77 | 78 | return ValidationResult.HasHttpError(modelResponse.StatusCode, 79 | $"API request failed with status {modelResponse.StatusCode}. Response: {TruncateResponse(responseBody)}"); 80 | } 81 | } 82 | 83 | protected override bool IsValidKeyFormat(string apiKey) 84 | { 85 | return !string.IsNullOrWhiteSpace(apiKey) && 86 | apiKey.StartsWith("sk-") && 87 | apiKey.Length >= 23; // sk- + at least 20 chars 88 | } 89 | 90 | private List? ParseOpenAIModels(string jsonResponse) 91 | { 92 | try 93 | { 94 | using var doc = JsonDocument.Parse(jsonResponse); 95 | if (!doc.RootElement.TryGetProperty("data", out var dataArray)) 96 | { 97 | return null; 98 | } 99 | 100 | var models = new List(); 101 | foreach (var modelElement in dataArray.EnumerateArray()) 102 | { 103 | var model = new ModelInfo 104 | { 105 | ModelId = modelElement.GetProperty("id").GetString() ?? "", 106 | DisplayName = modelElement.GetProperty("id").GetString() ?? "", // OpenAI uses id as display name 107 | Description = modelElement.TryGetProperty("description", out var desc) ? desc.GetString() : null 108 | }; 109 | 110 | // Extract model group from the ID 111 | if (!string.IsNullOrEmpty(model.ModelId)) 112 | { 113 | // Group models by family (e.g., "gpt-4", "gpt-3.5", "text-embedding") 114 | if (model.ModelId.StartsWith("gpt-4")) 115 | { 116 | model.ModelGroup = "GPT-4"; 117 | 118 | // Check for specific capabilities 119 | if (model.ModelId.Contains("turbo")) 120 | { 121 | model.Description = "GPT-4 Turbo model with enhanced capabilities"; 122 | } 123 | else if (model.ModelId.Contains("vision")) 124 | { 125 | model.Description = "GPT-4 model with vision capabilities"; 126 | } 127 | } 128 | else if (model.ModelId.StartsWith("gpt-3.5")) 129 | { 130 | model.ModelGroup = "GPT-3.5"; 131 | } 132 | else if (model.ModelId.StartsWith("o1")) 133 | { 134 | model.ModelGroup = "O1"; 135 | model.Description = "OpenAI's reasoning model"; 136 | } 137 | else if (model.ModelId.StartsWith("text-embedding")) 138 | { 139 | model.ModelGroup = "Embeddings"; 140 | model.Description = "Text embedding model"; 141 | } 142 | else if (model.ModelId.StartsWith("dall-e")) 143 | { 144 | model.ModelGroup = "DALL-E"; 145 | model.Description = "Image generation model"; 146 | } 147 | else if (model.ModelId.StartsWith("whisper")) 148 | { 149 | model.ModelGroup = "Whisper"; 150 | model.Description = "Speech recognition model"; 151 | } 152 | else if (model.ModelId.StartsWith("tts")) 153 | { 154 | model.ModelGroup = "TTS"; 155 | model.Description = "Text-to-speech model"; 156 | } 157 | else 158 | { 159 | model.ModelGroup = "Other"; 160 | } 161 | } 162 | 163 | models.Add(model); 164 | } 165 | 166 | return models; 167 | } 168 | catch (Exception ex) 169 | { 170 | _logger?.LogError(ex, "Error parsing OpenAI models response"); 171 | return null; 172 | } 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /UnsecuredAPIKeys.Providers/AI Providers/GoogleProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http.Headers; 3 | using System.Text.Json; 4 | using Microsoft.Extensions.Logging; 5 | using UnsecuredAPIKeys.Data.Common; 6 | using UnsecuredAPIKeys.Providers._Base; 7 | using UnsecuredAPIKeys.Providers.Common; 8 | 9 | namespace UnsecuredAPIKeys.Providers.AI_Providers 10 | { 11 | /// 12 | /// Provider implementation for handling Google AI API keys. 13 | /// 14 | [ApiProvider] 15 | public class GoogleProvider : BaseApiKeyProvider 16 | { 17 | public override string ProviderName => "Google"; 18 | public override ApiTypeEnum ApiType => ApiTypeEnum.GoogleAI; 19 | 20 | // Regex patterns specific to Google AI keys (from Scraper_Program.cs) 21 | public override IEnumerable RegexPatterns => 22 | [ 23 | @"AIza[0-9A-Za-z\-_]{35}", // Standard length is exactly 39 characters total 24 | @"AIza[0-9A-Za-z\-_]{35,40}" // Allow for some variation in newer keys 25 | ]; 26 | 27 | public GoogleProvider() : base() 28 | { 29 | } 30 | 31 | public GoogleProvider(ILogger? logger) : base(logger) 32 | { 33 | } 34 | 35 | protected override async Task ValidateKeyWithHttpClientAsync(string apiKey, HttpClient httpClient) 36 | { 37 | // Use Google's models endpoint for lightweight validation 38 | using var modelRequest = new HttpRequestMessage(HttpMethod.Get, "https://generativelanguage.googleapis.com/v1beta/models"); 39 | // Google uses x-goog-api-key header 40 | modelRequest.Headers.Add("x-goog-api-key", apiKey); 41 | 42 | var modelResponse = await httpClient.SendAsync(modelRequest); 43 | string responseBody = await modelResponse.Content.ReadAsStringAsync(); 44 | 45 | _logger?.LogDebug("Google AI models API response: Status={StatusCode}, Body={Body}", 46 | modelResponse.StatusCode, TruncateResponse(responseBody)); 47 | 48 | if (IsSuccessStatusCode(modelResponse.StatusCode)) 49 | { 50 | // Parse the models from the response 51 | var models = ParseGoogleModels(responseBody); 52 | return ValidationResult.Success(modelResponse.StatusCode, models); 53 | } 54 | else if (modelResponse.StatusCode == HttpStatusCode.Unauthorized || 55 | modelResponse.StatusCode == HttpStatusCode.Forbidden) 56 | { 57 | // Google often uses 403 for invalid keys 58 | if (ContainsAny(responseBody, UnauthorizedIndicators)) 59 | { 60 | return ValidationResult.IsUnauthorized(modelResponse.StatusCode); 61 | } 62 | return ValidationResult.IsUnauthorized(modelResponse.StatusCode); 63 | } 64 | else if (modelResponse.StatusCode == HttpStatusCode.BadRequest) 65 | { 66 | // Check specific error types 67 | if (ContainsAny(responseBody, UnauthorizedIndicators)) 68 | { 69 | return ValidationResult.IsUnauthorized(modelResponse.StatusCode); 70 | } 71 | return ValidationResult.HasHttpError(modelResponse.StatusCode, $"Bad request. Response: {TruncateResponse(responseBody)}"); 72 | } 73 | else if ((int)modelResponse.StatusCode == 429) 74 | { 75 | // Rate limited means the key is valid 76 | return ValidationResult.Success(modelResponse.StatusCode); 77 | } 78 | else 79 | { 80 | // Check for quota/billing issues 81 | if (ContainsAny(responseBody, QuotaIndicators)) 82 | { 83 | return ValidationResult.Success(modelResponse.StatusCode); 84 | } 85 | 86 | return ValidationResult.HasHttpError(modelResponse.StatusCode, 87 | $"API request failed with status {modelResponse.StatusCode}. Response: {TruncateResponse(responseBody)}"); 88 | } 89 | } 90 | 91 | protected override bool IsValidKeyFormat(string apiKey) 92 | { 93 | return !string.IsNullOrWhiteSpace(apiKey) && 94 | apiKey.StartsWith("AIza") && 95 | apiKey.Length >= 39; // AIza + 35 chars 96 | } 97 | 98 | private List? ParseGoogleModels(string jsonResponse) 99 | { 100 | try 101 | { 102 | using var doc = JsonDocument.Parse(jsonResponse); 103 | if (!doc.RootElement.TryGetProperty("models", out var modelsArray)) 104 | { 105 | return null; 106 | } 107 | 108 | var models = new List(); 109 | foreach (var modelElement in modelsArray.EnumerateArray()) 110 | { 111 | var model = new ModelInfo 112 | { 113 | ModelId = modelElement.GetProperty("name").GetString() ?? "", 114 | DisplayName = modelElement.TryGetProperty("displayName", out var displayName) ? displayName.GetString() : null, 115 | Description = modelElement.TryGetProperty("description", out var description) ? description.GetString() : null, 116 | Version = modelElement.TryGetProperty("version", out var version) ? version.GetString() : null, 117 | InputTokenLimit = modelElement.TryGetProperty("inputTokenLimit", out var inputLimit) ? inputLimit.GetInt64() : null, 118 | OutputTokenLimit = modelElement.TryGetProperty("outputTokenLimit", out var outputLimit) ? outputLimit.GetInt64() : null, 119 | Temperature = modelElement.TryGetProperty("temperature", out var temp) ? (float?)temp.GetDouble() : null, 120 | TopP = modelElement.TryGetProperty("topP", out var topP) ? (float?)topP.GetDouble() : null, 121 | TopK = modelElement.TryGetProperty("topK", out var topK) ? topK.GetInt32() : null, 122 | MaxTemperature = modelElement.TryGetProperty("maxTemperature", out var maxTemp) ? (float?)maxTemp.GetDouble() : null 123 | }; 124 | 125 | // Parse supported methods 126 | if (modelElement.TryGetProperty("supportedGenerationMethods", out var methods)) 127 | { 128 | model.SupportedMethods = new List(); 129 | foreach (var method in methods.EnumerateArray()) 130 | { 131 | if (method.GetString() is string methodStr) 132 | { 133 | model.SupportedMethods.Add(methodStr); 134 | } 135 | } 136 | } 137 | 138 | // Extract model group from the display name 139 | if (model.DisplayName != null) 140 | { 141 | // Extract model family (e.g., "Gemini 1.5", "Gemini 2.0", etc.) 142 | if (model.DisplayName.Contains("Gemini")) 143 | { 144 | var parts = model.DisplayName.Split(' '); 145 | if (parts.Length >= 2) 146 | { 147 | model.ModelGroup = $"{parts[0]} {parts[1]}"; 148 | } 149 | } 150 | } 151 | 152 | models.Add(model); 153 | } 154 | 155 | return models; 156 | } 157 | catch (Exception ex) 158 | { 159 | _logger?.LogError(ex, "Error parsing Google models response"); 160 | return null; 161 | } 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UnsecuredAPIKeys Lite 2 | 3 | [![GitHub Stars](https://img.shields.io/github/stars/TSCarterJr/UnsecuredAPIKeys-OpenSource?style=social)](https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource) 4 | [![.NET 10](https://img.shields.io/badge/.NET-10.0-512BD4)](https://dotnet.microsoft.com/download/dotnet/10.0) 5 | [![License](https://img.shields.io/badge/License-Custom-blue)](LICENSE) 6 | ![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/TSCarterJr/UnsecuredAPIKeys-OpenSource?utm_source=oss&utm_medium=github&utm_campaign=TSCarterJr%2FUnsecuredAPIKeys-OpenSource&labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews) 7 | 8 | > **Thank you to everyone who has starred this project!** Your support helps raise awareness about API key security and encourages responsible disclosure practices. 9 | 10 | > **Full Version Available:** [www.UnsecuredAPIKeys.com](https://www.UnsecuredAPIKeys.com) 11 | > 12 | > The full version offers: Web UI, all API providers, community features, and more. 13 | 14 | A command-line tool for discovering and validating exposed API keys on GitHub. This lite version focuses on educational and security awareness purposes. 15 | 16 | ## Lite Version Limits 17 | 18 | | Feature | Lite (This Repo) | Full Version | 19 | |---------|------------------|--------------| 20 | | Search Provider | GitHub only | GitHub, GitLab, SourceGraph | 21 | | API Providers | OpenAI, Anthropic, Google | 15+ providers | 22 | | Valid Key Cap | 50 keys | Higher limits | 23 | | Interface | CLI | Web UI + API | 24 | | Database | SQLite (local) | PostgreSQL | 25 | 26 | ## ⚠️ Educational Purpose Only 27 | 28 | This tool is for **educational and security awareness purposes only**. 29 | 30 | - **Learn** how API keys get exposed in public repositories 31 | - **Understand** the importance of secret management 32 | - **Report** exposed keys responsibly to repository owners 33 | - **Never** use discovered keys for unauthorized access 34 | 35 | **Do NOT publish your database or results publicly.** This would expose working API keys to malicious actors. 36 | 37 | ## Quick Start 38 | 39 | ### 1. Download 40 | 41 | Download the latest release for your platform from [**Releases**](https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/releases): 42 | 43 | | Platform | File | 44 | |----------|------| 45 | | Windows | `unsecuredapikeys-win-x64.exe` | 46 | | Linux | `unsecuredapikeys-linux-x64` | 47 | 48 | **No .NET runtime required** - these are self-contained executables. 49 | 50 | ### 2. Run 51 | 52 | **Windows:** 53 | ```bash 54 | .\unsecuredapikeys-win-x64.exe 55 | ``` 56 | 57 | **Linux:** 58 | ```bash 59 | chmod +x unsecuredapikeys-linux-x64 60 | ./unsecuredapikeys-linux-x64 61 | ``` 62 | 63 | ### 3. Configure GitHub Token 64 | 65 | On first run, go to **Configure Settings** > **Set GitHub Token**. 66 | 67 | Create a token at: https://github.com/settings/tokens 68 | Required scope: `public_repo` 69 | 70 | ### 4. Start Searching 71 | 72 | - **Start Scraper**: Searches GitHub for exposed API keys (runs continuously) 73 | - **Start Verifier**: Maintains up to 50 valid keys (re-checks as needed) 74 | - **View Status**: Shows current statistics 75 | - **Export Keys**: Export to JSON or CSV 76 | 77 | ### Building from Source (Optional) 78 | 79 | If you prefer to build from source: 80 | 81 | ```bash 82 | git clone https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource.git 83 | cd UnsecuredAPIKeys-OpenSource 84 | dotnet build 85 | cd UnsecuredAPIKeys.CLI 86 | dotnet run 87 | ``` 88 | 89 | ## How It Works 90 | 91 | ### Scraper 92 | 1. Uses your GitHub token to search for common API key patterns 93 | 2. Extracts potential keys using regex patterns for OpenAI, Anthropic, and Google 94 | 3. Stores discovered keys in a local SQLite database 95 | 96 | ### Verifier 97 | 1. Validates discovered keys against the actual provider APIs 98 | 2. Maintains exactly 50 valid keys (lite limit) 99 | 3. Re-checks existing valid keys periodically 100 | 4. When a key becomes invalid, verifies new ones until back to 50 101 | 102 | ## Project Structure 103 | 104 | ``` 105 | UnsecuredAPIKeys-OpenSource/ 106 | ├── UnsecuredAPIKeys.CLI/ # Main CLI application 107 | ├── UnsecuredAPIKeys.Data/ # SQLite database layer 108 | ├── UnsecuredAPIKeys.Providers/ # API validation providers 109 | ├── unsecuredapikeys.db # SQLite database (auto-created) 110 | └── README.md 111 | ``` 112 | 113 | ## Prerequisites 114 | 115 | - **.NET 10 SDK** - [Download here](https://dotnet.microsoft.com/download/dotnet/10.0) 116 | - **GitHub Personal Access Token** - [Create here](https://github.com/settings/tokens) 117 | - **Platform**: Windows, macOS, or Linux 118 | 119 | ## Supported Providers (Lite) 120 | 121 | | Provider | Pattern Examples | 122 | |----------|------------------| 123 | | OpenAI | `sk-proj-*`, `sk-or-v1-*` | 124 | | Anthropic | `sk-ant-api*` | 125 | | Google AI | `AIzaSy*` | 126 | 127 | ## Configuration 128 | 129 | Copy `appsettings.example.json` to `appsettings.json` and configure: 130 | 131 | ```json 132 | { 133 | "GitHub": { 134 | "Token": "ghp_YOUR_TOKEN" 135 | }, 136 | "Database": { 137 | "Path": "unsecuredapikeys.db" 138 | } 139 | } 140 | ``` 141 | 142 | Or configure directly via the CLI menu. 143 | 144 | ## Database 145 | 146 | The SQLite database (`unsecuredapikeys.db`) is auto-created on first run in the working directory. 147 | 148 | | Action | How | 149 | |--------|-----| 150 | | **Location** | Same folder as the executable | 151 | | **Reset** | Delete `unsecuredapikeys.db` and restart | 152 | | **Backup** | Copy the `.db` file | 153 | | **View data** | Use any SQLite browser (e.g., DB Browser for SQLite) | 154 | 155 | ## Search Queries 156 | 157 | On first run, default search queries are automatically seeded: 158 | 159 | - `sk-proj-`, `sk-or-v1-`, `OPENAI_API_KEY` (OpenAI) 160 | - `sk-ant-api`, `ANTHROPIC_API_KEY` (Anthropic) 161 | - `AIzaSy`, `GOOGLE_API_KEY` (Google) 162 | 163 | The scraper rotates through these queries automatically. 164 | 165 | ## Rate Limiting 166 | 167 | Built-in delays prevent API abuse: 168 | 169 | | Operation | Delay | 170 | |-----------|-------| 171 | | Between searches | 5 seconds | 172 | | Between verifications | 1 second | 173 | | Batch size | 10 keys | 174 | 175 | GitHub's API allows ~30 searches/minute with authentication. 176 | 177 | ## Troubleshooting 178 | 179 | | Issue | Solution | 180 | |-------|----------| 181 | | "No GitHub token configured" | Go to Configure Settings > Set GitHub Token | 182 | | "Rate limit exceeded" | Wait 60 seconds, or use a different token | 183 | | Build fails | Ensure .NET 10 SDK is installed: `dotnet --version` | 184 | | No keys found | Check your token has `public_repo` scope | 185 | | Database locked | Close other apps using the .db file | 186 | 187 | ## Legal & Ethical Use 188 | 189 | - **Educational Purpose**: This tool demonstrates API security vulnerabilities 190 | - **Responsible Use**: Only use for legitimate security research 191 | - **No Abuse**: Do not use discovered keys for unauthorized access 192 | - **Compliance**: Follow all applicable laws and terms of service 193 | 194 | ## License 195 | 196 | This project uses a **custom attribution-required license** based on MIT. 197 | 198 | ### Attribution Required 199 | 200 | Any use of this code requires visible attribution: 201 | - Display: "Based on UnsecuredAPIKeys Open Source" 202 | - Link to: https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource 203 | - Must be visible in UI/documentation 204 | 205 | See [LICENSE](LICENSE) for full details. 206 | 207 | ## Legacy UI Version 208 | 209 | Looking for the original Web UI + WebAPI architecture? Check the [`legacy_ui`](https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/tree/legacy_ui) branch. 210 | 211 | > **Note**: The legacy branch is no longer actively maintained. For the full-featured web experience, use [www.UnsecuredAPIKeys.com](https://www.UnsecuredAPIKeys.com). 212 | 213 | ## Full Version 214 | 215 | For higher limits, more providers, web interface, and community features: 216 | 217 | **[www.UnsecuredAPIKeys.com](https://www.UnsecuredAPIKeys.com)** 218 | 219 | --- 220 | 221 | **Remember**: Use responsibly and in accordance with applicable laws. 222 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd 364 | 365 | # Claude Code 366 | .claude/ 367 | CLAUDE.md 368 | 369 | # Environment files 370 | **/.env 371 | **/.env.* 372 | !**/.env.example 373 | 374 | # SQLite databases 375 | *.db 376 | *.sqlite 377 | *.sqlite3 378 | 379 | # Local config (users will copy from .example) 380 | **/appsettings.json 381 | !**/appsettings.example.json 382 | **/appsettings.Development.json 383 | **/appsettings.Production.json -------------------------------------------------------------------------------- /UnsecuredAPIKeys.Providers/_Base/BaseApiKeyProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using Microsoft.Extensions.Logging; 3 | using UnsecuredAPIKeys.Data.Common; 4 | using UnsecuredAPIKeys.Providers._Interfaces; 5 | using UnsecuredAPIKeys.Providers.Common; 6 | 7 | namespace UnsecuredAPIKeys.Providers._Base 8 | { 9 | /// 10 | /// Base class for API key providers with common functionality and retry logic. 11 | /// Lite version: OpenAI, Anthropic, Google only. 12 | /// Full version with all providers: www.UnsecuredAPIKeys.com 13 | /// 14 | public abstract class BaseApiKeyProvider(ILogger? logger = null) : IApiKeyProvider 15 | { 16 | protected const int DEFAULT_MAX_RETRIES = 3; 17 | protected const int DEFAULT_TIMEOUT_SECONDS = 30; 18 | 19 | protected readonly ILogger? _logger = logger; 20 | 21 | public abstract string ProviderName { get; } 22 | public abstract ApiTypeEnum ApiType { get; } 23 | public abstract IEnumerable RegexPatterns { get; } 24 | 25 | /// 26 | /// Validates an API key with retry logic and proper resource management. 27 | /// 28 | public async Task ValidateKeyAsync(string apiKey, IHttpClientFactory httpClientFactory) 29 | { 30 | if (string.IsNullOrWhiteSpace(apiKey)) 31 | { 32 | return ValidationResult.HasProviderSpecificError("API key is null or whitespace."); 33 | } 34 | 35 | // Clean the API key 36 | apiKey = CleanApiKey(apiKey); 37 | 38 | // Validate format if implemented 39 | if (!IsValidKeyFormat(apiKey)) 40 | { 41 | return ValidationResult.HasProviderSpecificError("API key format is invalid."); 42 | } 43 | 44 | Exception? lastException = null; 45 | 46 | for (int retry = 0; retry < GetMaxRetries(); retry++) 47 | { 48 | if (retry > 0) 49 | { 50 | var delay = TimeSpan.FromSeconds(Math.Pow(2, retry - 1)); 51 | _logger?.LogDebug("Retrying {Provider} validation after {Delay}ms (attempt {Retry}/{MaxRetries})", 52 | ProviderName, delay.TotalMilliseconds, retry + 1, GetMaxRetries()); 53 | await Task.Delay(delay); 54 | } 55 | 56 | try 57 | { 58 | using var httpClient = CreateHttpClient(httpClientFactory); 59 | var result = await ValidateKeyWithHttpClientAsync(apiKey, httpClient); 60 | 61 | if (result.Status != ValidationAttemptStatus.NetworkError) 62 | { 63 | return result; 64 | } 65 | 66 | // Continue retrying on network errors 67 | lastException = new Exception(result.Detail); 68 | } 69 | catch (HttpRequestException ex) 70 | { 71 | lastException = ex; 72 | _logger?.LogWarning(ex, "HTTP request failed on attempt {Retry}/{MaxRetries} for {Provider}", 73 | retry + 1, GetMaxRetries(), ProviderName); 74 | 75 | if (retry == GetMaxRetries() - 1) 76 | { 77 | return ValidationResult.HasNetworkError($"HTTP request failed after {GetMaxRetries()} retries: {ex.Message}"); 78 | } 79 | } 80 | catch (TaskCanceledException ex) 81 | { 82 | lastException = ex; 83 | _logger?.LogWarning(ex, "Request timeout on attempt {Retry}/{MaxRetries} for {Provider}", 84 | retry + 1, GetMaxRetries(), ProviderName); 85 | 86 | if (retry == GetMaxRetries() - 1) 87 | { 88 | return ValidationResult.HasNetworkError($"Request timeout after {GetMaxRetries()} retries: {ex.Message}"); 89 | } 90 | } 91 | catch (Exception ex) 92 | { 93 | _logger?.LogError(ex, "Unexpected error during {Provider} key validation", ProviderName); 94 | return ValidationResult.HasProviderSpecificError($"Unexpected error: {ex.Message}"); 95 | } 96 | } 97 | 98 | return ValidationResult.HasNetworkError($"Failed after {GetMaxRetries()} retries. Last error: {lastException?.Message ?? "Unknown error"}"); 99 | } 100 | 101 | /// 102 | /// Abstract method for provider-specific validation logic. 103 | /// 104 | protected abstract Task ValidateKeyWithHttpClientAsync(string apiKey, HttpClient httpClient); 105 | 106 | /// 107 | /// Creates an HttpClient with proper configuration. 108 | /// 109 | protected virtual HttpClient CreateHttpClient(IHttpClientFactory httpClientFactory) 110 | { 111 | try 112 | { 113 | var client = httpClientFactory.CreateClient(ProviderName.ToLowerInvariant().Replace(" ", "")); 114 | client.Timeout = TimeSpan.FromSeconds(GetTimeoutSeconds()); 115 | return client; 116 | } 117 | catch 118 | { 119 | // Fall back to manual creation if factory fails 120 | return new HttpClient 121 | { 122 | Timeout = TimeSpan.FromSeconds(GetTimeoutSeconds()) 123 | }; 124 | } 125 | } 126 | 127 | /// 128 | /// Cleans the API key by removing common prefixes and whitespace. 129 | /// 130 | protected virtual string CleanApiKey(string apiKey) 131 | { 132 | apiKey = apiKey.Trim(); 133 | 134 | // Remove common prefixes 135 | if (apiKey.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) 136 | { 137 | apiKey = apiKey.Substring(7).Trim(); 138 | } 139 | else if (apiKey.StartsWith("x-api-key:", StringComparison.OrdinalIgnoreCase)) 140 | { 141 | apiKey = apiKey.Substring(10).Trim(); 142 | } 143 | 144 | return apiKey; 145 | } 146 | 147 | /// 148 | /// Validates the API key format. Override in derived classes for specific validation. 149 | /// 150 | protected virtual bool IsValidKeyFormat(string apiKey) 151 | { 152 | return !string.IsNullOrWhiteSpace(apiKey) && apiKey.Length >= 10; 153 | } 154 | 155 | /// 156 | /// Gets the maximum number of retries. Override in derived classes if needed. 157 | /// 158 | protected virtual int GetMaxRetries() => DEFAULT_MAX_RETRIES; 159 | 160 | /// 161 | /// Gets the timeout in seconds. Override in derived classes if needed. 162 | /// 163 | protected virtual int GetTimeoutSeconds() => DEFAULT_TIMEOUT_SECONDS; 164 | 165 | /// 166 | /// Common method to check if response body contains any of the specified indicators. 167 | /// 168 | protected static bool ContainsAny(string text, HashSet indicators) 169 | { 170 | return indicators.Any(indicator => text.Contains(indicator, StringComparison.OrdinalIgnoreCase)); 171 | } 172 | 173 | /// 174 | /// Truncates response text for logging purposes. 175 | /// 176 | protected static string TruncateResponse(string response, int maxLength = 200) 177 | { 178 | if (string.IsNullOrEmpty(response)) 179 | return string.Empty; 180 | 181 | return response.Length > maxLength 182 | ? response.Substring(0, maxLength) + "..." 183 | : response; 184 | } 185 | 186 | /// 187 | /// Checks if the status code indicates success. 188 | /// 189 | protected static bool IsSuccessStatusCode(HttpStatusCode statusCode) 190 | { 191 | return (int)statusCode >= 200 && (int)statusCode < 300; 192 | } 193 | 194 | /// 195 | /// Common quota/billing indicators across providers. 196 | /// 197 | protected static readonly HashSet QuotaIndicators = new(StringComparer.OrdinalIgnoreCase) 198 | { 199 | "credit", "quota", "billing", "insufficient_funds", "payment", "exceeded", "balance", "limit", 200 | "insufficient_quota", "exceeded_quota", "rate_limit", "rate_limit_exceeded", "RESOURCE_EXHAUSTED" 201 | }; 202 | 203 | /// 204 | /// Common unauthorized indicators across providers. 205 | /// 206 | protected static readonly HashSet UnauthorizedIndicators = new(StringComparer.OrdinalIgnoreCase) 207 | { 208 | "invalid_api_key", "authentication_error", "unauthorized", "invalid x-api-key", "API_KEY_INVALID", 209 | "API key not valid", "API key expired", "invalid token", "authentication failed" 210 | }; 211 | 212 | /// 213 | /// Common permission indicators across providers. 214 | /// 215 | protected static readonly HashSet PermissionIndicators = new(StringComparer.OrdinalIgnoreCase) 216 | { 217 | "permission", "access", "not_authorized_for_model", "forbidden", "read-only", "Pro service" 218 | }; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to UnsecuredAPIKeys Open Source. 4 | 5 | ## [1.0.0] - 2025-12-09 - Lite Version Release 6 | 7 | This release transforms the project from a full-featured web platform into a streamlined CLI tool for local use. 8 | 9 | ### Why This Change? 10 | 11 | The original open-source release included the full platform architecture (WebAPI, UI, PostgreSQL, 15+ providers). This created barriers for users who just wanted to: 12 | - Learn about API key security 13 | - Run simple searches locally 14 | - Understand how key discovery works 15 | 16 | The lite version removes these barriers while the full platform remains available at [www.UnsecuredAPIKeys.com](https://www.UnsecuredAPIKeys.com). 17 | 18 | --- 19 | 20 | ### Added 21 | 22 | #### New CLI Application (`UnsecuredAPIKeys.CLI/`) 23 | - **Menu-driven interface** using Spectre.Console for rich terminal UI 24 | - **ScraperService** - Searches GitHub for exposed API keys continuously 25 | - **VerifierService** - Maintains exactly 50 valid keys with automatic re-verification 26 | - **Fallback validation**: Tries multiple providers when assigned provider fails 27 | - **Auto-reclassification**: Updates key's ApiType if different provider validates it 28 | - **DatabaseService** - Handles SQLite initialization, statistics, and exports 29 | - **Constants.cs** - Centralized limits (`MAX_VALID_KEYS = 50`) and app info 30 | - **appsettings.example.json** - Configuration template for self-hosting 31 | 32 | #### Documentation 33 | - **CHANGELOG.md** - This file 34 | - **Badges** in README (GitHub Stars, .NET 10, License) 35 | - **Stars thank you** message for community support 36 | - **Self-hosting sections**: Database management, Search Queries, Rate Limiting, Troubleshooting 37 | - **Platform support** documented (Windows, macOS, Linux) 38 | 39 | #### Default Search Queries (Auto-seeded) 40 | - OpenAI: `sk-proj-`, `sk-or-v1-`, `OPENAI_API_KEY`, `openai.api_key` 41 | - Anthropic: `sk-ant-api`, `ANTHROPIC_API_KEY`, `anthropic_api_key` 42 | - Google: `AIzaSy`, `GOOGLE_API_KEY`, `gemini_api_key` 43 | 44 | --- 45 | 46 | ### Changed 47 | 48 | #### Database: PostgreSQL → SQLite 49 | - **Before**: Required PostgreSQL server, connection strings, migrations 50 | - **After**: Single `unsecuredapikeys.db` file, auto-created on first run 51 | - No migrations needed - uses `EnsureCreated()` for simplicity 52 | - Package change: `Npgsql.EntityFrameworkCore.PostgreSQL` → `Microsoft.EntityFrameworkCore.Sqlite` 53 | 54 | #### Providers: 15+ → 3 55 | - **Kept**: OpenAI, Anthropic, Google AI 56 | - **Removed**: Cohere, DeepSeek, ElevenLabs, Groq, HuggingFace, MistralAI, OpenRouter, PerplexityAI, Replicate, StabilityAI, TogetherAI 57 | 58 | #### Search Providers: 3 → 1 59 | - **Kept**: GitHub (via Octokit) 60 | - **Removed**: GitLab, SourceGraph 61 | 62 | #### Architecture: Web Platform → CLI Tool 63 | - **Before**: WebAPI + Next.js UI + Separate Bots + PostgreSQL 64 | - **After**: Single CLI application + SQLite 65 | 66 | #### Valid Key Limit 67 | - **Before**: Configurable/unlimited 68 | - **After**: Hard cap of 50 keys (enforced in `LiteLimits.MAX_VALID_KEYS`) 69 | 70 | --- 71 | 72 | ### Removed 73 | 74 | #### Projects Deleted 75 | | Project | Description | 76 | |---------|-------------| 77 | | `UnsecuredAPIKeys.WebAPI/` | REST API, SignalR hub, controllers | 78 | | `UnsecuredAPIKeys.UI/` | Next.js frontend, React components | 79 | | `UnsecuredAPIKeys.Bots.Scraper/` | Standalone scraper service | 80 | | `UnsecuredAPIKeys.Bots.Verifier/` | Standalone verifier service | 81 | | `UnsecuredAPIKeys.Common/` | Shared utilities (was empty) | 82 | 83 | #### AI Providers Removed (11) 84 | - CohereProvider.cs 85 | - DeepSeekProvider.cs 86 | - ElevenLabsProvider.cs 87 | - GroqProvider.cs 88 | - HuggingFaceProvider.cs 89 | - MistralAIProvider.cs 90 | - OpenRouterProvider.cs 91 | - PerplexityAIProvider.cs 92 | - ReplicateProvider.cs 93 | - StabilityAIProvider.cs 94 | - TogetherAIProvider.cs 95 | 96 | #### Search Providers Removed (2) 97 | - GitLabSearchProvider.cs 98 | - SourceGraphSearchProvider.cs 99 | 100 | #### Services Removed 101 | - GitHubIssueService.cs (auto issue creation) 102 | - SnitchLeaderboardService.cs (community leaderboard) 103 | - UserModerationService.cs (user bans) 104 | 105 | #### Database Models Removed (15) 106 | | Model | Purpose | 107 | |-------|---------| 108 | | DiscordUser | Discord OAuth integration | 109 | | UserSession | Session management | 110 | | UserBan | User moderation | 111 | | DonationTracking | PayPal donations | 112 | | DonationSupporter | Donor recognition | 113 | | IssueSubmissionTracking | GitHub issue automation | 114 | | IssueVerification | Issue verification flow | 115 | | SnitchLeaderboard | Community rankings | 116 | | VerificationBatch | Batch job tracking | 117 | | VerificationBatchResult | Batch results | 118 | | KeyInvalidation | Key lifecycle tracking | 119 | | KeyRotation | Key rotation events | 120 | | PatternEffectiveness | Search pattern analytics | 121 | | Proxy | Proxy rotation support | 122 | | RateLimitLog | API rate limit tracking | 123 | 124 | #### Database Migrations Removed (30+ files) 125 | All PostgreSQL migrations deleted - SQLite uses runtime schema creation. 126 | 127 | #### Configuration Files Removed 128 | - `.dockerignore` 129 | - `.vite/` directories 130 | - `Deploy-VerificationBot.ps1` 131 | - `package.json`, `package-lock.json` 132 | - `analyze-unmatched-keys.ps1` 133 | - `check-unmatched-keys.ps1` 134 | - All `appsettings.json` files (kept `.example` versions) 135 | 136 | #### Features Not in Lite Version 137 | - Web UI dashboard 138 | - Real-time SignalR updates 139 | - Discord OAuth login 140 | - PayPal donation integration 141 | - GitHub issue auto-creation 142 | - Snitch leaderboard 143 | - User bans/moderation 144 | - Proxy rotation 145 | - Rate limit tracking tables 146 | - Multi-search-provider support 147 | - Batch verification locking 148 | 149 | --- 150 | 151 | ### Security 152 | 153 | - **Enforced .gitignore** rules for: 154 | - `*.db`, `*.sqlite`, `*.sqlite3` (database files) 155 | - `.env`, `.env.*` (environment files) 156 | - `.claude/` (AI assistant files) 157 | - `appsettings.json` (local config with tokens) 158 | - **Warning comments** added throughout code about not publishing results publicly 159 | - **Security Warning** section prominent in README 160 | 161 | --- 162 | 163 | ### Migration Guide 164 | 165 | #### For Users of the Old Version 166 | 167 | The lite version is a **complete rewrite**. There is no migration path - it's designed for fresh local use. 168 | 169 | If you need the original Web UI + WebAPI architecture: 170 | - Check the [`legacy_ui`](https://github.com/TSCarterJr/UnsecuredAPIKeys-OpenSource/tree/legacy_ui) branch (no longer maintained) 171 | 172 | If you need the full platform features: 173 | - Visit [www.UnsecuredAPIKeys.com](https://www.UnsecuredAPIKeys.com) 174 | 175 | #### For Contributors 176 | 177 | Open PRs against the old architecture are now outdated: 178 | - PR #5 (Docker Compose + Postgres) - No longer applicable 179 | - PR #8 (Next.js dependency bump) - UI removed 180 | - PR #9 (start.ps1) - Empty PR 181 | 182 | New contributions should target the CLI architecture. 183 | 184 | --- 185 | 186 | ### Lite vs Full Comparison 187 | 188 | | Feature | Lite (This Repo) | Full Version | 189 | |---------|------------------|--------------| 190 | | Search Providers | GitHub | GitHub, GitLab, SourceGraph | 191 | | API Providers | 3 (OpenAI, Anthropic, Google) | 15+ | 192 | | Valid Key Cap | 50 | Higher limits | 193 | | Interface | CLI | Web UI + API | 194 | | Database | SQLite (local file) | PostgreSQL | 195 | | Real-time Updates | No | SignalR | 196 | | Community Features | No | Leaderboard, Discord | 197 | | Self-hosted | Yes | Yes (complex) | 198 | | Beginner Friendly | Yes | No | 199 | 200 | --- 201 | 202 | ### Technical Details 203 | 204 | #### Dependencies (CLI) 205 | ```xml 206 | 207 | 208 | 209 | ``` 210 | 211 | #### Rate Limits (Built-in) 212 | | Operation | Delay | 213 | |-----------|-------| 214 | | Between searches | 5,000ms | 215 | | Between verifications | 1,000ms | 216 | | Verification batch size | 10 keys | 217 | 218 | #### File Structure 219 | ``` 220 | UnsecuredAPIKeys-OpenSource/ 221 | ├── UnsecuredAPIKeys.CLI/ 222 | │ ├── Program.cs # Entry point, menu UI 223 | │ ├── Constants.cs # LiteLimits, AppInfo 224 | │ ├── Services/ 225 | │ │ ├── ScraperService.cs # GitHub search 226 | │ │ ├── VerifierService.cs # Key validation 227 | │ │ └── DatabaseService.cs # SQLite ops 228 | │ └── appsettings.example.json 229 | ├── UnsecuredAPIKeys.Data/ 230 | │ ├── DBContext.cs # EF Core context 231 | │ ├── Models/ # 5 essential models 232 | │ └── Common/CommonEnums.cs # Simplified enums 233 | ├── UnsecuredAPIKeys.Providers/ 234 | │ ├── AI Providers/ # 3 providers 235 | │ ├── Search Providers/ # GitHub only 236 | │ └── _Base/, _Interfaces/ # Framework 237 | ├── CHANGELOG.md 238 | ├── CLAUDE.md 239 | ├── LICENSE 240 | └── README.md 241 | ``` 242 | 243 | --- 244 | 245 | ## [1.0.0] - 2024-07-21 - Initial Open Source Release 246 | 247 | Initial release of the full UnsecuredAPIKeys platform as open source. 248 | 249 | ### Included 250 | - WebAPI with REST endpoints and SignalR 251 | - Next.js frontend with HeroUI components 252 | - PostgreSQL database with EF Core migrations 253 | - 15+ API key validation providers 254 | - 3 search providers (GitHub, GitLab, SourceGraph) 255 | - Discord OAuth integration 256 | - PayPal donation integration 257 | - Automated scraper and verifier bots 258 | - Snitch leaderboard 259 | - User moderation system 260 | 261 | --- 262 | 263 | **Full Version**: [www.UnsecuredAPIKeys.com](https://www.UnsecuredAPIKeys.com) 264 | -------------------------------------------------------------------------------- /UnsecuredAPIKeys.CLI/Services/ScraperService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.Logging; 3 | using Spectre.Console; 4 | using UnsecuredAPIKeys.Data; 5 | using UnsecuredAPIKeys.Data.Common; 6 | using UnsecuredAPIKeys.Data.Models; 7 | using UnsecuredAPIKeys.Providers; 8 | using UnsecuredAPIKeys.Providers._Interfaces; 9 | using UnsecuredAPIKeys.Providers.Search_Providers; 10 | 11 | namespace UnsecuredAPIKeys.CLI.Services; 12 | 13 | /// 14 | /// Scraper service for finding API keys on GitHub. 15 | /// Lite version: GitHub only, 3 AI providers. 16 | /// Full version: www.UnsecuredAPIKeys.com 17 | /// 18 | public class ScraperService 19 | { 20 | private readonly DBContext _dbContext; 21 | private readonly IHttpClientFactory _httpClientFactory; 22 | private readonly ILogger? _logger; 23 | private readonly IReadOnlyList _providers; 24 | private CancellationTokenSource? _cancellationTokenSource; 25 | 26 | private int _newKeysFound; 27 | private int _duplicateKeysFound; 28 | 29 | public ScraperService(DBContext dbContext, IHttpClientFactory httpClientFactory, ILogger? logger = null) 30 | { 31 | _dbContext = dbContext; 32 | _httpClientFactory = httpClientFactory; 33 | _logger = logger; 34 | _providers = ApiProviderRegistry.ScraperProviders; 35 | } 36 | 37 | public async Task RunAsync(CancellationToken cancellationToken) 38 | { 39 | _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); 40 | 41 | AnsiConsole.MarkupLine("[cyan]Starting GitHub scraper...[/]"); 42 | AnsiConsole.MarkupLine($"[dim]Loaded {_providers.Count} API key providers[/]"); 43 | 44 | foreach (var provider in _providers) 45 | { 46 | AnsiConsole.MarkupLine($" [dim]- {Markup.Escape(provider.ProviderName)}[/]"); 47 | } 48 | 49 | // Get GitHub token 50 | var token = await _dbContext.SearchProviderTokens 51 | .Where(t => t.IsEnabled && t.SearchProvider == SearchProviderEnum.GitHub) 52 | .FirstOrDefaultAsync(cancellationToken); 53 | 54 | if (token == null) 55 | { 56 | AnsiConsole.MarkupLine("[red]No GitHub token configured. Use 'Configure Settings' to add one.[/]"); 57 | return; 58 | } 59 | 60 | // Run continuously 61 | while (!_cancellationTokenSource.Token.IsCancellationRequested) 62 | { 63 | try 64 | { 65 | await RunScrapingCycleAsync(token); 66 | 67 | if (_cancellationTokenSource.Token.IsCancellationRequested) 68 | break; 69 | 70 | // Wait before next cycle 71 | AnsiConsole.MarkupLine($"[dim]Waiting {LiteLimits.SEARCH_DELAY_MS / 1000}s before next search...[/]"); 72 | await Task.Delay(LiteLimits.SEARCH_DELAY_MS, _cancellationTokenSource.Token); 73 | 74 | // Reset counters 75 | _newKeysFound = 0; 76 | _duplicateKeysFound = 0; 77 | } 78 | catch (OperationCanceledException) 79 | { 80 | break; 81 | } 82 | catch (Exception ex) 83 | { 84 | AnsiConsole.MarkupLine($"[red]Error during scraping: {Markup.Escape(ex.Message)}[/]"); 85 | _logger?.LogError(ex, "Scraping cycle error"); 86 | await Task.Delay(5000, _cancellationTokenSource.Token); 87 | } 88 | } 89 | 90 | AnsiConsole.MarkupLine("[green]Scraper stopped.[/]"); 91 | } 92 | 93 | private async Task RunScrapingCycleAsync(SearchProviderToken token) 94 | { 95 | // Get next query to process 96 | var query = await _dbContext.SearchQueries 97 | .Where(x => x.IsEnabled && x.LastSearchUTC < DateTime.UtcNow.AddHours(-1)) 98 | .OrderBy(x => x.LastSearchUTC) 99 | .FirstOrDefaultAsync(_cancellationTokenSource!.Token); 100 | 101 | if (query == null) 102 | { 103 | AnsiConsole.MarkupLine("[dim]No queries due for search. Waiting...[/]"); 104 | return; 105 | } 106 | 107 | AnsiConsole.MarkupLine($"[cyan]Searching: {Markup.Escape(query.Query)}[/]"); 108 | 109 | // Update last search time 110 | query.LastSearchUTC = DateTime.UtcNow; 111 | await _dbContext.SaveChangesAsync(_cancellationTokenSource.Token); 112 | 113 | // Search GitHub 114 | var searchProvider = new GitHubSearchProvider(_dbContext); 115 | IEnumerable? results; 116 | 117 | try 118 | { 119 | results = await searchProvider.SearchAsync(query, token); 120 | } 121 | catch (Exception ex) 122 | { 123 | AnsiConsole.MarkupLine($"[red]Search error: {Markup.Escape(ex.Message)}[/]"); 124 | return; 125 | } 126 | 127 | if (results == null) 128 | { 129 | AnsiConsole.MarkupLine("[yellow]No results from search.[/]"); 130 | return; 131 | } 132 | 133 | var resultsList = results.ToList(); 134 | AnsiConsole.MarkupLine($"[dim]Found {resultsList.Count} potential matches[/]"); 135 | 136 | // Process each result 137 | await AnsiConsole.Progress() 138 | .StartAsync(async ctx => 139 | { 140 | var task = ctx.AddTask($"[cyan]Processing results[/]", maxValue: resultsList.Count); 141 | 142 | foreach (var repoRef in resultsList) 143 | { 144 | if (_cancellationTokenSource!.Token.IsCancellationRequested) 145 | break; 146 | 147 | await ProcessResultAsync(repoRef, token, query); 148 | task.Increment(1); 149 | } 150 | }); 151 | 152 | // Summary 153 | var table = new Table() 154 | .Border(TableBorder.Rounded) 155 | .AddColumn("[bold]Metric[/]") 156 | .AddColumn("[bold]Value[/]"); 157 | 158 | table.AddRow("Query", Markup.Escape(query.Query)); 159 | table.AddRow("Results Processed", resultsList.Count.ToString()); 160 | table.AddRow("New Keys", $"[green]{_newKeysFound}[/]"); 161 | table.AddRow("Duplicates", $"[dim]{_duplicateKeysFound}[/]"); 162 | 163 | AnsiConsole.Write(table); 164 | } 165 | 166 | private async Task ProcessResultAsync(RepoReference repoRef, SearchProviderToken token, SearchQuery query) 167 | { 168 | try 169 | { 170 | // Get file content 171 | var content = await FetchFileContentAsync(repoRef, token); 172 | if (string.IsNullOrEmpty(content)) 173 | return; 174 | 175 | // Search for API keys using all provider patterns 176 | foreach (var provider in _providers) 177 | { 178 | foreach (var pattern in provider.RegexPatterns) 179 | { 180 | var regex = new System.Text.RegularExpressions.Regex(pattern); 181 | var matches = regex.Matches(content); 182 | 183 | foreach (System.Text.RegularExpressions.Match match in matches) 184 | { 185 | var apiKey = match.Value; 186 | 187 | // Check if already exists 188 | var exists = await _dbContext.APIKeys 189 | .AnyAsync(k => k.ApiKey == apiKey, _cancellationTokenSource!.Token); 190 | 191 | if (exists) 192 | { 193 | Interlocked.Increment(ref _duplicateKeysFound); 194 | continue; 195 | } 196 | 197 | // Add new key 198 | var newKey = new APIKey 199 | { 200 | ApiKey = apiKey, 201 | ApiType = provider.ApiType, 202 | Status = ApiStatusEnum.Unverified, 203 | SearchProvider = SearchProviderEnum.GitHub, 204 | FirstFoundUTC = DateTime.UtcNow, 205 | LastFoundUTC = DateTime.UtcNow 206 | }; 207 | 208 | // Add repo reference 209 | repoRef.SearchQueryId = query.Id; 210 | repoRef.FoundUTC = DateTime.UtcNow; 211 | repoRef.Provider = "GitHub"; 212 | newKey.References.Add(repoRef); 213 | 214 | _dbContext.APIKeys.Add(newKey); 215 | await _dbContext.SaveChangesAsync(_cancellationTokenSource!.Token); 216 | 217 | Interlocked.Increment(ref _newKeysFound); 218 | AnsiConsole.MarkupLine($"[green]+ New {Markup.Escape(provider.ProviderName)} key found![/]"); 219 | } 220 | } 221 | } 222 | } 223 | catch (Exception ex) 224 | { 225 | _logger?.LogWarning(ex, "Error processing result: {Url}", repoRef.FileURL); 226 | } 227 | } 228 | 229 | private async Task FetchFileContentAsync(RepoReference repoRef, SearchProviderToken token) 230 | { 231 | try 232 | { 233 | using var client = _httpClientFactory.CreateClient(); 234 | client.DefaultRequestHeaders.UserAgent.ParseAdd("UnsecuredAPIKeys-Lite/1.0"); 235 | client.DefaultRequestHeaders.Authorization = 236 | new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token.Token); 237 | 238 | // Build raw content URL from repo info 239 | // Format: https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path} 240 | string? url = null; 241 | 242 | if (!string.IsNullOrEmpty(repoRef.RepoOwner) && 243 | !string.IsNullOrEmpty(repoRef.RepoName) && 244 | !string.IsNullOrEmpty(repoRef.FilePath)) 245 | { 246 | var branch = repoRef.Branch ?? "main"; 247 | url = $"https://raw.githubusercontent.com/{repoRef.RepoOwner}/{repoRef.RepoName}/{branch}/{repoRef.FilePath}"; 248 | } 249 | 250 | if (string.IsNullOrEmpty(url)) 251 | return null; 252 | 253 | var response = await client.GetAsync(url, _cancellationTokenSource!.Token); 254 | 255 | // Try 'master' if 'main' fails 256 | if (!response.IsSuccessStatusCode && repoRef.Branch == null) 257 | { 258 | url = $"https://raw.githubusercontent.com/{repoRef.RepoOwner}/{repoRef.RepoName}/master/{repoRef.FilePath}"; 259 | response = await client.GetAsync(url, _cancellationTokenSource!.Token); 260 | } 261 | 262 | if (!response.IsSuccessStatusCode) 263 | return null; 264 | 265 | return await response.Content.ReadAsStringAsync(_cancellationTokenSource.Token); 266 | } 267 | catch 268 | { 269 | return null; 270 | } 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /UnsecuredAPIKeys.CLI/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Logging; 3 | using Spectre.Console; 4 | using UnsecuredAPIKeys.CLI; 5 | using UnsecuredAPIKeys.CLI.Services; 6 | using UnsecuredAPIKeys.Data; 7 | 8 | // Initialize services 9 | var services = new ServiceCollection(); 10 | services.AddLogging(builder => builder 11 | .SetMinimumLevel(LogLevel.Warning) 12 | .AddConsole()); 13 | services.AddHttpClient(); 14 | 15 | await using var serviceProvider = services.BuildServiceProvider(); 16 | var httpClientFactory = serviceProvider.GetRequiredService(); 17 | 18 | // Initialize database 19 | var dbService = new DatabaseService(AppInfo.DatabaseName); 20 | DBContext? dbContext = null; 21 | 22 | try 23 | { 24 | dbContext = await dbService.InitializeDatabaseAsync(); 25 | } 26 | catch (Exception ex) 27 | { 28 | AnsiConsole.MarkupLine($"[red]Failed to initialize database: {Markup.Escape(ex.Message)}[/]"); 29 | return; 30 | } 31 | 32 | // Display banner 33 | DisplayBanner(); 34 | 35 | // Main menu loop 36 | var running = true; 37 | while (running) 38 | { 39 | var choice = AnsiConsole.Prompt( 40 | new SelectionPrompt() 41 | .Title("[yellow]What would you like to do?[/]") 42 | .PageSize(10) 43 | .AddChoices(new[] 44 | { 45 | "1. Start Scraper (search GitHub for keys)", 46 | "2. Start Verifier (maintain valid keys)", 47 | "3. View Status", 48 | "4. Configure Settings", 49 | "5. Export Keys", 50 | "6. Exit" 51 | })); 52 | 53 | AnsiConsole.WriteLine(); 54 | 55 | switch (choice[0]) 56 | { 57 | case '1': 58 | await RunScraperAsync(dbContext, httpClientFactory); 59 | break; 60 | case '2': 61 | await RunVerifierAsync(dbContext, httpClientFactory); 62 | break; 63 | case '3': 64 | await ShowStatusAsync(dbContext, dbService); 65 | break; 66 | case '4': 67 | await ConfigureSettingsAsync(dbContext, dbService); 68 | break; 69 | case '5': 70 | await ExportKeysAsync(dbContext, dbService); 71 | break; 72 | case '6': 73 | running = false; 74 | break; 75 | } 76 | 77 | if (running) 78 | { 79 | AnsiConsole.WriteLine(); 80 | AnsiConsole.MarkupLine("[dim]Press any key to continue...[/]"); 81 | Console.ReadKey(true); 82 | AnsiConsole.Clear(); 83 | DisplayBanner(); 84 | } 85 | } 86 | 87 | AnsiConsole.MarkupLine("[green]Goodbye![/]"); 88 | dbContext?.Dispose(); 89 | 90 | // === Helper Methods === 91 | 92 | void DisplayBanner() 93 | { 94 | AnsiConsole.Write( 95 | new FigletText(AppInfo.Name) 96 | .LeftJustified() 97 | .Color(Color.Cyan1)); 98 | 99 | AnsiConsole.Write(new Rule("[dim]Lite Version[/]").RuleStyle("grey").LeftJustified()); 100 | AnsiConsole.MarkupLine($"[dim]Full version: [link]{Markup.Escape(AppInfo.FullVersionUrl)}[/][/]"); 101 | AnsiConsole.MarkupLine($"[dim]Valid key limit: [yellow]{LiteLimits.MAX_VALID_KEYS}[/][/]"); 102 | AnsiConsole.WriteLine(); 103 | 104 | // Educational purpose notice 105 | var warningPanel = new Panel( 106 | "[yellow]This tool is for EDUCATIONAL PURPOSES ONLY.[/]\n\n" + 107 | "If you discover exposed API keys, please help secure them:\n" + 108 | " [green]1.[/] Open an issue on the repository to notify the owner\n" + 109 | " [green]2.[/] Never use keys for unauthorized access\n" + 110 | " [green]3.[/] Do NOT publish your results publicly\n\n" + 111 | "[dim]Help make the internet more secure by reporting, not exploiting.[/]") 112 | .Header("[yellow]Educational Use Only[/]") 113 | .Border(BoxBorder.Rounded) 114 | .BorderColor(Color.Yellow); 115 | 116 | AnsiConsole.Write(warningPanel); 117 | AnsiConsole.WriteLine(); 118 | } 119 | 120 | async Task RunScraperAsync(DBContext db, IHttpClientFactory factory) 121 | { 122 | AnsiConsole.Write(new Rule("[cyan]GitHub Scraper[/]").RuleStyle("cyan")); 123 | AnsiConsole.MarkupLine("[dim]Searches GitHub for exposed API keys. Runs continuously.[/]"); 124 | AnsiConsole.MarkupLine("[dim]Press [yellow]Ctrl+C[/] to stop.[/]"); 125 | AnsiConsole.WriteLine(); 126 | 127 | using var cts = new CancellationTokenSource(); 128 | Console.CancelKeyPress += (s, e) => 129 | { 130 | e.Cancel = true; 131 | cts.Cancel(); 132 | AnsiConsole.MarkupLine("\n[yellow]Stopping scraper...[/]"); 133 | }; 134 | 135 | var scraper = new ScraperService(db, factory); 136 | await scraper.RunAsync(cts.Token); 137 | } 138 | 139 | async Task RunVerifierAsync(DBContext db, IHttpClientFactory factory) 140 | { 141 | AnsiConsole.Write(new Rule("[green]Key Verifier[/]").RuleStyle("green")); 142 | AnsiConsole.MarkupLine($"[dim]Maintains up to [yellow]{LiteLimits.MAX_VALID_KEYS}[/] valid keys.[/]"); 143 | AnsiConsole.MarkupLine("[dim]Re-checks valid keys and verifies new ones as needed.[/]"); 144 | AnsiConsole.MarkupLine("[dim]Press [yellow]Ctrl+C[/] to stop.[/]"); 145 | AnsiConsole.WriteLine(); 146 | 147 | using var cts = new CancellationTokenSource(); 148 | Console.CancelKeyPress += (s, e) => 149 | { 150 | e.Cancel = true; 151 | cts.Cancel(); 152 | AnsiConsole.MarkupLine("\n[yellow]Stopping verifier...[/]"); 153 | }; 154 | 155 | var verifier = new VerifierService(db, factory); 156 | await verifier.RunAsync(cts.Token); 157 | } 158 | 159 | async Task ShowStatusAsync(DBContext db, DatabaseService dbService) 160 | { 161 | AnsiConsole.Write(new Rule("[blue]Current Status[/]").RuleStyle("blue")); 162 | 163 | var stats = await AnsiConsole.Status() 164 | .Spinner(Spinner.Known.Dots) 165 | .SpinnerStyle(Style.Parse("blue")) 166 | .StartAsync("Loading statistics...", async ctx => 167 | { 168 | return await dbService.GetStatisticsAsync(db); 169 | }); 170 | 171 | // Create status table 172 | var table = new Table() 173 | .Border(TableBorder.Rounded) 174 | .BorderColor(Color.Grey) 175 | .AddColumn(new TableColumn("[bold]Metric[/]").LeftAligned()) 176 | .AddColumn(new TableColumn("[bold]Value[/]").RightAligned()); 177 | 178 | table.AddRow("Total Keys Found", stats.TotalKeys.ToString()); 179 | table.AddRow("Valid Keys", $"[green]{stats.ValidKeys}[/] / [yellow]{LiteLimits.MAX_VALID_KEYS}[/]"); 180 | table.AddRow("Valid (No Credits)", $"[yellow]{stats.ValidNoCreditsKeys}[/]"); 181 | table.AddRow("Invalid Keys", $"[red]{stats.InvalidKeys}[/]"); 182 | table.AddRow("Pending Verification", $"[blue]{stats.UnverifiedKeys}[/]"); 183 | table.AddRow(new Rule().RuleStyle("dim")); 184 | table.AddRow("OpenAI Keys", stats.OpenAIKeys.ToString()); 185 | table.AddRow("Anthropic Keys", stats.AnthropicKeys.ToString()); 186 | table.AddRow("Google Keys", stats.GoogleKeys.ToString()); 187 | table.AddRow(new Rule().RuleStyle("dim")); 188 | table.AddRow("Database", $"[dim]{Markup.Escape(AppInfo.DatabaseName)}[/]"); 189 | table.AddRow("GitHub Token", stats.HasGitHubToken ? "[green]Configured[/]" : "[red]Not configured[/]"); 190 | 191 | AnsiConsole.Write(table); 192 | } 193 | 194 | async Task ConfigureSettingsAsync(DBContext db, DatabaseService dbService) 195 | { 196 | AnsiConsole.Write(new Rule("[magenta]Configuration[/]").RuleStyle("magenta")); 197 | 198 | var configChoice = AnsiConsole.Prompt( 199 | new SelectionPrompt() 200 | .Title("[yellow]What would you like to configure?[/]") 201 | .AddChoices(new[] 202 | { 203 | "1. Set GitHub Token", 204 | "2. View Current Settings", 205 | "3. Reset Database", 206 | "4. Back to Main Menu" 207 | })); 208 | 209 | switch (configChoice[0]) 210 | { 211 | case '1': 212 | await SetGitHubTokenAsync(db, dbService); 213 | break; 214 | case '2': 215 | await ShowCurrentSettingsAsync(db, dbService); 216 | break; 217 | case '3': 218 | await ResetDatabaseAsync(dbService); 219 | break; 220 | } 221 | } 222 | 223 | async Task SetGitHubTokenAsync(DBContext db, DatabaseService dbService) 224 | { 225 | AnsiConsole.MarkupLine("[dim]Enter your GitHub Personal Access Token.[/]"); 226 | AnsiConsole.MarkupLine("[dim]Create one at: https:[[//]]github.com[[/]]settings[[/]]tokens[/]"); 227 | AnsiConsole.MarkupLine("[dim]Required scopes: [yellow]public_repo[/] (for searching public repos)[/]"); 228 | AnsiConsole.WriteLine(); 229 | 230 | var token = AnsiConsole.Prompt( 231 | new TextPrompt("[green]GitHub Token:[/]") 232 | .Secret()); 233 | 234 | if (string.IsNullOrWhiteSpace(token)) 235 | { 236 | AnsiConsole.MarkupLine("[red]Token cannot be empty.[/]"); 237 | return; 238 | } 239 | 240 | // Validate token format 241 | if (!token.StartsWith("ghp_") && !token.StartsWith("github_pat_")) 242 | { 243 | var proceed = AnsiConsole.Confirm( 244 | "[yellow]Token doesn't match expected GitHub token format. Save anyway?[/]", 245 | false); 246 | 247 | if (!proceed) return; 248 | } 249 | 250 | await dbService.SaveGitHubTokenAsync(db, token); 251 | AnsiConsole.MarkupLine("[green]GitHub token saved successfully![/]"); 252 | } 253 | 254 | async Task ShowCurrentSettingsAsync(DBContext db, DatabaseService dbService) 255 | { 256 | var stats = await dbService.GetStatisticsAsync(db); 257 | 258 | var table = new Table() 259 | .Border(TableBorder.Rounded) 260 | .BorderColor(Color.Grey) 261 | .AddColumn("[bold]Setting[/]") 262 | .AddColumn("[bold]Value[/]"); 263 | 264 | var dbPath = Path.Combine(Environment.CurrentDirectory, AppInfo.DatabaseName); 265 | table.AddRow("Database Path", Markup.Escape(dbPath)); 266 | table.AddRow("GitHub Token", stats.HasGitHubToken ? "[green]Configured[/]" : "[red]Not configured[/]"); 267 | table.AddRow("Max Valid Keys", LiteLimits.MAX_VALID_KEYS.ToString()); 268 | table.AddRow("Supported Providers", "OpenAI, Anthropic, Google"); 269 | 270 | AnsiConsole.Write(table); 271 | } 272 | 273 | async Task ResetDatabaseAsync(DatabaseService dbService) 274 | { 275 | var confirm = AnsiConsole.Confirm( 276 | "[red]Are you sure you want to reset the database? All data will be lost![/]", 277 | false); 278 | 279 | if (!confirm) 280 | { 281 | AnsiConsole.MarkupLine("[dim]Database reset cancelled.[/]"); 282 | return; 283 | } 284 | 285 | var doubleConfirm = AnsiConsole.Confirm( 286 | "[red]This action is irreversible. Are you absolutely sure?[/]", 287 | false); 288 | 289 | if (!doubleConfirm) 290 | { 291 | AnsiConsole.MarkupLine("[dim]Database reset cancelled.[/]"); 292 | return; 293 | } 294 | 295 | await AnsiConsole.Status() 296 | .Spinner(Spinner.Known.Dots) 297 | .SpinnerStyle(Style.Parse("red")) 298 | .StartAsync("Resetting database...", async ctx => 299 | { 300 | await dbService.ResetDatabaseAsync(); 301 | }); 302 | 303 | AnsiConsole.MarkupLine("[green]Database reset complete.[/]"); 304 | } 305 | 306 | async Task ExportKeysAsync(DBContext db, DatabaseService dbService) 307 | { 308 | AnsiConsole.Write(new Rule("[yellow]Export Keys[/]").RuleStyle("yellow")); 309 | 310 | var exportChoice = AnsiConsole.Prompt( 311 | new SelectionPrompt() 312 | .Title("[yellow]Export format:[/]") 313 | .AddChoices(new[] 314 | { 315 | "1. JSON", 316 | "2. CSV", 317 | "3. Back to Main Menu" 318 | })); 319 | 320 | if (exportChoice[0] == '3') return; 321 | 322 | var validOnly = AnsiConsole.Confirm("Export only valid keys?", true); 323 | 324 | var format = exportChoice[0] == '1' ? "json" : "csv"; 325 | var defaultFileName = exportChoice[0] == '1' ? "keys.json" : "keys.csv"; 326 | var fileName = AnsiConsole.Prompt( 327 | new TextPrompt("[green]Output file name:[/]") 328 | .DefaultValue(defaultFileName)); 329 | 330 | await AnsiConsole.Status() 331 | .Spinner(Spinner.Known.Dots) 332 | .SpinnerStyle(Style.Parse("yellow")) 333 | .StartAsync($"Exporting to {Markup.Escape(fileName)}...", async ctx => 334 | { 335 | await dbService.ExportKeysAsync(db, fileName, validOnly, format); 336 | }); 337 | 338 | AnsiConsole.MarkupLine($"[green]Exported to [bold]{Markup.Escape(fileName)}[/][/]"); 339 | } 340 | -------------------------------------------------------------------------------- /UnsecuredAPIKeys.CLI/Services/VerifierService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.Logging; 3 | using Spectre.Console; 4 | using UnsecuredAPIKeys.Data; 5 | using UnsecuredAPIKeys.Data.Common; 6 | using UnsecuredAPIKeys.Data.Models; 7 | using UnsecuredAPIKeys.Providers; 8 | using UnsecuredAPIKeys.Providers._Interfaces; 9 | 10 | namespace UnsecuredAPIKeys.CLI.Services; 11 | 12 | /// 13 | /// Verifier service that maintains up to 50 valid API keys. 14 | /// When a key becomes invalid, verifies new keys to maintain the limit. 15 | /// Lite version: 50 key cap. 16 | /// Full version: www.UnsecuredAPIKeys.com 17 | /// 18 | public class VerifierService( 19 | DBContext dbContext, 20 | IHttpClientFactory httpClientFactory, 21 | ILogger? logger = null) 22 | { 23 | private readonly IReadOnlyList _providers = ApiProviderRegistry.VerifierProviders; 24 | private CancellationTokenSource? _cancellationTokenSource; 25 | 26 | private int _validCount; 27 | private int _invalidCount; 28 | private int _verifiedCount; 29 | 30 | public async Task RunAsync(CancellationToken cancellationToken) 31 | { 32 | _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); 33 | 34 | AnsiConsole.MarkupLine("[green]Starting verifier service...[/]"); 35 | AnsiConsole.MarkupLine($"[dim]Target valid keys: [yellow]{LiteLimits.MAX_VALID_KEYS}[/][/]"); 36 | AnsiConsole.MarkupLine($"[dim]Loaded {_providers.Count} verification providers[/]"); 37 | 38 | foreach (var provider in _providers) 39 | { 40 | AnsiConsole.MarkupLine($" [dim]- {Markup.Escape(provider.ProviderName)}[/]"); 41 | } 42 | 43 | // Run continuously 44 | while (!_cancellationTokenSource.Token.IsCancellationRequested) 45 | { 46 | try 47 | { 48 | await RunVerificationCycleAsync(); 49 | 50 | if (_cancellationTokenSource.Token.IsCancellationRequested) 51 | break; 52 | 53 | // Wait before next cycle 54 | AnsiConsole.MarkupLine($"[dim]Waiting {LiteLimits.VERIFICATION_DELAY_MS / 1000}s before next verification cycle...[/]"); 55 | await Task.Delay(LiteLimits.VERIFICATION_DELAY_MS, _cancellationTokenSource.Token); 56 | 57 | // Reset counters 58 | _validCount = 0; 59 | _invalidCount = 0; 60 | _verifiedCount = 0; 61 | } 62 | catch (OperationCanceledException) 63 | { 64 | break; 65 | } 66 | catch (Exception ex) 67 | { 68 | AnsiConsole.MarkupLine($"[red]Error during verification: {Markup.Escape(ex.Message)}[/]"); 69 | logger?.LogError(ex, "Verification cycle error"); 70 | await Task.Delay(5000, _cancellationTokenSource.Token); 71 | } 72 | } 73 | 74 | AnsiConsole.MarkupLine("[green]Verifier stopped.[/]"); 75 | } 76 | 77 | private async Task RunVerificationCycleAsync() 78 | { 79 | // Count current valid keys 80 | var currentValidCount = await dbContext.APIKeys 81 | .CountAsync(k => k.Status == ApiStatusEnum.Valid, _cancellationTokenSource!.Token); 82 | 83 | AnsiConsole.MarkupLine($"[dim]Current valid keys: [yellow]{currentValidCount}[/] / [yellow]{LiteLimits.MAX_VALID_KEYS}[/][/]"); 84 | 85 | if (currentValidCount >= LiteLimits.MAX_VALID_KEYS) 86 | { 87 | // Re-verify existing valid keys to ensure they're still valid 88 | await ReVerifyExistingKeysAsync(); 89 | } 90 | else 91 | { 92 | // Verify unverified keys until we reach the limit 93 | await VerifyNewKeysAsync(LiteLimits.MAX_VALID_KEYS - currentValidCount); 94 | } 95 | 96 | // Display summary 97 | var table = new Table() 98 | .Border(TableBorder.Rounded) 99 | .AddColumn("[bold]Metric[/]") 100 | .AddColumn("[bold]Value[/]"); 101 | 102 | table.AddRow("Keys Verified", _verifiedCount.ToString()); 103 | table.AddRow("Now Valid", $"[green]{_validCount}[/]"); 104 | table.AddRow("Now Invalid", $"[red]{_invalidCount}[/]"); 105 | 106 | var newValidCount = await dbContext.APIKeys 107 | .CountAsync(k => k.Status == ApiStatusEnum.Valid, _cancellationTokenSource!.Token); 108 | table.AddRow("Total Valid", $"[yellow]{newValidCount}[/] / [yellow]{LiteLimits.MAX_VALID_KEYS}[/]"); 109 | 110 | AnsiConsole.Write(table); 111 | } 112 | 113 | private async Task ReVerifyExistingKeysAsync() 114 | { 115 | AnsiConsole.MarkupLine("[dim]Re-verifying existing valid keys...[/]"); 116 | 117 | // Get oldest verified keys first 118 | var keysToReVerify = await dbContext.APIKeys 119 | .Where(k => k.Status == ApiStatusEnum.Valid) 120 | .OrderBy(k => k.LastCheckedUTC) 121 | .Take(LiteLimits.VERIFICATION_BATCH_SIZE) 122 | .ToListAsync(_cancellationTokenSource!.Token); 123 | 124 | await AnsiConsole.Progress() 125 | .StartAsync(async ctx => 126 | { 127 | var task = ctx.AddTask("[green]Re-verifying keys[/]", maxValue: keysToReVerify.Count); 128 | 129 | foreach (var key in keysToReVerify) 130 | { 131 | if (_cancellationTokenSource.Token.IsCancellationRequested) 132 | break; 133 | 134 | await VerifyKeyAsync(key); 135 | task.Increment(1); 136 | } 137 | }); 138 | 139 | await dbContext.SaveChangesAsync(_cancellationTokenSource.Token); 140 | } 141 | 142 | private async Task VerifyNewKeysAsync(int neededCount) 143 | { 144 | AnsiConsole.MarkupLine($"[dim]Verifying unverified keys (need {neededCount} more valid)...[/]"); 145 | 146 | // Get unverified keys 147 | var keysToVerify = await dbContext.APIKeys 148 | .Where(k => k.Status == ApiStatusEnum.Unverified) 149 | .OrderBy(k => k.FirstFoundUTC) 150 | .Take(Math.Max(neededCount * 2, LiteLimits.VERIFICATION_BATCH_SIZE)) 151 | .ToListAsync(_cancellationTokenSource!.Token); 152 | 153 | if (keysToVerify.Count == 0) 154 | { 155 | AnsiConsole.MarkupLine("[yellow]No unverified keys available.[/]"); 156 | return; 157 | } 158 | 159 | await AnsiConsole.Progress() 160 | .StartAsync(async ctx => 161 | { 162 | var task = ctx.AddTask("[green]Verifying new keys[/]", maxValue: keysToVerify.Count); 163 | var validFound = 0; 164 | 165 | foreach (var key in keysToVerify) 166 | { 167 | if (_cancellationTokenSource.Token.IsCancellationRequested) 168 | break; 169 | 170 | // Stop if we've reached our target 171 | if (validFound >= neededCount) 172 | { 173 | task.Value = keysToVerify.Count; 174 | break; 175 | } 176 | 177 | var wasValid = await VerifyKeyAsync(key); 178 | if (wasValid) 179 | validFound++; 180 | 181 | task.Increment(1); 182 | } 183 | }); 184 | 185 | await dbContext.SaveChangesAsync(_cancellationTokenSource.Token); 186 | } 187 | 188 | private async Task VerifyKeyAsync(APIKey key) 189 | { 190 | Interlocked.Increment(ref _verifiedCount); 191 | 192 | // Build list of providers to try, starting with the assigned one 193 | var providersToTry = GetProvidersToTry(key); 194 | 195 | if (providersToTry.Count == 0) 196 | { 197 | key.Status = ApiStatusEnum.Error; 198 | key.LastCheckedUTC = DateTime.UtcNow; 199 | AnsiConsole.MarkupLine($"[yellow]No matching providers for key[/]"); 200 | return false; 201 | } 202 | 203 | // Try each matching provider until one succeeds 204 | foreach (var provider in providersToTry) 205 | { 206 | try 207 | { 208 | var result = await provider.ValidateKeyAsync(key.ApiKey, httpClientFactory); 209 | key.LastCheckedUTC = DateTime.UtcNow; 210 | 211 | switch (result.Status) 212 | { 213 | case Providers.Common.ValidationAttemptStatus.Valid: 214 | // Update the key's API type if a different provider validated it 215 | if (key.ApiType != provider.ApiType) 216 | { 217 | AnsiConsole.MarkupLine($"[dim]Reclassified from {key.ApiType} to {provider.ApiType}[/]"); 218 | key.ApiType = provider.ApiType; 219 | } 220 | key.Status = ApiStatusEnum.Valid; 221 | key.ErrorCount = 0; 222 | Interlocked.Increment(ref _validCount); 223 | AnsiConsole.MarkupLine($"[green]Valid: {Markup.Escape(provider.ProviderName)} key[/]"); 224 | return true; 225 | 226 | case Providers.Common.ValidationAttemptStatus.HttpError: 227 | // Check if it's a quota/credits issue based on detail 228 | if (result.Detail?.Contains("quota", StringComparison.OrdinalIgnoreCase) == true || 229 | result.Detail?.Contains("credit", StringComparison.OrdinalIgnoreCase) == true || 230 | result.Detail?.Contains("billing", StringComparison.OrdinalIgnoreCase) == true) 231 | { 232 | // Update the key's API type if a different provider validated it 233 | if (key.ApiType != provider.ApiType) 234 | { 235 | AnsiConsole.MarkupLine($"[dim]Reclassified from {key.ApiType} to {provider.ApiType}[/]"); 236 | key.ApiType = provider.ApiType; 237 | } 238 | key.Status = ApiStatusEnum.ValidNoCredits; 239 | key.ErrorCount = 0; 240 | Interlocked.Increment(ref _validCount); 241 | AnsiConsole.MarkupLine($"[yellow]Valid [[no credits]]: {Markup.Escape(provider.ProviderName)} key[/]"); 242 | return true; 243 | } 244 | // HTTP error but not quota - try next provider 245 | continue; 246 | 247 | case Providers.Common.ValidationAttemptStatus.Unauthorized: 248 | // This provider explicitly rejected it - try next provider 249 | continue; 250 | 251 | case Providers.Common.ValidationAttemptStatus.NetworkError: 252 | // Network error - don't try other providers, just increment error count 253 | key.ErrorCount++; 254 | if (key.ErrorCount >= 3) 255 | { 256 | key.Status = ApiStatusEnum.Error; 257 | } 258 | return false; 259 | 260 | default: 261 | // Provider-specific error - try next provider 262 | continue; 263 | } 264 | } 265 | catch (Exception ex) 266 | { 267 | logger?.LogWarning(ex, "Error verifying key {KeyId} with provider {Provider}", key.Id, provider.ProviderName); 268 | // Continue to next provider on exception 269 | continue; 270 | } 271 | } 272 | 273 | // All providers failed - mark as invalid 274 | key.Status = ApiStatusEnum.Invalid; 275 | Interlocked.Increment(ref _invalidCount); 276 | return false; 277 | } 278 | 279 | /// 280 | /// Gets providers to try for a key, ordered by: assigned provider first, then other matching providers. 281 | /// 282 | private List GetProvidersToTry(APIKey key) 283 | { 284 | var result = new List(); 285 | 286 | // First, add the assigned provider (if it exists) 287 | var assignedProvider = _providers.FirstOrDefault(p => p.ApiType == key.ApiType); 288 | if (assignedProvider != null) 289 | { 290 | result.Add(assignedProvider); 291 | } 292 | 293 | // Then add other providers whose patterns match this key 294 | foreach (var provider in _providers) 295 | { 296 | // Skip the already-added assigned provider 297 | if (provider.ApiType == key.ApiType) 298 | continue; 299 | 300 | // Check if any of this provider's patterns match the key 301 | foreach (var pattern in provider.RegexPatterns) 302 | { 303 | try 304 | { 305 | var regex = new System.Text.RegularExpressions.Regex(pattern); 306 | if (regex.IsMatch(key.ApiKey)) 307 | { 308 | result.Add(provider); 309 | break; // One match is enough for this provider 310 | } 311 | } 312 | catch 313 | { 314 | // Invalid regex pattern - skip it 315 | } 316 | } 317 | } 318 | 319 | return result; 320 | } 321 | } 322 | --------------------------------------------------------------------------------