{"repo": "argareksapati/bhyakugan", "files": {"internal/plugins/secrets/secrets.go": {"96650a5a8eeaff7598922d70f0e127d044a80e57": {"t": 1785746076.2420979, "n": 15694, "h": "0b0348c8aecb224b", "c": "package secrets\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/areksaxyz/bhyakugan/internal/core\"\n\t\"github.com/areksaxyz/bhyakugan/internal/utils\"\n)\n\n// Validator defines how to check if a key is live\ntype Validator struct {\n\tMethod string\n\tURL string // Use %s placeholder for the key\n\tHeaders map[string]string\n\tExpectedCode int\n\tExpectedContent string // Substring to look for in success response\n\tBasicAuthUser bool // If true, use key as username in Basic Auth\n}\n\ntype SecretPattern struct {\n\tName string\n\tPattern *regexp.Regexp\n\tSeverity string\n\tValidator *Validator\n}\n\nvar Patterns = []SecretPattern{\n\t{\n\t\tName: \"OpenAI API Key\",\n\t\tPattern: regexp.MustCompile(`sk-[a-zA-Z0-9]{48}|sk-proj-[a-zA-Z0-9-_]{48,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.openai.com/v1/models\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Grok (xAI) API Key\",\n\t\tPattern: regexp.MustCompile(`xai-[a-zA-Z0-9]{40,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.x.ai/v1/models\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"DeepSeek API Key\",\n\t\tPattern: regexp.MustCompile(`sk-[a-f0-9]{32}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.deepseek.com/user/balance\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Anthropic API Key\",\n\t\tPattern: regexp.MustCompile(`sk-ant-sid01-[a-zA-Z0-9-_]{93,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://api.anthropic.com/v1/messages\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"x-api-key\": \"%s\",\n\t\t\t\t\"anthropic-version\": \"2023-06-01\",\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t},\n\t\t\tExpectedCode: 400, // 400 = Key valid but request bad. 401 = Invalid.\n\t\t},\n\t},\n\t{\n\t\tName: \"HuggingFace Token\",\n\t\tPattern: regexp.MustCompile(`hf_[a-zA-Z0-9]{34}`),\n\t\tSeverity: \"High\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://huggingface.co/api/whoami-v2\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"AWS Access Key\",\n\t\tPattern: regexp.MustCompile(`\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b`),\n\t\tSeverity: \"Info\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"AWS Secret Access Key\",\n\t\t// Requiring a prefix or common assignment context to avoid entropy-only false positives\n\t\tPattern: regexp.MustCompile(`(?i)(?:aws_secret|aws_secret_access_key|secret_key|secret_access_key).{0,20}\\b([0-9a-zA-Z/+=]{40})\\b`),\n\t\tSeverity: \"Info\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Firebase Configuration\",\n\t\tPattern: regexp.MustCompile(`(?is)(?:firebase|config).*?apiKey\\s*[:=]\\s*[\"'](AIza[0-9A-Za-z\\-_]{35,})[\"']`),\n\t\tSeverity: \"High\",\n\t\tValidator: nil, // Note: The generic Google API Key validator in exploitation_engine.go handles Firebase installation abuse testing\n\t},\n\t{\n\t\tName: \"Google API Key\",\n\t\tPattern: regexp.MustCompile(`AIza[0-9A-Za-z\\-_]+`),\n\t\tSeverity: \"Info\",\n\t\t// Validation logic moved to exploitation_engine.go\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Stripe Live Key\",\n\t\tPattern: regexp.MustCompile(`sk_live_[0-9a-zA-Z]{24,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.stripe.com/v1/customers\",\n\t\t\tBasicAuthUser: true,\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Slack Bot Token\",\n\t\tPattern: regexp.MustCompile(`xoxb-[0-9]{11,}-[0-9]{11,}-[0-9a-zA-Z]{24}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://slack.com/api/auth.test\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\", \"Content-Type\": \"application/json\"},\n\t\t\tExpectedCode: 200,\n\t\t\tExpectedContent: `\"ok\":true`,\n\t\t},\n\t},\n\t{\n\t\tName: \"GitHub PAT\",\n\t\tPattern: regexp.MustCompile(`ghp_[0-9a-zA-Z]{36}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.github.com/user\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"token %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Heroku API Key\",\n\t\t// Stricter Regex: Must be preceded by 'heroku'\n\t\tPattern: regexp.MustCompile(`(?i)(?:heroku).{0,20}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})`),\n\t\tSeverity: \"Medium\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://api.heroku.com/apps\",\n\t\t\tHeaders: map[string]string{\"Accept\": \"application/vnd.heroku+json; version=3\", \"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Private Key\",\n\t\tPattern: regexp.MustCompile(`(?s)-----BEGIN (?:RSA |EC |PGP )?PRIVATE KEY-----\\s*[A-Za-z0-9+/=\\r\\n]{64,}\\s*-----END (?:RSA |EC |PGP )?PRIVATE KEY-----`),\n\t\tSeverity: \"High\", // High (Unvalidated Credential)\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"CodeIgniter DB Config\",\n\t\tPattern: regexp.MustCompile(`'password'\\s*=>\\s*'[^']+'`),\n\t\tSeverity: \"Medium\", // Default to Medium (Potential Config Leak)\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"SQL Dump (Plaintext Admin)\",\n\t\tPattern: regexp.MustCompile(`(?i)INSERT\\s+INTO.*(?:user|admin|account).*(?:VALUES|\\().*`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"SQL Dump (PII Data)\",\n\t\tPattern: regexp.MustCompile(`(?i)INSERT\\s+INTO.*(?:donatur|member|customer).*(?:VALUES|\\().*`),\n\t\tSeverity: \"High\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Database Backup File\",\n\t\tPattern: regexp.MustCompile(`(?i)\\b[a-z0-9._/-]{0,120}(?:backup|dump|db)[a-z0-9._/-]{0,120}\\.sql\\b`),\n\t\tSeverity: \"Info\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Generic Client ID\",\n\t\tPattern: regexp.MustCompile(`(?i)(?:client_id|clientid|app_id|appid)\\s*[:=]\\s*[\"']([a-zA-Z0-9\\-_]{16,})[\"']`),\n\t\tSeverity: \"Info\",\n\t},\n\t{\n\t\tName: \"Generic Client Secret\",\n\t\tPattern: regexp.MustCompile(`(?i)(?:client_secret|clientsecret|app_secret|appsecret)\\s*[:=]\\s*[\"']([a-zA-Z0-9\\-_]{32,})[\"']`),\n\t\tSeverity: \"High\",\n\t},\n}\n\n// Additional helper for SQL content verification\nfunc isSQLContent(content string) bool {\n\tupper := strings.ToUpper(content)\n\t// Check for SQL header signatures or common statements\n\tif strings.Contains(upper, \"CREATE TABLE\") ||\n\t\tstrings.Contains(upper, \"INSERT INTO\") ||\n\t\tstrings.Contains(upper, \"MYSQLDUMP\") ||\n\t\tstrings.Contains(upper, \"-- MYSQL DUMP\") ||\n\t\tstrings.Contains(upper, \"SQLITE FORMAT\") ||\n\t\tstrings.Contains(upper, \"DROP TABLE\") ||\n\t\tstrings.Contains(upper, \"ALTER TABLE\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Scan checks the response body for secrets\nfunc Scan(url string, client *http.Client, onFound func(core.Finding)) {\n\treq, err := http.NewRequest(\"GET\", url, nil)\n\tif err != nil {\n\t\treturn\n\t}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, _ := io.ReadAll(io.LimitReader(io.LimitReader(resp.Body, 5*1024*1024), 5*1024*1024))\n\tbodyStr := string(body)\n\n\tDetectInContent(bodyStr, url, onFound)\n}\n\nfunc DetectInContent(content, sourceURL string, onFound func(core.Finding)) {\n\ttype FoundKey struct {\n\t\tName string\n\t\tKey string\n\t\tSeverity string\n\t\tPattern SecretPattern\n\t}\n\n\tfoundKeys := []FoundKey{}\n\tawsAccessKeys := []string{}\n\tawsSecretKeys := []string{}\n\n\tfor _, p := range Patterns {\n\t\tmatches := p.Pattern.FindAllStringSubmatch(content, -1)\n\t\tseen := make(map[string]bool)\n\t\tfor _, m := range matches {\n\t\t\trawKey := m[0]\n\t\t\tif len(m) > 1 {\n\t\t\t\trawKey = m[len(m)-1]\n\t\t\t}\n\t\t\tcleanKey := strings.Trim(rawKey, ` \"'=`)\n\n\t\t\tif len(cleanKey) < 8 && p.Name != \"Database Backup File\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif p.Name == \"Google API Key\" && len(cleanKey) < 35 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif p.Name == \"Private Key\" && !isLikelyRealPrivateKeyBlock(cleanKey) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Ignore placeholders / documentation tokens\n\t\t\tupperKey := strings.ToUpper(cleanKey)\n\t\t\tif strings.Contains(upperKey, \"EXAMPLE\") ||\n\t\t\t\tstrings.Contains(upperKey, \"TEST\") ||\n\t\t\t\tstrings.Contains(upperKey, \"MOCK\") ||\n\t\t\t\tstrings.Contains(upperKey, \"YOUR_\") ||\n\t\t\t\tstrings.Contains(upperKey, \"USERNAME\") ||\n\t\t\t\tstrings.Contains(upperKey, \"PASSWORD\") ||\n\t\t\t\tstrings.Contains(upperKey, \"TOKEN\") ||\n\t\t\t\tstrings.Contains(upperKey, \"<\") ||\n\t\t\t\tstrings.Contains(upperKey, \">\") {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif seen[cleanKey] {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tseen[cleanKey] = true\n\n\t\t\t// Special handling for AWS Secret Access Key: Entropy check and Context Validation\n\t\t\tif p.Name == \"AWS Secret Access Key\" {\n\t\t\t\tentropy := utils.CalculateShannonEntropy(cleanKey)\n\t\t\t\tif entropy < 4.3 { // Stricter threshold for solo keys to avoid JS noise\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// If no pair is found later, we want to know if 'aws' or 'key' keywords are nearby\n\t\t\t\t// We'll store the match index or just check context here\n\t\t\t\tif !hasContextKeywords(content, cleanKey, []string{\"aws\", \"secret\", \"access\", \"key\", \"s3\", \"bucket\", \"config\"}) {\n\t\t\t\t\tcontinue // Skip if no context found\n\t\t\t\t}\n\n\t\t\t\tawsSecretKeys = append(awsSecretKeys, cleanKey)\n\t\t\t}\n\n\t\t\tif p.Name == \"AWS Access Key\" {\n\t\t\t\tawsAccessKeys = append(awsAccessKeys, cleanKey)\n\t\t\t}\n\n\t\t\tfoundKeys = append(foundKeys, FoundKey{\n\t\t\t\tName: p.Name,\n\t\t\t\tKey: cleanKey,\n\t\t\t\tSeverity: p.Severity,\n\t\t\t\tPattern: p,\n\t\t\t})\n\t\t}\n\t}\n\n\t// Pair Detection for AWS\n\tisAWSPairFound := len(awsAccessKeys) > 0 && len(awsSecretKeys) > 0\n\tif isAWSPairFound {\n\t\tonFound(core.Finding{\n\t\t\tType: \"Secret Leak\",\n\t\t\tTarget: sourceURL,\n\t\t\tDetail: fmt.Sprintf(\"Found AWS Pair (Access Key ID and Secret Access Key) in the same content. This is a High Signal for Valid Credentials.\\nAccess Key: %s\\nSecret Key: [REDACTED]\", awsAccessKeys[0]),\n\t\t\tSeverity: \"High\",\n\t\t})\n\t}\n\n\tfor _, fk := range foundKeys {\n\t\tp := fk.Pattern\n\t\tcleanKey := fk.Key\n\t\tseverity := fk.Severity\n\t\tdetail := fmt.Sprintf(\"Found %s pattern.\", p.Name)\n\n\t\t// Special handling for Database Backup File:\n\t\tif p.Name == \"Database Backup File\" {\n\t\t\tif strings.HasSuffix(strings.ToLower(sourceURL), \".sql\") {\n\t\t\t\tif !isSQLContent(content) {\n\t\t\t\t\tcontinue // False Positive (Soft 404 or non-SQL content)\n\t\t\t\t}\n\t\t\t\tseverity = \"Critical\" // It's a verified dump!\n\t\t\t\tdetail = \"Verified SQL Dump file (Header/Structure confirmed).\"\n\t\t\t} else {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\t// Special handling for CodeIgniter Config\n\t\tif p.Name == \"CodeIgniter DB Config\" {\n\t\t\tif strings.Contains(cleanKey, \"''\") || strings.Contains(cleanKey, \"'password'\") { // empty password\n\t\t\t\tseverity = \"Medium\"\n\t\t\t\tdetail = \"Found DB Config pattern (Empty/Placeholder password).\"\n\t\t\t} else {\n\t\t\t\tseverity = \"High\"\n\t\t\t\tdetail = \"Found DB Config with potential password assignment.\"\n\t\t\t}\n\t\t}\n\n\t\tif p.Name == \"Google API Key\" {\n\t\t\tRunExploitationEngine(p.Name, cleanKey, sourceURL, onFound)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Don't duplicate if already flagged as AWS pair\n\t\tif isAWSPairFound && (p.Name == \"AWS Access Key\" || p.Name == \"AWS Secret Access Key\") {\n\t\t\t// Still might want to report them individually if they were found separately, but in the same content we already flagged the pair.\n\t\t\t// Let's report the Secret Key as Medium if part of a pair but not individually for now to avoid noise.\n\t\t\tif p.Name == \"AWS Secret Access Key\" {\n\t\t\t\tseverity = \"Medium\"\n\t\t\t\tdetail = fmt.Sprintf(\"AWS Secret Key part of a detected pair. Entropy: %.2f\", utils.CalculateShannonEntropy(cleanKey))\n\t\t\t} else {\n\t\t\t\tcontinue // Skip individual Access Key report if pair found\n\t\t\t}\n\t\t} else if p.Name == \"AWS Secret Access Key\" {\n\t\t\t// Individual AWS Secret Key with no Access Key found in same content\n\t\t\tseverity = \"Low\"\n\t\t\tdetail = fmt.Sprintf(\"Found AWS Secret Access Key pattern (No matching Access Key found). Entropy: %.2f\", utils.CalculateShannonEntropy(cleanKey))\n\t\t}\n\n\t\tif p.Validator != nil {\n\t\t\tif strings.Contains(sourceURL, \"localhost\") || strings.Contains(sourceURL, \"127.0.0.1\") {\n\t\t\t\tdetail = fmt.Sprintf(\"Found %s pattern (Validation skipped on localhost).\", p.Name)\n\t\t\t\t// Keep severity as defined in pattern\n\t\t\t} else {\n\t\t\t\tstatus, msg := verifyKey(cleanKey, p.Validator)\n\n\t\t\t\tswitch status {\n\t\t\t\tcase \"Valid\":\n\t\t\t\t\tseverity = \"Critical\"\n\t\t\t\t\tdetail = fmt.Sprintf(\"VERIFIED %s: %s (%s)\", p.Name, cleanKey, msg)\n\t\t\t\tcase \"Restricted\":\n\t\t\t\t\tseverity = \"Info\"\n\t\t\t\t\tdetail = fmt.Sprintf(\"VERIFIED %s (Restricted): %s (%s)\", p.Name, cleanKey, msg)\n\t\t\t\tcase \"Invalid\":\n\t\t\t\t\tcontinue\n\t\t\t\tcase \"Error\":\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t} else if severity == \"High\" {\n\t\t\tdetail += \" Validity not verified. No permission testing performed.\"\n\t\t}\n\n\t\tfmt.Printf(\"[+] SECRET MATCH: %s [%s]\\n\", p.Name, severity)\n\t\tonFound(core.Finding{\n\t\t\tType: \"Secret Leak\",\n\t\t\tTarget: sourceURL,\n\t\t\tDetail: detail,\n\t\t\tSeverity: severity,\n\t\t})\n\t}\n}\n\nfunc hasContextKeywords(content, key string, keywords []string) bool {\n\tidx := strings.Index(content, key)\n\tif idx == -1 {\n\t\treturn false\n\t}\n\n\tstart := idx - 100\n\tif start < 0 {\n\t\tstart = 0\n\t}\n\tend := idx + len(key) + 100\n\tif end > len(content) {\n\t\tend = len(content)\n\t}\n\n\tcontext := strings.ToLower(content[start:end])\n\tfor _, kw := range keywords {\n\t\tif strings.Contains(context, kw) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isLikelyRealPrivateKeyBlock(block string) bool {\n\tif strings.Contains(block, `replace(\"-----BEGIN PRIVATE KEY-----\"`) ||\n\t\tstrings.Contains(block, `replace(\"-----END PRIVATE KEY-----\"`) ||\n\t\tstrings.Contains(block, `REPLACE(\"-----BEGIN PRIVATE KEY-----\"`) ||\n\t\tstrings.Contains(block, `REPLACE(\"-----END PRIVATE KEY-----\"`) {\n\t\treturn false\n\t}\n\n\tre := regexp.MustCompile(`(?s)-----BEGIN (?:RSA |EC |PGP )?PRIVATE KEY-----\\s*([A-Za-z0-9+/=\\r\\n]+)\\s*-----END (?:RSA |EC |PGP )?PRIVATE KEY-----`)\n\tm := re.FindStringSubmatch(block)\n\tif len(m) < 2 {\n\t\treturn false\n\t}\n\n\tpayload := strings.TrimSpace(m[1])\n\tif payload == \"\" {\n\t\treturn false\n\t}\n\tif strings.ContainsAny(payload, `\"'\\{\\}\\[\\],;`) {\n\t\treturn false\n\t}\n\n\tcompact := strings.ReplaceAll(strings.ReplaceAll(payload, \"\\n\", \"\"), \"\\r\", \"\")\n\tif len(compact) < 128 {\n\t\treturn false\n\t}\n\n\treturn strings.Count(payload, \"\\n\") >= 2\n}\n\n// status: Valid, Restricted, Invalid, Error\nfunc verifyKey(key string, v *Validator) (string, string) {\n\tclient := &http.Client{Timeout: 5 * time.Second}\n\n\ttargetURL := v.URL\n\tif strings.Contains(targetURL, \"%s\") {\n\t\ttargetURL = fmt.Sprintf(targetURL, key)\n\t}\n\n\treq, err := http.NewRequest(v.Method, targetURL, nil)\n\tif err != nil {\n\t\treturn \"Error\", err.Error()\n\t}\n\n\tfor k, val := range v.Headers {\n\t\tif strings.Contains(val, \"%s\") {\n\t\t\treq.Header.Set(k, fmt.Sprintf(val, key))\n\t\t} else {\n\t\t\treq.Header.Set(k, val)\n\t\t}\n\t}\n\n\tif v.BasicAuthUser {\n\t\treq.SetBasicAuth(key, \"\")\n\t}\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"Error\", err.Error()\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, _ := io.ReadAll(io.LimitReader(io.LimitReader(resp.Body, 5*1024*1024), 5*1024*1024))\n\tbodyStr := string(body)\n\n\tif resp.StatusCode == v.ExpectedCode {\n\t\tif v.ExpectedContent != \"\" {\n\t\t\tif !strings.Contains(bodyStr, v.ExpectedContent) {\n\t\t\t\treturn \"Invalid\", \"Content mismatch\"\n\t\t\t}\n\t\t}\n\n\t\tif strings.Contains(bodyStr, \"REQUEST_DENIED\") || strings.Contains(bodyStr, \"API_KEY_INVALID\") || strings.Contains(bodyStr, \"key is invalid\") {\n\t\t\treturn \"Invalid\", \"Denied by Provider\"\n\t\t}\n\n\t\treturn \"Valid\", \"Active\"\n\t}\n\n\tif resp.StatusCode == 403 || resp.StatusCode == 401 {\n\t\treturn \"Restricted\", \"Unauthorized (Invalid or Restricted)\"\n\t}\n\n\tif resp.StatusCode == 401 {\n\t\treturn \"Invalid\", \"401 Unauthorized\"\n\t}\n\n\tif resp.StatusCode >= 500 {\n\t\treturn \"Error\", fmt.Sprintf(\"Server Error %d\", resp.StatusCode)\n\t}\n\n\treturn \"Invalid\", fmt.Sprintf(\"Status %d\", resp.StatusCode)\n}\n"}, "ea1ba80d48ff6d425c88376321a2269d6e2f816c": {"t": 1785759573.2726398, "n": 15694, "h": "0b0348c8aecb224b", "c": "package secrets\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/areksaxyz/bhyakugan/internal/core\"\n\t\"github.com/areksaxyz/bhyakugan/internal/utils\"\n)\n\n// Validator defines how to check if a key is live\ntype Validator struct {\n\tMethod string\n\tURL string // Use %s placeholder for the key\n\tHeaders map[string]string\n\tExpectedCode int\n\tExpectedContent string // Substring to look for in success response\n\tBasicAuthUser bool // If true, use key as username in Basic Auth\n}\n\ntype SecretPattern struct {\n\tName string\n\tPattern *regexp.Regexp\n\tSeverity string\n\tValidator *Validator\n}\n\nvar Patterns = []SecretPattern{\n\t{\n\t\tName: \"OpenAI API Key\",\n\t\tPattern: regexp.MustCompile(`sk-[a-zA-Z0-9]{48}|sk-proj-[a-zA-Z0-9-_]{48,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.openai.com/v1/models\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Grok (xAI) API Key\",\n\t\tPattern: regexp.MustCompile(`xai-[a-zA-Z0-9]{40,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.x.ai/v1/models\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"DeepSeek API Key\",\n\t\tPattern: regexp.MustCompile(`sk-[a-f0-9]{32}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.deepseek.com/user/balance\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Anthropic API Key\",\n\t\tPattern: regexp.MustCompile(`sk-ant-sid01-[a-zA-Z0-9-_]{93,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://api.anthropic.com/v1/messages\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"x-api-key\": \"%s\",\n\t\t\t\t\"anthropic-version\": \"2023-06-01\",\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t},\n\t\t\tExpectedCode: 400, // 400 = Key valid but request bad. 401 = Invalid.\n\t\t},\n\t},\n\t{\n\t\tName: \"HuggingFace Token\",\n\t\tPattern: regexp.MustCompile(`hf_[a-zA-Z0-9]{34}`),\n\t\tSeverity: \"High\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://huggingface.co/api/whoami-v2\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"AWS Access Key\",\n\t\tPattern: regexp.MustCompile(`\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b`),\n\t\tSeverity: \"Info\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"AWS Secret Access Key\",\n\t\t// Requiring a prefix or common assignment context to avoid entropy-only false positives\n\t\tPattern: regexp.MustCompile(`(?i)(?:aws_secret|aws_secret_access_key|secret_key|secret_access_key).{0,20}\\b([0-9a-zA-Z/+=]{40})\\b`),\n\t\tSeverity: \"Info\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Firebase Configuration\",\n\t\tPattern: regexp.MustCompile(`(?is)(?:firebase|config).*?apiKey\\s*[:=]\\s*[\"'](AIza[0-9A-Za-z\\-_]{35,})[\"']`),\n\t\tSeverity: \"High\",\n\t\tValidator: nil, // Note: The generic Google API Key validator in exploitation_engine.go handles Firebase installation abuse testing\n\t},\n\t{\n\t\tName: \"Google API Key\",\n\t\tPattern: regexp.MustCompile(`AIza[0-9A-Za-z\\-_]+`),\n\t\tSeverity: \"Info\",\n\t\t// Validation logic moved to exploitation_engine.go\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Stripe Live Key\",\n\t\tPattern: regexp.MustCompile(`sk_live_[0-9a-zA-Z]{24,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.stripe.com/v1/customers\",\n\t\t\tBasicAuthUser: true,\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Slack Bot Token\",\n\t\tPattern: regexp.MustCompile(`xoxb-[0-9]{11,}-[0-9]{11,}-[0-9a-zA-Z]{24}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://slack.com/api/auth.test\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\", \"Content-Type\": \"application/json\"},\n\t\t\tExpectedCode: 200,\n\t\t\tExpectedContent: `\"ok\":true`,\n\t\t},\n\t},\n\t{\n\t\tName: \"GitHub PAT\",\n\t\tPattern: regexp.MustCompile(`ghp_[0-9a-zA-Z]{36}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.github.com/user\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"token %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Heroku API Key\",\n\t\t// Stricter Regex: Must be preceded by 'heroku'\n\t\tPattern: regexp.MustCompile(`(?i)(?:heroku).{0,20}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})`),\n\t\tSeverity: \"Medium\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://api.heroku.com/apps\",\n\t\t\tHeaders: map[string]string{\"Accept\": \"application/vnd.heroku+json; version=3\", \"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Private Key\",\n\t\tPattern: regexp.MustCompile(`(?s)-----BEGIN (?:RSA |EC |PGP )?PRIVATE KEY-----\\s*[A-Za-z0-9+/=\\r\\n]{64,}\\s*-----END (?:RSA |EC |PGP )?PRIVATE KEY-----`),\n\t\tSeverity: \"High\", // High (Unvalidated Credential)\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"CodeIgniter DB Config\",\n\t\tPattern: regexp.MustCompile(`'password'\\s*=>\\s*'[^']+'`),\n\t\tSeverity: \"Medium\", // Default to Medium (Potential Config Leak)\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"SQL Dump (Plaintext Admin)\",\n\t\tPattern: regexp.MustCompile(`(?i)INSERT\\s+INTO.*(?:user|admin|account).*(?:VALUES|\\().*`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"SQL Dump (PII Data)\",\n\t\tPattern: regexp.MustCompile(`(?i)INSERT\\s+INTO.*(?:donatur|member|customer).*(?:VALUES|\\().*`),\n\t\tSeverity: \"High\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Database Backup File\",\n\t\tPattern: regexp.MustCompile(`(?i)\\b[a-z0-9._/-]{0,120}(?:backup|dump|db)[a-z0-9._/-]{0,120}\\.sql\\b`),\n\t\tSeverity: \"Info\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Generic Client ID\",\n\t\tPattern: regexp.MustCompile(`(?i)(?:client_id|clientid|app_id|appid)\\s*[:=]\\s*[\"']([a-zA-Z0-9\\-_]{16,})[\"']`),\n\t\tSeverity: \"Info\",\n\t},\n\t{\n\t\tName: \"Generic Client Secret\",\n\t\tPattern: regexp.MustCompile(`(?i)(?:client_secret|clientsecret|app_secret|appsecret)\\s*[:=]\\s*[\"']([a-zA-Z0-9\\-_]{32,})[\"']`),\n\t\tSeverity: \"High\",\n\t},\n}\n\n// Additional helper for SQL content verification\nfunc isSQLContent(content string) bool {\n\tupper := strings.ToUpper(content)\n\t// Check for SQL header signatures or common statements\n\tif strings.Contains(upper, \"CREATE TABLE\") ||\n\t\tstrings.Contains(upper, \"INSERT INTO\") ||\n\t\tstrings.Contains(upper, \"MYSQLDUMP\") ||\n\t\tstrings.Contains(upper, \"-- MYSQL DUMP\") ||\n\t\tstrings.Contains(upper, \"SQLITE FORMAT\") ||\n\t\tstrings.Contains(upper, \"DROP TABLE\") ||\n\t\tstrings.Contains(upper, \"ALTER TABLE\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Scan checks the response body for secrets\nfunc Scan(url string, client *http.Client, onFound func(core.Finding)) {\n\treq, err := http.NewRequest(\"GET\", url, nil)\n\tif err != nil {\n\t\treturn\n\t}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, _ := io.ReadAll(io.LimitReader(io.LimitReader(resp.Body, 5*1024*1024), 5*1024*1024))\n\tbodyStr := string(body)\n\n\tDetectInContent(bodyStr, url, onFound)\n}\n\nfunc DetectInContent(content, sourceURL string, onFound func(core.Finding)) {\n\ttype FoundKey struct {\n\t\tName string\n\t\tKey string\n\t\tSeverity string\n\t\tPattern SecretPattern\n\t}\n\n\tfoundKeys := []FoundKey{}\n\tawsAccessKeys := []string{}\n\tawsSecretKeys := []string{}\n\n\tfor _, p := range Patterns {\n\t\tmatches := p.Pattern.FindAllStringSubmatch(content, -1)\n\t\tseen := make(map[string]bool)\n\t\tfor _, m := range matches {\n\t\t\trawKey := m[0]\n\t\t\tif len(m) > 1 {\n\t\t\t\trawKey = m[len(m)-1]\n\t\t\t}\n\t\t\tcleanKey := strings.Trim(rawKey, ` \"'=`)\n\n\t\t\tif len(cleanKey) < 8 && p.Name != \"Database Backup File\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif p.Name == \"Google API Key\" && len(cleanKey) < 35 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif p.Name == \"Private Key\" && !isLikelyRealPrivateKeyBlock(cleanKey) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Ignore placeholders / documentation tokens\n\t\t\tupperKey := strings.ToUpper(cleanKey)\n\t\t\tif strings.Contains(upperKey, \"EXAMPLE\") ||\n\t\t\t\tstrings.Contains(upperKey, \"TEST\") ||\n\t\t\t\tstrings.Contains(upperKey, \"MOCK\") ||\n\t\t\t\tstrings.Contains(upperKey, \"YOUR_\") ||\n\t\t\t\tstrings.Contains(upperKey, \"USERNAME\") ||\n\t\t\t\tstrings.Contains(upperKey, \"PASSWORD\") ||\n\t\t\t\tstrings.Contains(upperKey, \"TOKEN\") ||\n\t\t\t\tstrings.Contains(upperKey, \"<\") ||\n\t\t\t\tstrings.Contains(upperKey, \">\") {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif seen[cleanKey] {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tseen[cleanKey] = true\n\n\t\t\t// Special handling for AWS Secret Access Key: Entropy check and Context Validation\n\t\t\tif p.Name == \"AWS Secret Access Key\" {\n\t\t\t\tentropy := utils.CalculateShannonEntropy(cleanKey)\n\t\t\t\tif entropy < 4.3 { // Stricter threshold for solo keys to avoid JS noise\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// If no pair is found later, we want to know if 'aws' or 'key' keywords are nearby\n\t\t\t\t// We'll store the match index or just check context here\n\t\t\t\tif !hasContextKeywords(content, cleanKey, []string{\"aws\", \"secret\", \"access\", \"key\", \"s3\", \"bucket\", \"config\"}) {\n\t\t\t\t\tcontinue // Skip if no context found\n\t\t\t\t}\n\n\t\t\t\tawsSecretKeys = append(awsSecretKeys, cleanKey)\n\t\t\t}\n\n\t\t\tif p.Name == \"AWS Access Key\" {\n\t\t\t\tawsAccessKeys = append(awsAccessKeys, cleanKey)\n\t\t\t}\n\n\t\t\tfoundKeys = append(foundKeys, FoundKey{\n\t\t\t\tName: p.Name,\n\t\t\t\tKey: cleanKey,\n\t\t\t\tSeverity: p.Severity,\n\t\t\t\tPattern: p,\n\t\t\t})\n\t\t}\n\t}\n\n\t// Pair Detection for AWS\n\tisAWSPairFound := len(awsAccessKeys) > 0 && len(awsSecretKeys) > 0\n\tif isAWSPairFound {\n\t\tonFound(core.Finding{\n\t\t\tType: \"Secret Leak\",\n\t\t\tTarget: sourceURL,\n\t\t\tDetail: fmt.Sprintf(\"Found AWS Pair (Access Key ID and Secret Access Key) in the same content. This is a High Signal for Valid Credentials.\\nAccess Key: %s\\nSecret Key: [REDACTED]\", awsAccessKeys[0]),\n\t\t\tSeverity: \"High\",\n\t\t})\n\t}\n\n\tfor _, fk := range foundKeys {\n\t\tp := fk.Pattern\n\t\tcleanKey := fk.Key\n\t\tseverity := fk.Severity\n\t\tdetail := fmt.Sprintf(\"Found %s pattern.\", p.Name)\n\n\t\t// Special handling for Database Backup File:\n\t\tif p.Name == \"Database Backup File\" {\n\t\t\tif strings.HasSuffix(strings.ToLower(sourceURL), \".sql\") {\n\t\t\t\tif !isSQLContent(content) {\n\t\t\t\t\tcontinue // False Positive (Soft 404 or non-SQL content)\n\t\t\t\t}\n\t\t\t\tseverity = \"Critical\" // It's a verified dump!\n\t\t\t\tdetail = \"Verified SQL Dump file (Header/Structure confirmed).\"\n\t\t\t} else {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\t// Special handling for CodeIgniter Config\n\t\tif p.Name == \"CodeIgniter DB Config\" {\n\t\t\tif strings.Contains(cleanKey, \"''\") || strings.Contains(cleanKey, \"'password'\") { // empty password\n\t\t\t\tseverity = \"Medium\"\n\t\t\t\tdetail = \"Found DB Config pattern (Empty/Placeholder password).\"\n\t\t\t} else {\n\t\t\t\tseverity = \"High\"\n\t\t\t\tdetail = \"Found DB Config with potential password assignment.\"\n\t\t\t}\n\t\t}\n\n\t\tif p.Name == \"Google API Key\" {\n\t\t\tRunExploitationEngine(p.Name, cleanKey, sourceURL, onFound)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Don't duplicate if already flagged as AWS pair\n\t\tif isAWSPairFound && (p.Name == \"AWS Access Key\" || p.Name == \"AWS Secret Access Key\") {\n\t\t\t// Still might want to report them individually if they were found separately, but in the same content we already flagged the pair.\n\t\t\t// Let's report the Secret Key as Medium if part of a pair but not individually for now to avoid noise.\n\t\t\tif p.Name == \"AWS Secret Access Key\" {\n\t\t\t\tseverity = \"Medium\"\n\t\t\t\tdetail = fmt.Sprintf(\"AWS Secret Key part of a detected pair. Entropy: %.2f\", utils.CalculateShannonEntropy(cleanKey))\n\t\t\t} else {\n\t\t\t\tcontinue // Skip individual Access Key report if pair found\n\t\t\t}\n\t\t} else if p.Name == \"AWS Secret Access Key\" {\n\t\t\t// Individual AWS Secret Key with no Access Key found in same content\n\t\t\tseverity = \"Low\"\n\t\t\tdetail = fmt.Sprintf(\"Found AWS Secret Access Key pattern (No matching Access Key found). Entropy: %.2f\", utils.CalculateShannonEntropy(cleanKey))\n\t\t}\n\n\t\tif p.Validator != nil {\n\t\t\tif strings.Contains(sourceURL, \"localhost\") || strings.Contains(sourceURL, \"127.0.0.1\") {\n\t\t\t\tdetail = fmt.Sprintf(\"Found %s pattern (Validation skipped on localhost).\", p.Name)\n\t\t\t\t// Keep severity as defined in pattern\n\t\t\t} else {\n\t\t\t\tstatus, msg := verifyKey(cleanKey, p.Validator)\n\n\t\t\t\tswitch status {\n\t\t\t\tcase \"Valid\":\n\t\t\t\t\tseverity = \"Critical\"\n\t\t\t\t\tdetail = fmt.Sprintf(\"VERIFIED %s: %s (%s)\", p.Name, cleanKey, msg)\n\t\t\t\tcase \"Restricted\":\n\t\t\t\t\tseverity = \"Info\"\n\t\t\t\t\tdetail = fmt.Sprintf(\"VERIFIED %s (Restricted): %s (%s)\", p.Name, cleanKey, msg)\n\t\t\t\tcase \"Invalid\":\n\t\t\t\t\tcontinue\n\t\t\t\tcase \"Error\":\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t} else if severity == \"High\" {\n\t\t\tdetail += \" Validity not verified. No permission testing performed.\"\n\t\t}\n\n\t\tfmt.Printf(\"[+] SECRET MATCH: %s [%s]\\n\", p.Name, severity)\n\t\tonFound(core.Finding{\n\t\t\tType: \"Secret Leak\",\n\t\t\tTarget: sourceURL,\n\t\t\tDetail: detail,\n\t\t\tSeverity: severity,\n\t\t})\n\t}\n}\n\nfunc hasContextKeywords(content, key string, keywords []string) bool {\n\tidx := strings.Index(content, key)\n\tif idx == -1 {\n\t\treturn false\n\t}\n\n\tstart := idx - 100\n\tif start < 0 {\n\t\tstart = 0\n\t}\n\tend := idx + len(key) + 100\n\tif end > len(content) {\n\t\tend = len(content)\n\t}\n\n\tcontext := strings.ToLower(content[start:end])\n\tfor _, kw := range keywords {\n\t\tif strings.Contains(context, kw) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isLikelyRealPrivateKeyBlock(block string) bool {\n\tif strings.Contains(block, `replace(\"-----BEGIN PRIVATE KEY-----\"`) ||\n\t\tstrings.Contains(block, `replace(\"-----END PRIVATE KEY-----\"`) ||\n\t\tstrings.Contains(block, `REPLACE(\"-----BEGIN PRIVATE KEY-----\"`) ||\n\t\tstrings.Contains(block, `REPLACE(\"-----END PRIVATE KEY-----\"`) {\n\t\treturn false\n\t}\n\n\tre := regexp.MustCompile(`(?s)-----BEGIN (?:RSA |EC |PGP )?PRIVATE KEY-----\\s*([A-Za-z0-9+/=\\r\\n]+)\\s*-----END (?:RSA |EC |PGP )?PRIVATE KEY-----`)\n\tm := re.FindStringSubmatch(block)\n\tif len(m) < 2 {\n\t\treturn false\n\t}\n\n\tpayload := strings.TrimSpace(m[1])\n\tif payload == \"\" {\n\t\treturn false\n\t}\n\tif strings.ContainsAny(payload, `\"'\\{\\}\\[\\],;`) {\n\t\treturn false\n\t}\n\n\tcompact := strings.ReplaceAll(strings.ReplaceAll(payload, \"\\n\", \"\"), \"\\r\", \"\")\n\tif len(compact) < 128 {\n\t\treturn false\n\t}\n\n\treturn strings.Count(payload, \"\\n\") >= 2\n}\n\n// status: Valid, Restricted, Invalid, Error\nfunc verifyKey(key string, v *Validator) (string, string) {\n\tclient := &http.Client{Timeout: 5 * time.Second}\n\n\ttargetURL := v.URL\n\tif strings.Contains(targetURL, \"%s\") {\n\t\ttargetURL = fmt.Sprintf(targetURL, key)\n\t}\n\n\treq, err := http.NewRequest(v.Method, targetURL, nil)\n\tif err != nil {\n\t\treturn \"Error\", err.Error()\n\t}\n\n\tfor k, val := range v.Headers {\n\t\tif strings.Contains(val, \"%s\") {\n\t\t\treq.Header.Set(k, fmt.Sprintf(val, key))\n\t\t} else {\n\t\t\treq.Header.Set(k, val)\n\t\t}\n\t}\n\n\tif v.BasicAuthUser {\n\t\treq.SetBasicAuth(key, \"\")\n\t}\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"Error\", err.Error()\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, _ := io.ReadAll(io.LimitReader(io.LimitReader(resp.Body, 5*1024*1024), 5*1024*1024))\n\tbodyStr := string(body)\n\n\tif resp.StatusCode == v.ExpectedCode {\n\t\tif v.ExpectedContent != \"\" {\n\t\t\tif !strings.Contains(bodyStr, v.ExpectedContent) {\n\t\t\t\treturn \"Invalid\", \"Content mismatch\"\n\t\t\t}\n\t\t}\n\n\t\tif strings.Contains(bodyStr, \"REQUEST_DENIED\") || strings.Contains(bodyStr, \"API_KEY_INVALID\") || strings.Contains(bodyStr, \"key is invalid\") {\n\t\t\treturn \"Invalid\", \"Denied by Provider\"\n\t\t}\n\n\t\treturn \"Valid\", \"Active\"\n\t}\n\n\tif resp.StatusCode == 403 || resp.StatusCode == 401 {\n\t\treturn \"Restricted\", \"Unauthorized (Invalid or Restricted)\"\n\t}\n\n\tif resp.StatusCode == 401 {\n\t\treturn \"Invalid\", \"401 Unauthorized\"\n\t}\n\n\tif resp.StatusCode >= 500 {\n\t\treturn \"Error\", fmt.Sprintf(\"Server Error %d\", resp.StatusCode)\n\t}\n\n\treturn \"Invalid\", fmt.Sprintf(\"Status %d\", resp.StatusCode)\n}\n"}, "2a529ca2a6e3997091130b0fec651d6c77494c20": {"t": 1785759578.106899, "n": 15688, "h": "140afcb11e3e8d24", "c": "package secrets\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/yupiyy/bhyakugan/internal/core\"\n\t\"github.com/yupiyy/bhyakugan/internal/utils\"\n)\n\n// Validator defines how to check if a key is live\ntype Validator struct {\n\tMethod string\n\tURL string // Use %s placeholder for the key\n\tHeaders map[string]string\n\tExpectedCode int\n\tExpectedContent string // Substring to look for in success response\n\tBasicAuthUser bool // If true, use key as username in Basic Auth\n}\n\ntype SecretPattern struct {\n\tName string\n\tPattern *regexp.Regexp\n\tSeverity string\n\tValidator *Validator\n}\n\nvar Patterns = []SecretPattern{\n\t{\n\t\tName: \"OpenAI API Key\",\n\t\tPattern: regexp.MustCompile(`sk-[a-zA-Z0-9]{48}|sk-proj-[a-zA-Z0-9-_]{48,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.openai.com/v1/models\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Grok (xAI) API Key\",\n\t\tPattern: regexp.MustCompile(`xai-[a-zA-Z0-9]{40,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.x.ai/v1/models\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"DeepSeek API Key\",\n\t\tPattern: regexp.MustCompile(`sk-[a-f0-9]{32}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.deepseek.com/user/balance\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Anthropic API Key\",\n\t\tPattern: regexp.MustCompile(`sk-ant-sid01-[a-zA-Z0-9-_]{93,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://api.anthropic.com/v1/messages\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"x-api-key\": \"%s\",\n\t\t\t\t\"anthropic-version\": \"2023-06-01\",\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t},\n\t\t\tExpectedCode: 400, // 400 = Key valid but request bad. 401 = Invalid.\n\t\t},\n\t},\n\t{\n\t\tName: \"HuggingFace Token\",\n\t\tPattern: regexp.MustCompile(`hf_[a-zA-Z0-9]{34}`),\n\t\tSeverity: \"High\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://huggingface.co/api/whoami-v2\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"AWS Access Key\",\n\t\tPattern: regexp.MustCompile(`\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b`),\n\t\tSeverity: \"Info\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"AWS Secret Access Key\",\n\t\t// Requiring a prefix or common assignment context to avoid entropy-only false positives\n\t\tPattern: regexp.MustCompile(`(?i)(?:aws_secret|aws_secret_access_key|secret_key|secret_access_key).{0,20}\\b([0-9a-zA-Z/+=]{40})\\b`),\n\t\tSeverity: \"Info\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Firebase Configuration\",\n\t\tPattern: regexp.MustCompile(`(?is)(?:firebase|config).*?apiKey\\s*[:=]\\s*[\"'](AIza[0-9A-Za-z\\-_]{35,})[\"']`),\n\t\tSeverity: \"High\",\n\t\tValidator: nil, // Note: The generic Google API Key validator in exploitation_engine.go handles Firebase installation abuse testing\n\t},\n\t{\n\t\tName: \"Google API Key\",\n\t\tPattern: regexp.MustCompile(`AIza[0-9A-Za-z\\-_]+`),\n\t\tSeverity: \"Info\",\n\t\t// Validation logic moved to exploitation_engine.go\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Stripe Live Key\",\n\t\tPattern: regexp.MustCompile(`sk_live_[0-9a-zA-Z]{24,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.stripe.com/v1/customers\",\n\t\t\tBasicAuthUser: true,\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Slack Bot Token\",\n\t\tPattern: regexp.MustCompile(`xoxb-[0-9]{11,}-[0-9]{11,}-[0-9a-zA-Z]{24}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://slack.com/api/auth.test\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\", \"Content-Type\": \"application/json\"},\n\t\t\tExpectedCode: 200,\n\t\t\tExpectedContent: `\"ok\":true`,\n\t\t},\n\t},\n\t{\n\t\tName: \"GitHub PAT\",\n\t\tPattern: regexp.MustCompile(`ghp_[0-9a-zA-Z]{36}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.github.com/user\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"token %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Heroku API Key\",\n\t\t// Stricter Regex: Must be preceded by 'heroku'\n\t\tPattern: regexp.MustCompile(`(?i)(?:heroku).{0,20}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})`),\n\t\tSeverity: \"Medium\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://api.heroku.com/apps\",\n\t\t\tHeaders: map[string]string{\"Accept\": \"application/vnd.heroku+json; version=3\", \"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Private Key\",\n\t\tPattern: regexp.MustCompile(`(?s)-----BEGIN (?:RSA |EC |PGP )?PRIVATE KEY-----\\s*[A-Za-z0-9+/=\\r\\n]{64,}\\s*-----END (?:RSA |EC |PGP )?PRIVATE KEY-----`),\n\t\tSeverity: \"High\", // High (Unvalidated Credential)\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"CodeIgniter DB Config\",\n\t\tPattern: regexp.MustCompile(`'password'\\s*=>\\s*'[^']+'`),\n\t\tSeverity: \"Medium\", // Default to Medium (Potential Config Leak)\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"SQL Dump (Plaintext Admin)\",\n\t\tPattern: regexp.MustCompile(`(?i)INSERT\\s+INTO.*(?:user|admin|account).*(?:VALUES|\\().*`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"SQL Dump (PII Data)\",\n\t\tPattern: regexp.MustCompile(`(?i)INSERT\\s+INTO.*(?:donatur|member|customer).*(?:VALUES|\\().*`),\n\t\tSeverity: \"High\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Database Backup File\",\n\t\tPattern: regexp.MustCompile(`(?i)\\b[a-z0-9._/-]{0,120}(?:backup|dump|db)[a-z0-9._/-]{0,120}\\.sql\\b`),\n\t\tSeverity: \"Info\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Generic Client ID\",\n\t\tPattern: regexp.MustCompile(`(?i)(?:client_id|clientid|app_id|appid)\\s*[:=]\\s*[\"']([a-zA-Z0-9\\-_]{16,})[\"']`),\n\t\tSeverity: \"Info\",\n\t},\n\t{\n\t\tName: \"Generic Client Secret\",\n\t\tPattern: regexp.MustCompile(`(?i)(?:client_secret|clientsecret|app_secret|appsecret)\\s*[:=]\\s*[\"']([a-zA-Z0-9\\-_]{32,})[\"']`),\n\t\tSeverity: \"High\",\n\t},\n}\n\n// Additional helper for SQL content verification\nfunc isSQLContent(content string) bool {\n\tupper := strings.ToUpper(content)\n\t// Check for SQL header signatures or common statements\n\tif strings.Contains(upper, \"CREATE TABLE\") ||\n\t\tstrings.Contains(upper, \"INSERT INTO\") ||\n\t\tstrings.Contains(upper, \"MYSQLDUMP\") ||\n\t\tstrings.Contains(upper, \"-- MYSQL DUMP\") ||\n\t\tstrings.Contains(upper, \"SQLITE FORMAT\") ||\n\t\tstrings.Contains(upper, \"DROP TABLE\") ||\n\t\tstrings.Contains(upper, \"ALTER TABLE\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Scan checks the response body for secrets\nfunc Scan(url string, client *http.Client, onFound func(core.Finding)) {\n\treq, err := http.NewRequest(\"GET\", url, nil)\n\tif err != nil {\n\t\treturn\n\t}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, _ := io.ReadAll(io.LimitReader(io.LimitReader(resp.Body, 5*1024*1024), 5*1024*1024))\n\tbodyStr := string(body)\n\n\tDetectInContent(bodyStr, url, onFound)\n}\n\nfunc DetectInContent(content, sourceURL string, onFound func(core.Finding)) {\n\ttype FoundKey struct {\n\t\tName string\n\t\tKey string\n\t\tSeverity string\n\t\tPattern SecretPattern\n\t}\n\n\tfoundKeys := []FoundKey{}\n\tawsAccessKeys := []string{}\n\tawsSecretKeys := []string{}\n\n\tfor _, p := range Patterns {\n\t\tmatches := p.Pattern.FindAllStringSubmatch(content, -1)\n\t\tseen := make(map[string]bool)\n\t\tfor _, m := range matches {\n\t\t\trawKey := m[0]\n\t\t\tif len(m) > 1 {\n\t\t\t\trawKey = m[len(m)-1]\n\t\t\t}\n\t\t\tcleanKey := strings.Trim(rawKey, ` \"'=`)\n\n\t\t\tif len(cleanKey) < 8 && p.Name != \"Database Backup File\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif p.Name == \"Google API Key\" && len(cleanKey) < 35 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif p.Name == \"Private Key\" && !isLikelyRealPrivateKeyBlock(cleanKey) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Ignore placeholders / documentation tokens\n\t\t\tupperKey := strings.ToUpper(cleanKey)\n\t\t\tif strings.Contains(upperKey, \"EXAMPLE\") ||\n\t\t\t\tstrings.Contains(upperKey, \"TEST\") ||\n\t\t\t\tstrings.Contains(upperKey, \"MOCK\") ||\n\t\t\t\tstrings.Contains(upperKey, \"YOUR_\") ||\n\t\t\t\tstrings.Contains(upperKey, \"USERNAME\") ||\n\t\t\t\tstrings.Contains(upperKey, \"PASSWORD\") ||\n\t\t\t\tstrings.Contains(upperKey, \"TOKEN\") ||\n\t\t\t\tstrings.Contains(upperKey, \"<\") ||\n\t\t\t\tstrings.Contains(upperKey, \">\") {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif seen[cleanKey] {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tseen[cleanKey] = true\n\n\t\t\t// Special handling for AWS Secret Access Key: Entropy check and Context Validation\n\t\t\tif p.Name == \"AWS Secret Access Key\" {\n\t\t\t\tentropy := utils.CalculateShannonEntropy(cleanKey)\n\t\t\t\tif entropy < 4.3 { // Stricter threshold for solo keys to avoid JS noise\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// If no pair is found later, we want to know if 'aws' or 'key' keywords are nearby\n\t\t\t\t// We'll store the match index or just check context here\n\t\t\t\tif !hasContextKeywords(content, cleanKey, []string{\"aws\", \"secret\", \"access\", \"key\", \"s3\", \"bucket\", \"config\"}) {\n\t\t\t\t\tcontinue // Skip if no context found\n\t\t\t\t}\n\n\t\t\t\tawsSecretKeys = append(awsSecretKeys, cleanKey)\n\t\t\t}\n\n\t\t\tif p.Name == \"AWS Access Key\" {\n\t\t\t\tawsAccessKeys = append(awsAccessKeys, cleanKey)\n\t\t\t}\n\n\t\t\tfoundKeys = append(foundKeys, FoundKey{\n\t\t\t\tName: p.Name,\n\t\t\t\tKey: cleanKey,\n\t\t\t\tSeverity: p.Severity,\n\t\t\t\tPattern: p,\n\t\t\t})\n\t\t}\n\t}\n\n\t// Pair Detection for AWS\n\tisAWSPairFound := len(awsAccessKeys) > 0 && len(awsSecretKeys) > 0\n\tif isAWSPairFound {\n\t\tonFound(core.Finding{\n\t\t\tType: \"Secret Leak\",\n\t\t\tTarget: sourceURL,\n\t\t\tDetail: fmt.Sprintf(\"Found AWS Pair (Access Key ID and Secret Access Key) in the same content. This is a High Signal for Valid Credentials.\\nAccess Key: %s\\nSecret Key: [REDACTED]\", awsAccessKeys[0]),\n\t\t\tSeverity: \"High\",\n\t\t})\n\t}\n\n\tfor _, fk := range foundKeys {\n\t\tp := fk.Pattern\n\t\tcleanKey := fk.Key\n\t\tseverity := fk.Severity\n\t\tdetail := fmt.Sprintf(\"Found %s pattern.\", p.Name)\n\n\t\t// Special handling for Database Backup File:\n\t\tif p.Name == \"Database Backup File\" {\n\t\t\tif strings.HasSuffix(strings.ToLower(sourceURL), \".sql\") {\n\t\t\t\tif !isSQLContent(content) {\n\t\t\t\t\tcontinue // False Positive (Soft 404 or non-SQL content)\n\t\t\t\t}\n\t\t\t\tseverity = \"Critical\" // It's a verified dump!\n\t\t\t\tdetail = \"Verified SQL Dump file (Header/Structure confirmed).\"\n\t\t\t} else {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\t// Special handling for CodeIgniter Config\n\t\tif p.Name == \"CodeIgniter DB Config\" {\n\t\t\tif strings.Contains(cleanKey, \"''\") || strings.Contains(cleanKey, \"'password'\") { // empty password\n\t\t\t\tseverity = \"Medium\"\n\t\t\t\tdetail = \"Found DB Config pattern (Empty/Placeholder password).\"\n\t\t\t} else {\n\t\t\t\tseverity = \"High\"\n\t\t\t\tdetail = \"Found DB Config with potential password assignment.\"\n\t\t\t}\n\t\t}\n\n\t\tif p.Name == \"Google API Key\" {\n\t\t\tRunExploitationEngine(p.Name, cleanKey, sourceURL, onFound)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Don't duplicate if already flagged as AWS pair\n\t\tif isAWSPairFound && (p.Name == \"AWS Access Key\" || p.Name == \"AWS Secret Access Key\") {\n\t\t\t// Still might want to report them individually if they were found separately, but in the same content we already flagged the pair.\n\t\t\t// Let's report the Secret Key as Medium if part of a pair but not individually for now to avoid noise.\n\t\t\tif p.Name == \"AWS Secret Access Key\" {\n\t\t\t\tseverity = \"Medium\"\n\t\t\t\tdetail = fmt.Sprintf(\"AWS Secret Key part of a detected pair. Entropy: %.2f\", utils.CalculateShannonEntropy(cleanKey))\n\t\t\t} else {\n\t\t\t\tcontinue // Skip individual Access Key report if pair found\n\t\t\t}\n\t\t} else if p.Name == \"AWS Secret Access Key\" {\n\t\t\t// Individual AWS Secret Key with no Access Key found in same content\n\t\t\tseverity = \"Low\"\n\t\t\tdetail = fmt.Sprintf(\"Found AWS Secret Access Key pattern (No matching Access Key found). Entropy: %.2f\", utils.CalculateShannonEntropy(cleanKey))\n\t\t}\n\n\t\tif p.Validator != nil {\n\t\t\tif strings.Contains(sourceURL, \"localhost\") || strings.Contains(sourceURL, \"127.0.0.1\") {\n\t\t\t\tdetail = fmt.Sprintf(\"Found %s pattern (Validation skipped on localhost).\", p.Name)\n\t\t\t\t// Keep severity as defined in pattern\n\t\t\t} else {\n\t\t\t\tstatus, msg := verifyKey(cleanKey, p.Validator)\n\n\t\t\t\tswitch status {\n\t\t\t\tcase \"Valid\":\n\t\t\t\t\tseverity = \"Critical\"\n\t\t\t\t\tdetail = fmt.Sprintf(\"VERIFIED %s: %s (%s)\", p.Name, cleanKey, msg)\n\t\t\t\tcase \"Restricted\":\n\t\t\t\t\tseverity = \"Info\"\n\t\t\t\t\tdetail = fmt.Sprintf(\"VERIFIED %s (Restricted): %s (%s)\", p.Name, cleanKey, msg)\n\t\t\t\tcase \"Invalid\":\n\t\t\t\t\tcontinue\n\t\t\t\tcase \"Error\":\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t} else if severity == \"High\" {\n\t\t\tdetail += \" Validity not verified. No permission testing performed.\"\n\t\t}\n\n\t\tfmt.Printf(\"[+] SECRET MATCH: %s [%s]\\n\", p.Name, severity)\n\t\tonFound(core.Finding{\n\t\t\tType: \"Secret Leak\",\n\t\t\tTarget: sourceURL,\n\t\t\tDetail: detail,\n\t\t\tSeverity: severity,\n\t\t})\n\t}\n}\n\nfunc hasContextKeywords(content, key string, keywords []string) bool {\n\tidx := strings.Index(content, key)\n\tif idx == -1 {\n\t\treturn false\n\t}\n\n\tstart := idx - 100\n\tif start < 0 {\n\t\tstart = 0\n\t}\n\tend := idx + len(key) + 100\n\tif end > len(content) {\n\t\tend = len(content)\n\t}\n\n\tcontext := strings.ToLower(content[start:end])\n\tfor _, kw := range keywords {\n\t\tif strings.Contains(context, kw) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isLikelyRealPrivateKeyBlock(block string) bool {\n\tif strings.Contains(block, `replace(\"-----BEGIN PRIVATE KEY-----\"`) ||\n\t\tstrings.Contains(block, `replace(\"-----END PRIVATE KEY-----\"`) ||\n\t\tstrings.Contains(block, `REPLACE(\"-----BEGIN PRIVATE KEY-----\"`) ||\n\t\tstrings.Contains(block, `REPLACE(\"-----END PRIVATE KEY-----\"`) {\n\t\treturn false\n\t}\n\n\tre := regexp.MustCompile(`(?s)-----BEGIN (?:RSA |EC |PGP )?PRIVATE KEY-----\\s*([A-Za-z0-9+/=\\r\\n]+)\\s*-----END (?:RSA |EC |PGP )?PRIVATE KEY-----`)\n\tm := re.FindStringSubmatch(block)\n\tif len(m) < 2 {\n\t\treturn false\n\t}\n\n\tpayload := strings.TrimSpace(m[1])\n\tif payload == \"\" {\n\t\treturn false\n\t}\n\tif strings.ContainsAny(payload, `\"'\\{\\}\\[\\],;`) {\n\t\treturn false\n\t}\n\n\tcompact := strings.ReplaceAll(strings.ReplaceAll(payload, \"\\n\", \"\"), \"\\r\", \"\")\n\tif len(compact) < 128 {\n\t\treturn false\n\t}\n\n\treturn strings.Count(payload, \"\\n\") >= 2\n}\n\n// status: Valid, Restricted, Invalid, Error\nfunc verifyKey(key string, v *Validator) (string, string) {\n\tclient := &http.Client{Timeout: 5 * time.Second}\n\n\ttargetURL := v.URL\n\tif strings.Contains(targetURL, \"%s\") {\n\t\ttargetURL = fmt.Sprintf(targetURL, key)\n\t}\n\n\treq, err := http.NewRequest(v.Method, targetURL, nil)\n\tif err != nil {\n\t\treturn \"Error\", err.Error()\n\t}\n\n\tfor k, val := range v.Headers {\n\t\tif strings.Contains(val, \"%s\") {\n\t\t\treq.Header.Set(k, fmt.Sprintf(val, key))\n\t\t} else {\n\t\t\treq.Header.Set(k, val)\n\t\t}\n\t}\n\n\tif v.BasicAuthUser {\n\t\treq.SetBasicAuth(key, \"\")\n\t}\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"Error\", err.Error()\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, _ := io.ReadAll(io.LimitReader(io.LimitReader(resp.Body, 5*1024*1024), 5*1024*1024))\n\tbodyStr := string(body)\n\n\tif resp.StatusCode == v.ExpectedCode {\n\t\tif v.ExpectedContent != \"\" {\n\t\t\tif !strings.Contains(bodyStr, v.ExpectedContent) {\n\t\t\t\treturn \"Invalid\", \"Content mismatch\"\n\t\t\t}\n\t\t}\n\n\t\tif strings.Contains(bodyStr, \"REQUEST_DENIED\") || strings.Contains(bodyStr, \"API_KEY_INVALID\") || strings.Contains(bodyStr, \"key is invalid\") {\n\t\t\treturn \"Invalid\", \"Denied by Provider\"\n\t\t}\n\n\t\treturn \"Valid\", \"Active\"\n\t}\n\n\tif resp.StatusCode == 403 || resp.StatusCode == 401 {\n\t\treturn \"Restricted\", \"Unauthorized (Invalid or Restricted)\"\n\t}\n\n\tif resp.StatusCode == 401 {\n\t\treturn \"Invalid\", \"401 Unauthorized\"\n\t}\n\n\tif resp.StatusCode >= 500 {\n\t\treturn \"Error\", fmt.Sprintf(\"Server Error %d\", resp.StatusCode)\n\t}\n\n\treturn \"Invalid\", fmt.Sprintf(\"Status %d\", resp.StatusCode)\n}\n"}, "bd04b8da733490108e80911f9bc1821ca6206d9a": {"t": 1785759580.5539403, "n": 15575, "h": "41af2f4242f53352", "c": "package secrets\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/yupiyy/bhyakugan/internal/core\"\n\t\"github.com/yupiyy/bhyakugan/internal/utils\"\n)\n\n// Validator defines how to check if a key is live\ntype Validator struct {\n\tMethod string\n\tURL string // Use %s placeholder for the key\n\tHeaders map[string]string\n\tExpectedCode int\n\tExpectedContent string // Substring to look for in success response\n\tBasicAuthUser bool // If true, use key as username in Basic Auth\n}\n\ntype SecretPattern struct {\n\tName string\n\tPattern *regexp.Regexp\n\tSeverity string\n\tValidator *Validator\n}\n\nvar Patterns = []SecretPattern{\n\t{\n\t\tName: \"OpenAI API Key\",\n\t\tPattern: regexp.MustCompile(`sk-[a-zA-Z0-9]{48}|sk-proj-[a-zA-Z0-9-_]{48,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.openai.com/v1/models\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Grok (xAI) API Key\",\n\t\tPattern: regexp.MustCompile(`xai-[a-zA-Z0-9]{40,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.x.ai/v1/models\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"DeepSeek API Key\",\n\t\tPattern: regexp.MustCompile(`sk-[a-f0-9]{32}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.deepseek.com/user/balance\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Anthropic API Key\",\n\t\tPattern: regexp.MustCompile(`sk-ant-sid01-[a-zA-Z0-9-_]{93,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://api.anthropic.com/v1/messages\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"x-api-key\": \"%s\",\n\t\t\t\t\"anthropic-version\": \"2023-06-01\",\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t},\n\t\t\tExpectedCode: 400, // 400 = Key valid but request bad. 401 = Invalid.\n\t\t},\n\t},\n\t{\n\t\tName: \"HuggingFace Token\",\n\t\tPattern: regexp.MustCompile(`hf_[a-zA-Z0-9]{34}`),\n\t\tSeverity: \"High\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://huggingface.co/api/whoami-v2\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"AWS Access Key\",\n\t\tPattern: regexp.MustCompile(`\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b`),\n\t\tSeverity: \"Info\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"AWS Secret Access Key\",\n\t\t// Requiring a prefix or common assignment context to avoid entropy-only false positives\n\t\tPattern: regexp.MustCompile(`(?i)(?:aws_secret|aws_secret_access_key|secret_key|secret_access_key).{0,20}\\b([0-9a-zA-Z/+=]{40})\\b`),\n\t\tSeverity: \"Info\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Firebase Configuration\",\n\t\tPattern: regexp.MustCompile(`(?is)(?:firebase|config).*?apiKey\\s*[:=]\\s*[\"'](AIza[0-9A-Za-z\\-_]{35,})[\"']`),\n\t\tSeverity: \"High\",\n\t\tValidator: nil, // Note: The generic Google API Key validator in exploitation_engine.go handles Firebase installation abuse testing\n\t},\n\t{\n\t\tName: \"Google API Key\",\n\t\tPattern: regexp.MustCompile(`AIza[0-9A-Za-z\\-_]+`),\n\t\tSeverity: \"Info\",\n\t\t// Validation logic moved to exploitation_engine.go\n\t\tValidator: nil, \n\t},\n\t{\n\t\tName: \"Stripe Live Key\",\n\t\tPattern: regexp.MustCompile(`sk_live_[0-9a-zA-Z]{24,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.stripe.com/v1/customers\",\n\t\t\tBasicAuthUser: true,\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Slack Bot Token\",\n\t\tPattern: regexp.MustCompile(`xoxb-[0-9]{11,}-[0-9]{11,}-[0-9a-zA-Z]{24}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://slack.com/api/auth.test\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\", \"Content-Type\": \"application/json\"},\n\t\t\tExpectedCode: 200,\n\t\t\tExpectedContent: `\"ok\":true`,\n\t\t},\n\t},\n\t{\n\t\tName: \"GitHub PAT\",\n\t\tPattern: regexp.MustCompile(`ghp_[0-9a-zA-Z]{36}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.github.com/user\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"token %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Heroku API Key\",\n\t\t// Stricter Regex: Must be preceded by 'heroku'\n\t\tPattern: regexp.MustCompile(`(?i)(?:heroku).{0,20}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})`),\n\t\tSeverity: \"Medium\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://api.heroku.com/apps\",\n\t\t\tHeaders: map[string]string{\"Accept\": \"application/vnd.heroku+json; version=3\", \"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Private Key\",\n\t\tPattern: regexp.MustCompile(`(?s)-----BEGIN (?:RSA |EC |PGP )?PRIVATE KEY-----\\s*[A-Za-z0-9+/=\\r\\n]{64,}\\s*-----END (?:RSA |EC |PGP )?PRIVATE KEY-----`),\n\t\tSeverity: \"High\", // High (Unvalidated Credential)\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"CodeIgniter DB Config\",\n\t\tPattern: regexp.MustCompile(`'password'\\s*=>\\s*'[^']+'`),\n\t\tSeverity: \"Medium\", // Default to Medium (Potential Config Leak)\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"SQL Dump (Plaintext Admin)\",\n\t\tPattern: regexp.MustCompile(`(?i)INSERT\\s+INTO.*(?:user|admin|account).*(?:VALUES|\\().*`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"SQL Dump (PII Data)\",\n\t\tPattern: regexp.MustCompile(`(?i)INSERT\\s+INTO.*(?:donatur|member|customer).*(?:VALUES|\\().*`),\n\t\tSeverity: \"High\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Database Backup File\",\n\t\tPattern: regexp.MustCompile(`(?i)\\b[a-z0-9._/-]{0,120}(?:backup|dump|db)[a-z0-9._/-]{0,120}\\.sql\\b`),\n\t\tSeverity: \"Info\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Generic Client ID\",\n\t\tPattern: regexp.MustCompile(`(?i)(?:client_id|clientid|app_id|appid)\\s*[:=]\\s*[\"']([a-zA-Z0-9\\-_]{16,})[\"']`),\n\t\tSeverity: \"Info\",\n\t},\n\t{\n\t\tName: \"Generic Client Secret\",\n\t\tPattern: regexp.MustCompile(`(?i)(?:client_secret|clientsecret|app_secret|appsecret)\\s*[:=]\\s*[\"']([a-zA-Z0-9\\-_]{32,})[\"']`),\n\t\tSeverity: \"High\",\n\t},\n}\n\n// Additional helper for SQL content verification\nfunc isSQLContent(content string) bool {\n\tupper := strings.ToUpper(content)\n\t// Check for SQL header signatures or common statements\n\tif strings.Contains(upper, \"CREATE TABLE\") ||\n\t\tstrings.Contains(upper, \"INSERT INTO\") ||\n\t\tstrings.Contains(upper, \"MYSQLDUMP\") ||\n\t\tstrings.Contains(upper, \"-- MYSQL DUMP\") ||\n\t\tstrings.Contains(upper, \"SQLITE FORMAT\") ||\n\t\tstrings.Contains(upper, \"DROP TABLE\") ||\n\t\tstrings.Contains(upper, \"ALTER TABLE\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Scan checks the response body for secrets\nfunc Scan(url string, client *http.Client, onFound func(core.Finding)) {\n\treq, err := http.NewRequest(\"GET\", url, nil)\n\tif err != nil {\n\t\treturn\n\t}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, _ := io.ReadAll(resp.Body)\n\tbodyStr := string(body)\n\n\tDetectInContent(bodyStr, url, onFound)\n}\n\nfunc DetectInContent(content, sourceURL string, onFound func(core.Finding)) {\n\ttype FoundKey struct {\n\t\tName string\n\t\tKey string\n\t\tSeverity string\n\t\tPattern SecretPattern\n\t}\n\n\tfoundKeys := []FoundKey{}\n\tawsAccessKeys := []string{}\n\tawsSecretKeys := []string{}\n\n\tfor _, p := range Patterns {\n\t\tmatches := p.Pattern.FindAllStringSubmatch(content, -1)\n\t\tseen := make(map[string]bool)\n\t\tfor _, m := range matches {\n\t\t\trawKey := m[0]\n\t\t\tif len(m) > 1 {\n\t\t\t\trawKey = m[len(m)-1]\n\t\t\t}\n\t\t\tcleanKey := strings.Trim(rawKey, ` \"'=`)\n\n\t\t\tif len(cleanKey) < 8 && p.Name != \"Database Backup File\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif p.Name == \"Google API Key\" && len(cleanKey) < 35 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif p.Name == \"Private Key\" && !isLikelyRealPrivateKeyBlock(cleanKey) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Ignore placeholders / documentation tokens\n\t\t\tupperKey := strings.ToUpper(cleanKey)\n\t\t\tif strings.Contains(upperKey, \"EXAMPLE\") ||\n\t\t\t\tstrings.Contains(upperKey, \"TEST\") ||\n\t\t\t\tstrings.Contains(upperKey, \"MOCK\") ||\n\t\t\t\tstrings.Contains(upperKey, \"YOUR_\") ||\n\t\t\t\tstrings.Contains(upperKey, \"USERNAME\") ||\n\t\t\t\tstrings.Contains(upperKey, \"PASSWORD\") ||\n\t\t\t\tstrings.Contains(upperKey, \"TOKEN\") ||\n\t\t\t\tstrings.Contains(upperKey, \"<\") ||\n\t\t\t\tstrings.Contains(upperKey, \">\") {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif seen[cleanKey] {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tseen[cleanKey] = true\n\n\t\t\t// Special handling for AWS Secret Access Key: Entropy check and Context Validation\n\t\t\tif p.Name == \"AWS Secret Access Key\" {\n\t\t\t\tentropy := utils.CalculateShannonEntropy(cleanKey)\n\t\t\t\tif entropy < 4.3 { // Stricter threshold for solo keys to avoid JS noise\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// If no pair is found later, we want to know if 'aws' or 'key' keywords are nearby\n\t\t\t\t// We'll store the match index or just check context here\n\t\t\t\tif !hasContextKeywords(content, cleanKey, []string{\"aws\", \"secret\", \"access\", \"key\", \"s3\", \"bucket\", \"config\"}) {\n\t\t\t\t\tcontinue // Skip if no context found\n\t\t\t\t}\n\n\t\t\t\tawsSecretKeys = append(awsSecretKeys, cleanKey)\n\t\t\t}\n\n\t\t\tif p.Name == \"AWS Access Key\" {\n\t\t\t\tawsAccessKeys = append(awsAccessKeys, cleanKey)\n\t\t\t}\n\n\t\t\tfoundKeys = append(foundKeys, FoundKey{\n\t\t\t\tName: p.Name,\n\t\t\t\tKey: cleanKey,\n\t\t\t\tSeverity: p.Severity,\n\t\t\t\tPattern: p,\n\t\t\t})\n\t\t}\n\t}\n\n\t// Pair Detection for AWS\n\tisAWSPairFound := len(awsAccessKeys) > 0 && len(awsSecretKeys) > 0\n\tif isAWSPairFound {\n\t\tonFound(core.Finding{\n\t\t\tType: \"Secret Leak\",\n\t\t\tTarget: sourceURL,\n\t\t\tDetail: fmt.Sprintf(\"Found AWS Pair (Access Key ID and Secret Access Key) in the same content. This is a High Signal for Valid Credentials.\\nAccess Key: %s\\nSecret Key: [REDACTED]\", awsAccessKeys[0]),\n\t\t\tSeverity: \"High\",\n\t\t})\n\t}\n\n\tfor _, fk := range foundKeys {\n\t\tp := fk.Pattern\n\t\tcleanKey := fk.Key\n\t\tseverity := fk.Severity\n\t\tdetail := fmt.Sprintf(\"Found %s pattern.\", p.Name)\n\n\t\t// Special handling for Database Backup File:\n\t\tif p.Name == \"Database Backup File\" {\n\t\t\tif strings.HasSuffix(strings.ToLower(sourceURL), \".sql\") {\n\t\t\t\tif !isSQLContent(content) {\n\t\t\t\t\tcontinue // False Positive (Soft 404 or non-SQL content)\n\t\t\t\t}\n\t\t\t\tseverity = \"Critical\" // It's a verified dump!\n\t\t\t\tdetail = \"Verified SQL Dump file (Header/Structure confirmed).\"\n\t\t\t} else {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\t// Special handling for CodeIgniter Config\n\t\tif p.Name == \"CodeIgniter DB Config\" {\n\t\t\tif strings.Contains(cleanKey, \"''\") || strings.Contains(cleanKey, \"'password'\") { // empty password\n\t\t\t\tseverity = \"Medium\"\n\t\t\t\tdetail = \"Found DB Config pattern (Empty/Placeholder password).\"\n\t\t\t} else {\n\t\t\t\tseverity = \"High\"\n\t\t\t\tdetail = \"Found DB Config with potential password assignment.\"\n\t\t\t}\n\t\t}\n\n\t\tif p.Name == \"Google API Key\" {\n\t\t\tRunExploitationEngine(p.Name, cleanKey, sourceURL, onFound)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Don't duplicate if already flagged as AWS pair\n\t\tif isAWSPairFound && (p.Name == \"AWS Access Key\" || p.Name == \"AWS Secret Access Key\") {\n\t\t\t// Still might want to report them individually if they were found separately, but in the same content we already flagged the pair.\n\t\t\t// Let's report the Secret Key as Medium if part of a pair but not individually for now to avoid noise.\n\t\t\tif p.Name == \"AWS Secret Access Key\" {\n\t\t\t\tseverity = \"Medium\"\n\t\t\t\tdetail = fmt.Sprintf(\"AWS Secret Key part of a detected pair. Entropy: %.2f\", utils.CalculateShannonEntropy(cleanKey))\n\t\t\t} else {\n\t\t\t\tcontinue // Skip individual Access Key report if pair found\n\t\t\t}\n\t\t} else if p.Name == \"AWS Secret Access Key\" {\n\t\t\t// Individual AWS Secret Key with no Access Key found in same content\n\t\t\tseverity = \"Low\"\n\t\t\tdetail = fmt.Sprintf(\"Found AWS Secret Access Key pattern (No matching Access Key found). Entropy: %.2f\", utils.CalculateShannonEntropy(cleanKey))\n\t\t}\n\n\t\tif p.Validator != nil {\n\t\t\tif strings.Contains(sourceURL, \"localhost\") || strings.Contains(sourceURL, \"127.0.0.1\") {\n\t\t\t\tdetail = fmt.Sprintf(\"Found %s pattern (Validation skipped on localhost).\", p.Name)\n\t\t\t\t// Keep severity as defined in pattern\n\t\t\t} else {\n\t\t\t\tstatus, msg := verifyKey(cleanKey, p.Validator)\n\n\t\t\t\tswitch status {\n\t\t\t\tcase \"Valid\":\n\t\t\t\t\tseverity = \"Critical\"\n\t\t\t\t\tdetail = fmt.Sprintf(\"VERIFIED %s: %s (%s)\", p.Name, cleanKey, msg)\n\t\t\t\tcase \"Restricted\":\n\t\t\t\t\tseverity = \"Info\"\n\t\t\t\t\tdetail = fmt.Sprintf(\"VERIFIED %s (Restricted): %s (%s)\", p.Name, cleanKey, msg)\n\t\t\t\tcase \"Invalid\":\n\t\t\t\t\tcontinue\n\t\t\t\tcase \"Error\":\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t} else if severity == \"High\" {\n\t\t\tdetail += \" Validity not verified. No permission testing performed.\"\n\t\t}\n\n\t\tfmt.Printf(\"[+] SECRET MATCH: %s [%s]\\n\", p.Name, severity)\n\t\tonFound(core.Finding{\n\t\t\tType: \"Secret Leak\",\n\t\t\tTarget: sourceURL,\n\t\t\tDetail: detail,\n\t\t\tSeverity: severity,\n\t\t})\n\t}\n}\n\nfunc hasContextKeywords(content, key string, keywords []string) bool {\n\tidx := strings.Index(content, key)\n\tif idx == -1 {\n\t\treturn false\n\t}\n\n\tstart := idx - 100\n\tif start < 0 {\n\t\tstart = 0\n\t}\n\tend := idx + len(key) + 100\n\tif end > len(content) {\n\t\tend = len(content)\n\t}\n\n\tcontext := strings.ToLower(content[start:end])\n\tfor _, kw := range keywords {\n\t\tif strings.Contains(context, kw) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isLikelyRealPrivateKeyBlock(block string) bool {\n\tif strings.Contains(block, `replace(\"-----BEGIN PRIVATE KEY-----\"`) ||\n\t\tstrings.Contains(block, `replace(\"-----END PRIVATE KEY-----\"`) ||\n\t\tstrings.Contains(block, `REPLACE(\"-----BEGIN PRIVATE KEY-----\"`) ||\n\t\tstrings.Contains(block, `REPLACE(\"-----END PRIVATE KEY-----\"`) {\n\t\treturn false\n\t}\n\n\tre := regexp.MustCompile(`(?s)-----BEGIN (?:RSA |EC |PGP )?PRIVATE KEY-----\\s*([A-Za-z0-9+/=\\r\\n]+)\\s*-----END (?:RSA |EC |PGP )?PRIVATE KEY-----`)\n\tm := re.FindStringSubmatch(block)\n\tif len(m) < 2 {\n\t\treturn false\n\t}\n\n\tpayload := strings.TrimSpace(m[1])\n\tif payload == \"\" {\n\t\treturn false\n\t}\n\tif strings.ContainsAny(payload, `\"'\\{\\}\\[\\],;`) {\n\t\treturn false\n\t}\n\n\tcompact := strings.ReplaceAll(strings.ReplaceAll(payload, \"\\n\", \"\"), \"\\r\", \"\")\n\tif len(compact) < 128 {\n\t\treturn false\n\t}\n\n\treturn strings.Count(payload, \"\\n\") >= 2\n}\n\n// status: Valid, Restricted, Invalid, Error\nfunc verifyKey(key string, v *Validator) (string, string) {\n\tclient := &http.Client{Timeout: 5 * time.Second}\n\n\ttargetURL := v.URL\n\tif strings.Contains(targetURL, \"%s\") {\n\t\ttargetURL = fmt.Sprintf(targetURL, key)\n\t}\n\n\treq, err := http.NewRequest(v.Method, targetURL, nil)\n\tif err != nil {\n\t\treturn \"Error\", err.Error()\n\t}\n\n\tfor k, val := range v.Headers {\n\t\tif strings.Contains(val, \"%s\") {\n\t\t\treq.Header.Set(k, fmt.Sprintf(val, key))\n\t\t} else {\n\t\t\treq.Header.Set(k, val)\n\t\t}\n\t}\n\n\tif v.BasicAuthUser {\n\t\treq.SetBasicAuth(key, \"\")\n\t}\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"Error\", err.Error()\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, _ := io.ReadAll(resp.Body)\n\tbodyStr := string(body)\n\n\tif resp.StatusCode == v.ExpectedCode {\n\t\tif v.ExpectedContent != \"\" {\n\t\t\tif !strings.Contains(bodyStr, v.ExpectedContent) {\n\t\t\t\treturn \"Invalid\", \"Content mismatch\"\n\t\t\t}\n\t\t}\n\n\t\tif strings.Contains(bodyStr, \"REQUEST_DENIED\") || strings.Contains(bodyStr, \"API_KEY_INVALID\") || strings.Contains(bodyStr, \"key is invalid\") {\n\t\t\treturn \"Invalid\", \"Denied by Provider\"\n\t\t}\n\n\t\treturn \"Valid\", \"Active\"\n\t}\n\n\tif resp.StatusCode == 403 || resp.StatusCode == 401 {\n\t\treturn \"Restricted\", \"Unauthorized (Invalid or Restricted)\"\n\t}\n\n\tif resp.StatusCode == 401 {\n\t\treturn \"Invalid\", \"401 Unauthorized\"\n\t}\n\n\tif resp.StatusCode >= 500 {\n\t\treturn \"Error\", fmt.Sprintf(\"Server Error %d\", resp.StatusCode)\n\t}\n\n\treturn \"Invalid\", fmt.Sprintf(\"Status %d\", resp.StatusCode)\n}\n"}, "b607917f19eede4cb8b5e1dcb8a0d6100116f37d": {"t": 1785759587.5081887, "n": 15395, "h": "769c0bc25ebe1284", "c": "package secrets\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/yupiyy/bhyakugan/internal/core\"\n\t\"github.com/yupiyy/bhyakugan/internal/utils\"\n)\n\n// Validator defines how to check if a key is live\ntype Validator struct {\n\tMethod string\n\tURL string // Use %s placeholder for the key\n\tHeaders map[string]string\n\tExpectedCode int\n\tExpectedContent string // Substring to look for in success response\n\tBasicAuthUser bool // If true, use key as username in Basic Auth\n}\n\ntype SecretPattern struct {\n\tName string\n\tPattern *regexp.Regexp\n\tSeverity string\n\tValidator *Validator\n}\n\nvar Patterns = []SecretPattern{\n\t{\n\t\tName: \"OpenAI API Key\",\n\t\tPattern: regexp.MustCompile(`sk-[a-zA-Z0-9]{48}|sk-proj-[a-zA-Z0-9-_]{48,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.openai.com/v1/models\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Grok (xAI) API Key\",\n\t\tPattern: regexp.MustCompile(`xai-[a-zA-Z0-9]{40,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.x.ai/v1/models\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"DeepSeek API Key\",\n\t\tPattern: regexp.MustCompile(`sk-[a-f0-9]{32}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.deepseek.com/user/balance\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Anthropic API Key\",\n\t\tPattern: regexp.MustCompile(`sk-ant-sid01-[a-zA-Z0-9-_]{93,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://api.anthropic.com/v1/messages\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"x-api-key\": \"%s\",\n\t\t\t\t\"anthropic-version\": \"2023-06-01\",\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t},\n\t\t\tExpectedCode: 400, // 400 = Key valid but request bad. 401 = Invalid.\n\t\t},\n\t},\n\t{\n\t\tName: \"HuggingFace Token\",\n\t\tPattern: regexp.MustCompile(`hf_[a-zA-Z0-9]{34}`),\n\t\tSeverity: \"High\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://huggingface.co/api/whoami-v2\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"AWS Access Key\",\n\t\tPattern: regexp.MustCompile(`\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b`),\n\t\tSeverity: \"Info\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"AWS Secret Access Key\",\n\t\tPattern: regexp.MustCompile(`\\b[0-9a-zA-Z/+=]{40}\\b`),\n\t\tSeverity: \"Info\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Firebase Configuration\",\n\t\tPattern: regexp.MustCompile(`(?is)(?:firebase|config).*?apiKey\\s*[:=]\\s*[\"'](AIza[0-9A-Za-z\\-_]{35,})[\"']`),\n\t\tSeverity: \"High\",\n\t\tValidator: nil, // Note: The generic Google API Key validator in exploitation_engine.go handles Firebase installation abuse testing\n\t},\n\t{\n\t\tName: \"Google API Key\",\n\t\tPattern: regexp.MustCompile(`AIza[0-9A-Za-z\\-_]+`),\n\t\tSeverity: \"Info\",\n\t\t// Validation logic moved to exploitation_engine.go\n\t\tValidator: nil, \n\t},\n\t{\n\t\tName: \"Stripe Live Key\",\n\t\tPattern: regexp.MustCompile(`sk_live_[0-9a-zA-Z]{24,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.stripe.com/v1/customers\",\n\t\t\tBasicAuthUser: true,\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Slack Bot Token\",\n\t\tPattern: regexp.MustCompile(`xoxb-[0-9]{11,}-[0-9]{11,}-[0-9a-zA-Z]{24}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://slack.com/api/auth.test\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\", \"Content-Type\": \"application/json\"},\n\t\t\tExpectedCode: 200,\n\t\t\tExpectedContent: `\"ok\":true`,\n\t\t},\n\t},\n\t{\n\t\tName: \"GitHub PAT\",\n\t\tPattern: regexp.MustCompile(`ghp_[0-9a-zA-Z]{36}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.github.com/user\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"token %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Heroku API Key\",\n\t\t// Stricter Regex: Must be preceded by 'heroku'\n\t\tPattern: regexp.MustCompile(`(?i)(?:heroku).{0,20}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})`),\n\t\tSeverity: \"Medium\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://api.heroku.com/apps\",\n\t\t\tHeaders: map[string]string{\"Accept\": \"application/vnd.heroku+json; version=3\", \"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Private Key\",\n\t\tPattern: regexp.MustCompile(`(?s)-----BEGIN (?:RSA |EC |PGP )?PRIVATE KEY-----\\s*[A-Za-z0-9+/=\\r\\n]{64,}\\s*-----END (?:RSA |EC |PGP )?PRIVATE KEY-----`),\n\t\tSeverity: \"High\", // High (Unvalidated Credential)\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"CodeIgniter DB Config\",\n\t\tPattern: regexp.MustCompile(`'password'\\s*=>\\s*'[^']+'`),\n\t\tSeverity: \"Medium\", // Default to Medium (Potential Config Leak)\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"SQL Dump (Plaintext Admin)\",\n\t\tPattern: regexp.MustCompile(`(?i)INSERT\\s+INTO.*(?:user|admin|account).*(?:VALUES|\\().*`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"SQL Dump (PII Data)\",\n\t\tPattern: regexp.MustCompile(`(?i)INSERT\\s+INTO.*(?:donatur|member|customer).*(?:VALUES|\\().*`),\n\t\tSeverity: \"High\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Database Backup File\",\n\t\tPattern: regexp.MustCompile(`(?i)\\b[a-z0-9._/-]{0,120}(?:backup|dump|db)[a-z0-9._/-]{0,120}\\.sql\\b`),\n\t\tSeverity: \"Info\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Generic Client ID\",\n\t\tPattern: regexp.MustCompile(`(?i)(?:client_id|clientid|app_id|appid)\\s*[:=]\\s*[\"']([a-zA-Z0-9\\-_]{16,})[\"']`),\n\t\tSeverity: \"Info\",\n\t},\n\t{\n\t\tName: \"Generic Client Secret\",\n\t\tPattern: regexp.MustCompile(`(?i)(?:client_secret|clientsecret|app_secret|appsecret)\\s*[:=]\\s*[\"']([a-zA-Z0-9\\-_]{32,})[\"']`),\n\t\tSeverity: \"High\",\n\t},\n}\n\n// Additional helper for SQL content verification\nfunc isSQLContent(content string) bool {\n\tupper := strings.ToUpper(content)\n\t// Check for SQL header signatures or common statements\n\tif strings.Contains(upper, \"CREATE TABLE\") ||\n\t\tstrings.Contains(upper, \"INSERT INTO\") ||\n\t\tstrings.Contains(upper, \"MYSQLDUMP\") ||\n\t\tstrings.Contains(upper, \"-- MYSQL DUMP\") ||\n\t\tstrings.Contains(upper, \"SQLITE FORMAT\") ||\n\t\tstrings.Contains(upper, \"DROP TABLE\") ||\n\t\tstrings.Contains(upper, \"ALTER TABLE\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Scan checks the response body for secrets\nfunc Scan(url string, client *http.Client, onFound func(core.Finding)) {\n\treq, err := http.NewRequest(\"GET\", url, nil)\n\tif err != nil {\n\t\treturn\n\t}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, _ := io.ReadAll(resp.Body)\n\tbodyStr := string(body)\n\n\tDetectInContent(bodyStr, url, onFound)\n}\n\nfunc DetectInContent(content, sourceURL string, onFound func(core.Finding)) {\n\ttype FoundKey struct {\n\t\tName string\n\t\tKey string\n\t\tSeverity string\n\t\tPattern SecretPattern\n\t}\n\n\tfoundKeys := []FoundKey{}\n\tawsAccessKeys := []string{}\n\tawsSecretKeys := []string{}\n\n\tfor _, p := range Patterns {\n\t\tmatches := p.Pattern.FindAllStringSubmatch(content, -1)\n\t\tseen := make(map[string]bool)\n\t\tfor _, m := range matches {\n\t\t\trawKey := m[0]\n\t\t\tif len(m) > 1 {\n\t\t\t\trawKey = m[len(m)-1]\n\t\t\t}\n\t\t\tcleanKey := strings.Trim(rawKey, ` \"'=`)\n\n\t\t\tif len(cleanKey) < 8 && p.Name != \"Database Backup File\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif p.Name == \"Google API Key\" && len(cleanKey) < 35 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif p.Name == \"Private Key\" && !isLikelyRealPrivateKeyBlock(cleanKey) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Ignore placeholders / documentation tokens\n\t\t\tupperKey := strings.ToUpper(cleanKey)\n\t\t\tif strings.Contains(upperKey, \"EXAMPLE\") ||\n\t\t\t\tstrings.Contains(upperKey, \"TEST\") ||\n\t\t\t\tstrings.Contains(upperKey, \"MOCK\") ||\n\t\t\t\tstrings.Contains(upperKey, \"YOUR_\") ||\n\t\t\t\tstrings.Contains(upperKey, \"USERNAME\") ||\n\t\t\t\tstrings.Contains(upperKey, \"PASSWORD\") ||\n\t\t\t\tstrings.Contains(upperKey, \"TOKEN\") ||\n\t\t\t\tstrings.Contains(upperKey, \"<\") ||\n\t\t\t\tstrings.Contains(upperKey, \">\") {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif seen[cleanKey] {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tseen[cleanKey] = true\n\n\t\t\t// Special handling for AWS Secret Access Key: Entropy check and Context Validation\n\t\t\tif p.Name == \"AWS Secret Access Key\" {\n\t\t\t\tentropy := utils.CalculateShannonEntropy(cleanKey)\n\t\t\t\tif entropy < 3.7 { // Slightly higher threshold for solo keys\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// If no pair is found later, we want to know if 'aws' or 'key' keywords are nearby\n\t\t\t\t// We'll store the match index or just check context here\n\t\t\t\tif !hasContextKeywords(content, cleanKey, []string{\"aws\", \"secret\", \"access\", \"key\", \"s3\", \"bucket\", \"config\"}) {\n\t\t\t\t\tcontinue // Skip if no context found\n\t\t\t\t}\n\n\t\t\t\tawsSecretKeys = append(awsSecretKeys, cleanKey)\n\t\t\t}\n\n\t\t\tif p.Name == \"AWS Access Key\" {\n\t\t\t\tawsAccessKeys = append(awsAccessKeys, cleanKey)\n\t\t\t}\n\n\t\t\tfoundKeys = append(foundKeys, FoundKey{\n\t\t\t\tName: p.Name,\n\t\t\t\tKey: cleanKey,\n\t\t\t\tSeverity: p.Severity,\n\t\t\t\tPattern: p,\n\t\t\t})\n\t\t}\n\t}\n\n\t// Pair Detection for AWS\n\tisAWSPairFound := len(awsAccessKeys) > 0 && len(awsSecretKeys) > 0\n\tif isAWSPairFound {\n\t\tonFound(core.Finding{\n\t\t\tType: \"Secret Leak\",\n\t\t\tTarget: sourceURL,\n\t\t\tDetail: fmt.Sprintf(\"Found AWS Pair (Access Key ID and Secret Access Key) in the same content. This is a High Signal for Valid Credentials.\\nAccess Key: %s\\nSecret Key: [REDACTED]\", awsAccessKeys[0]),\n\t\t\tSeverity: \"High\",\n\t\t})\n\t}\n\n\tfor _, fk := range foundKeys {\n\t\tp := fk.Pattern\n\t\tcleanKey := fk.Key\n\t\tseverity := fk.Severity\n\t\tdetail := fmt.Sprintf(\"Found %s pattern.\", p.Name)\n\n\t\t// Special handling for Database Backup File:\n\t\tif p.Name == \"Database Backup File\" {\n\t\t\tif strings.HasSuffix(strings.ToLower(sourceURL), \".sql\") {\n\t\t\t\tif !isSQLContent(content) {\n\t\t\t\t\tcontinue // False Positive (Soft 404 or non-SQL content)\n\t\t\t\t}\n\t\t\t\tseverity = \"Critical\" // It's a verified dump!\n\t\t\t\tdetail = \"Verified SQL Dump file (Header/Structure confirmed).\"\n\t\t\t} else {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\t// Special handling for CodeIgniter Config\n\t\tif p.Name == \"CodeIgniter DB Config\" {\n\t\t\tif strings.Contains(cleanKey, \"''\") || strings.Contains(cleanKey, \"'password'\") { // empty password\n\t\t\t\tseverity = \"Medium\"\n\t\t\t\tdetail = \"Found DB Config pattern (Empty/Placeholder password).\"\n\t\t\t} else {\n\t\t\t\tseverity = \"High\"\n\t\t\t\tdetail = \"Found DB Config with potential password assignment.\"\n\t\t\t}\n\t\t}\n\n\t\tif p.Name == \"Google API Key\" {\n\t\t\tRunExploitationEngine(p.Name, cleanKey, sourceURL, onFound)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Don't duplicate if already flagged as AWS pair\n\t\tif isAWSPairFound && (p.Name == \"AWS Access Key\" || p.Name == \"AWS Secret Access Key\") {\n\t\t\t// Still might want to report them individually if they were found separately, but in the same content we already flagged the pair.\n\t\t\t// Let's report the Secret Key as Medium if part of a pair but not individually for now to avoid noise.\n\t\t\tif p.Name == \"AWS Secret Access Key\" {\n\t\t\t\tseverity = \"Medium\"\n\t\t\t\tdetail = fmt.Sprintf(\"AWS Secret Key part of a detected pair. Entropy: %.2f\", utils.CalculateShannonEntropy(cleanKey))\n\t\t\t} else {\n\t\t\t\tcontinue // Skip individual Access Key report if pair found\n\t\t\t}\n\t\t} else if p.Name == \"AWS Secret Access Key\" {\n\t\t\t// Individual AWS Secret Key with no Access Key found in same content\n\t\t\tseverity = \"Low\"\n\t\t\tdetail = fmt.Sprintf(\"Found AWS Secret Access Key pattern (No matching Access Key found). Entropy: %.2f\", utils.CalculateShannonEntropy(cleanKey))\n\t\t}\n\n\t\tif p.Validator != nil {\n\t\t\tif strings.Contains(sourceURL, \"localhost\") || strings.Contains(sourceURL, \"127.0.0.1\") {\n\t\t\t\tdetail = fmt.Sprintf(\"Found %s pattern (Validation skipped on localhost).\", p.Name)\n\t\t\t\t// Keep severity as defined in pattern\n\t\t\t} else {\n\t\t\t\tstatus, msg := verifyKey(cleanKey, p.Validator)\n\n\t\t\t\tswitch status {\n\t\t\t\tcase \"Valid\":\n\t\t\t\t\tseverity = \"Critical\"\n\t\t\t\t\tdetail = fmt.Sprintf(\"VERIFIED %s: %s (%s)\", p.Name, cleanKey, msg)\n\t\t\t\tcase \"Restricted\":\n\t\t\t\t\tseverity = \"Info\"\n\t\t\t\t\tdetail = fmt.Sprintf(\"VERIFIED %s (Restricted): %s (%s)\", p.Name, cleanKey, msg)\n\t\t\t\tcase \"Invalid\":\n\t\t\t\t\tcontinue\n\t\t\t\tcase \"Error\":\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t} else if severity == \"High\" {\n\t\t\tdetail += \" Validity not verified. No permission testing performed.\"\n\t\t}\n\n\t\tfmt.Printf(\"[+] SECRET MATCH: %s [%s]\\n\", p.Name, severity)\n\t\tonFound(core.Finding{\n\t\t\tType: \"Secret Leak\",\n\t\t\tTarget: sourceURL,\n\t\t\tDetail: detail,\n\t\t\tSeverity: severity,\n\t\t})\n\t}\n}\n\nfunc hasContextKeywords(content, key string, keywords []string) bool {\n\tidx := strings.Index(content, key)\n\tif idx == -1 {\n\t\treturn false\n\t}\n\n\tstart := idx - 100\n\tif start < 0 {\n\t\tstart = 0\n\t}\n\tend := idx + len(key) + 100\n\tif end > len(content) {\n\t\tend = len(content)\n\t}\n\n\tcontext := strings.ToLower(content[start:end])\n\tfor _, kw := range keywords {\n\t\tif strings.Contains(context, kw) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isLikelyRealPrivateKeyBlock(block string) bool {\n\tif strings.Contains(block, `replace(\"-----BEGIN PRIVATE KEY-----\"`) ||\n\t\tstrings.Contains(block, `replace(\"-----END PRIVATE KEY-----\"`) ||\n\t\tstrings.Contains(block, `REPLACE(\"-----BEGIN PRIVATE KEY-----\"`) ||\n\t\tstrings.Contains(block, `REPLACE(\"-----END PRIVATE KEY-----\"`) {\n\t\treturn false\n\t}\n\n\tre := regexp.MustCompile(`(?s)-----BEGIN (?:RSA |EC |PGP )?PRIVATE KEY-----\\s*([A-Za-z0-9+/=\\r\\n]+)\\s*-----END (?:RSA |EC |PGP )?PRIVATE KEY-----`)\n\tm := re.FindStringSubmatch(block)\n\tif len(m) < 2 {\n\t\treturn false\n\t}\n\n\tpayload := strings.TrimSpace(m[1])\n\tif payload == \"\" {\n\t\treturn false\n\t}\n\tif strings.ContainsAny(payload, `\"'\\{\\}\\[\\],;`) {\n\t\treturn false\n\t}\n\n\tcompact := strings.ReplaceAll(strings.ReplaceAll(payload, \"\\n\", \"\"), \"\\r\", \"\")\n\tif len(compact) < 128 {\n\t\treturn false\n\t}\n\n\treturn strings.Count(payload, \"\\n\") >= 2\n}\n\n// status: Valid, Restricted, Invalid, Error\nfunc verifyKey(key string, v *Validator) (string, string) {\n\tclient := &http.Client{Timeout: 5 * time.Second}\n\n\ttargetURL := v.URL\n\tif strings.Contains(targetURL, \"%s\") {\n\t\ttargetURL = fmt.Sprintf(targetURL, key)\n\t}\n\n\treq, err := http.NewRequest(v.Method, targetURL, nil)\n\tif err != nil {\n\t\treturn \"Error\", err.Error()\n\t}\n\n\tfor k, val := range v.Headers {\n\t\tif strings.Contains(val, \"%s\") {\n\t\t\treq.Header.Set(k, fmt.Sprintf(val, key))\n\t\t} else {\n\t\t\treq.Header.Set(k, val)\n\t\t}\n\t}\n\n\tif v.BasicAuthUser {\n\t\treq.SetBasicAuth(key, \"\")\n\t}\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"Error\", err.Error()\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, _ := io.ReadAll(resp.Body)\n\tbodyStr := string(body)\n\n\tif resp.StatusCode == v.ExpectedCode {\n\t\tif v.ExpectedContent != \"\" {\n\t\t\tif !strings.Contains(bodyStr, v.ExpectedContent) {\n\t\t\t\treturn \"Invalid\", \"Content mismatch\"\n\t\t\t}\n\t\t}\n\n\t\tif strings.Contains(bodyStr, \"REQUEST_DENIED\") || strings.Contains(bodyStr, \"API_KEY_INVALID\") || strings.Contains(bodyStr, \"key is invalid\") {\n\t\t\treturn \"Invalid\", \"Denied by Provider\"\n\t\t}\n\n\t\treturn \"Valid\", \"Active\"\n\t}\n\n\tif resp.StatusCode == 403 || resp.StatusCode == 401 {\n\t\treturn \"Restricted\", \"Unauthorized (Invalid or Restricted)\"\n\t}\n\n\tif resp.StatusCode == 401 {\n\t\treturn \"Invalid\", \"401 Unauthorized\"\n\t}\n\n\tif resp.StatusCode >= 500 {\n\t\treturn \"Error\", fmt.Sprintf(\"Server Error %d\", resp.StatusCode)\n\t}\n\n\treturn \"Invalid\", fmt.Sprintf(\"Status %d\", resp.StatusCode)\n}\n"}, "1209f2e26f89fb19a14b53a590f1574bc20268d7": {"t": 1785759596.0138652, "n": 11041, "h": "dfa65cb93c7d65e1", "c": "package secrets\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/yupiyy/bhyakugan/internal/core\"\n)\n\n// Validator defines how to check if a key is live\ntype Validator struct {\n\tMethod string\n\tURL string // Use %s placeholder for the key\n\tHeaders map[string]string\n\tExpectedCode int\n\tExpectedContent string // Substring to look for in success response\n\tBasicAuthUser bool // If true, use key as username in Basic Auth\n}\n\ntype SecretPattern struct {\n\tName string\n\tPattern *regexp.Regexp\n\tSeverity string\n\tValidator *Validator\n}\n\nvar Patterns = []SecretPattern{\n\t{\n\t\tName: \"OpenAI API Key\",\n\t\tPattern: regexp.MustCompile(`sk-[a-zA-Z0-9]{48}|sk-proj-[a-zA-Z0-9-_]{48,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.openai.com/v1/models\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Anthropic API Key\",\n\t\tPattern: regexp.MustCompile(`sk-ant-sid01-[a-zA-Z0-9-_]{93,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://api.anthropic.com/v1/messages\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"x-api-key\": \"%s\",\n\t\t\t\t\"anthropic-version\": \"2023-06-01\",\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t},\n\t\t\tExpectedCode: 400, // 400 = Key valid but request bad. 401 = Invalid.\n\t\t},\n\t},\n\t{\n\t\tName: \"HuggingFace Token\",\n\t\tPattern: regexp.MustCompile(`hf_[a-zA-Z0-9]{34}`),\n\t\tSeverity: \"High\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://huggingface.co/api/whoami-v2\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"AWS Access Key\",\n\t\tPattern: regexp.MustCompile(`\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b`),\n\t\tSeverity: \"Info\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"AWS Secret Access Key\",\n\t\tPattern: regexp.MustCompile(`\\b[0-9a-zA-Z/+=]{40}\\b`),\n\t\tSeverity: \"Info\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Google API Key\",\n\t\tPattern: regexp.MustCompile(`AIza[0-9A-Za-z\\-_]+`),\n\t\tSeverity: \"Info\",\n\t\t// Validation logic moved to exploitation_engine.go\n\t\tValidator: nil, \n\t},\n\t{\n\t\tName: \"Stripe Live Key\",\n\t\tPattern: regexp.MustCompile(`sk_live_[0-9a-zA-Z]{24,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.stripe.com/v1/customers\",\n\t\t\tBasicAuthUser: true,\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Slack Bot Token\",\n\t\tPattern: regexp.MustCompile(`xoxb-[0-9]{11,}-[0-9]{11,}-[0-9a-zA-Z]{24}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://slack.com/api/auth.test\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\", \"Content-Type\": \"application/json\"},\n\t\t\tExpectedCode: 200,\n\t\t\tExpectedContent: `\"ok\":true`,\n\t\t},\n\t},\n\t{\n\t\tName: \"GitHub PAT\",\n\t\tPattern: regexp.MustCompile(`ghp_[0-9a-zA-Z]{36}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.github.com/user\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"token %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Heroku API Key\",\n\t\t// Stricter Regex: Must be preceded by 'heroku'\n\t\tPattern: regexp.MustCompile(`(?i)(?:heroku).{0,20}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})`),\n\t\tSeverity: \"Medium\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://api.heroku.com/apps\",\n\t\t\tHeaders: map[string]string{\"Accept\": \"application/vnd.heroku+json; version=3\", \"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Private Key\",\n\t\tPattern: regexp.MustCompile(`(?s)-----BEGIN (?:RSA |EC |PGP )?PRIVATE KEY-----\\s*[A-Za-z0-9+/=\\r\\n]{64,}\\s*-----END (?:RSA |EC |PGP )?PRIVATE KEY-----`),\n\t\tSeverity: \"High\", // High (Unvalidated Credential)\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"CodeIgniter DB Config\",\n\t\tPattern: regexp.MustCompile(`'password'\\s*=>\\s*'[^']+'`),\n\t\tSeverity: \"Medium\", // Default to Medium (Potential Config Leak)\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"SQL Dump (Plaintext Admin)\",\n\t\tPattern: regexp.MustCompile(`(?i)INSERT\\s+INTO.*(?:user|admin|account).*(?:VALUES|\\().*`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"SQL Dump (PII Data)\",\n\t\tPattern: regexp.MustCompile(`(?i)INSERT\\s+INTO.*(?:donatur|member|customer).*(?:VALUES|\\().*`),\n\t\tSeverity: \"High\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Database Backup File\",\n\t\tPattern: regexp.MustCompile(`(?i)\\b[a-z0-9._/-]{0,120}(?:backup|dump|db)[a-z0-9._/-]{0,120}\\.sql\\b`),\n\t\tSeverity: \"Info\",\n\t\tValidator: nil,\n\t},\n}\n\n// Additional helper for SQL content verification\nfunc isSQLContent(content string) bool {\n\tupper := strings.ToUpper(content)\n\t// Check for SQL header signatures or common statements\n\tif strings.Contains(upper, \"CREATE TABLE\") ||\n\t\tstrings.Contains(upper, \"INSERT INTO\") ||\n\t\tstrings.Contains(upper, \"MYSQLDUMP\") ||\n\t\tstrings.Contains(upper, \"-- MYSQL DUMP\") ||\n\t\tstrings.Contains(upper, \"SQLITE FORMAT\") ||\n\t\tstrings.Contains(upper, \"DROP TABLE\") ||\n\t\tstrings.Contains(upper, \"ALTER TABLE\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Scan checks the response body for secrets\nfunc Scan(url string, client *http.Client, onFound func(core.Finding)) {\n\treq, err := http.NewRequest(\"GET\", url, nil)\n\tif err != nil {\n\t\treturn\n\t}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, _ := io.ReadAll(resp.Body)\n\tbodyStr := string(body)\n\n\tDetectInContent(bodyStr, url, onFound)\n}\n\nfunc DetectInContent(content, sourceURL string, onFound func(core.Finding)) {\n\tfor _, p := range Patterns {\n\t\tmatches := p.Pattern.FindAllStringSubmatch(content, -1)\n\t\tseen := make(map[string]bool)\n\t\tfor _, m := range matches {\n\t\t\trawKey := m[0]\n\t\t\tif len(m) > 1 {\n\t\t\t\trawKey = m[len(m)-1]\n\t\t\t}\n\t\t\tcleanKey := strings.Trim(rawKey, ` \"'=`)\n\t\t\t\n\t\t\tif len(cleanKey) < 8 && p.Name != \"Database Backup File\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif p.Name == \"Google API Key\" && len(cleanKey) < 35 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif p.Name == \"Private Key\" && !isLikelyRealPrivateKeyBlock(cleanKey) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Ignore placeholders / documentation tokens\n\t\t\tupperKey := strings.ToUpper(cleanKey)\n\t\t\tif strings.Contains(upperKey, \"EXAMPLE\") ||\n\t\t\t\tstrings.Contains(upperKey, \"TEST\") ||\n\t\t\t\tstrings.Contains(upperKey, \"MOCK\") ||\n\t\t\t\tstrings.Contains(upperKey, \"YOUR_\") ||\n\t\t\t\tstrings.Contains(upperKey, \"USERNAME\") ||\n\t\t\t\tstrings.Contains(upperKey, \"PASSWORD\") ||\n\t\t\t\tstrings.Contains(upperKey, \"TOKEN\") ||\n\t\t\t\tstrings.Contains(upperKey, \"<\") ||\n\t\t\t\tstrings.Contains(upperKey, \">\") {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif seen[cleanKey] {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tseen[cleanKey] = true\n\n\t\t\tdetail := fmt.Sprintf(\"Found %s pattern.\", p.Name)\n\t\t\tseverity := p.Severity\n\n\t\t\t// Special handling for Database Backup File:\n\t\t\tif p.Name == \"Database Backup File\" {\n\t\t\t\tif strings.HasSuffix(strings.ToLower(sourceURL), \".sql\") {\n\t\t\t\t\tif !isSQLContent(content) {\n\t\t\t\t\t\tcontinue // False Positive (Soft 404 or non-SQL content)\n\t\t\t\t\t}\n\t\t\t\t\tseverity = \"Critical\" // It's a verified dump!\n\t\t\t\t\tdetail = \"Verified SQL Dump file (Header/Structure confirmed).\"\n\t\t\t\t} else {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Special handling for CodeIgniter Config\n\t\t\tif p.Name == \"CodeIgniter DB Config\" {\n\t\t\t\tif strings.Contains(cleanKey, \"''\") || strings.Contains(cleanKey, \"'password'\") { // empty password\n\t\t\t\t\tseverity = \"Medium\"\n\t\t\t\t\tdetail = \"Found DB Config pattern (Empty/Placeholder password).\"\n\t\t\t\t} else {\n\t\t\t\t\tseverity = \"High\"\n\t\t\t\t\tdetail = \"Found DB Config with potential password assignment.\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif p.Name == \"Google API Key\" {\n\t\t\t\tRunExploitationEngine(p.Name, cleanKey, sourceURL, onFound)\n\t\t\t\tcontinue \n\t\t\t}\n\n\t\t\tif p.Validator != nil {\n\t\t\t\tif strings.Contains(sourceURL, \"localhost\") && strings.Contains(p.Validator.URL, \"localhost\") {\n\t\t\t\t\t// Skip\n\t\t\t\t} else {\n\t\t\t\t\tstatus, msg := verifyKey(cleanKey, p.Validator)\n\n\t\t\t\t\tswitch status {\n\t\t\t\t\tcase \"Valid\":\n\t\t\t\t\t\tseverity = \"Critical\"\n\t\t\t\t\t\tdetail = fmt.Sprintf(\"VERIFIED %s: %s (%s)\", p.Name, cleanKey, msg)\n\t\t\t\t\tcase \"Restricted\":\n\t\t\t\t\t\tseverity = \"Info\"\n\t\t\t\t\t\tdetail = fmt.Sprintf(\"VERIFIED %s (Restricted): %s (%s)\", p.Name, cleanKey, msg)\n\t\t\t\t\tcase \"Invalid\":\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tcase \"Error\":\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if severity == \"High\" {\n\t\t\t\tdetail += \" Validity not verified. No permission testing performed.\"\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[+] SECRET MATCH: %s [%s]\\n\", p.Name, severity)\n\t\t\tonFound(core.Finding{\n\t\t\t\tType: \"Secret Leak\",\n\t\t\t\tTarget: sourceURL,\n\t\t\t\tDetail: detail,\n\t\t\t\tSeverity: severity,\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc isLikelyRealPrivateKeyBlock(block string) bool {\n\tif strings.Contains(block, `replace(\"-----BEGIN PRIVATE KEY-----\"`) ||\n\t\tstrings.Contains(block, `replace(\"-----END PRIVATE KEY-----\"`) ||\n\t\tstrings.Contains(block, `REPLACE(\"-----BEGIN PRIVATE KEY-----\"`) ||\n\t\tstrings.Contains(block, `REPLACE(\"-----END PRIVATE KEY-----\"`) {\n\t\treturn false\n\t}\n\n\tre := regexp.MustCompile(`(?s)-----BEGIN (?:RSA |EC |PGP )?PRIVATE KEY-----\\s*([A-Za-z0-9+/=\\r\\n]+)\\s*-----END (?:RSA |EC |PGP )?PRIVATE KEY-----`)\n\tm := re.FindStringSubmatch(block)\n\tif len(m) < 2 {\n\t\treturn false\n\t}\n\n\tpayload := strings.TrimSpace(m[1])\n\tif payload == \"\" {\n\t\treturn false\n\t}\n\tif strings.ContainsAny(payload, `\"'\\{\\}\\[\\],;`) {\n\t\treturn false\n\t}\n\n\tcompact := strings.ReplaceAll(strings.ReplaceAll(payload, \"\\n\", \"\"), \"\\r\", \"\")\n\tif len(compact) < 128 {\n\t\treturn false\n\t}\n\n\treturn strings.Count(payload, \"\\n\") >= 2\n}\n\n// status: Valid, Restricted, Invalid, Error\nfunc verifyKey(key string, v *Validator) (string, string) {\n\tclient := &http.Client{Timeout: 5 * time.Second}\n\n\ttargetURL := v.URL\n\tif strings.Contains(targetURL, \"%s\") {\n\t\ttargetURL = fmt.Sprintf(targetURL, key)\n\t}\n\n\treq, err := http.NewRequest(v.Method, targetURL, nil)\n\tif err != nil {\n\t\treturn \"Error\", err.Error()\n\t}\n\n\tfor k, val := range v.Headers {\n\t\tif strings.Contains(val, \"%s\") {\n\t\t\treq.Header.Set(k, fmt.Sprintf(val, key))\n\t\t} else {\n\t\t\treq.Header.Set(k, val)\n\t\t}\n\t}\n\n\tif v.BasicAuthUser {\n\t\treq.SetBasicAuth(key, \"\")\n\t}\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"Error\", err.Error()\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, _ := io.ReadAll(resp.Body)\n\tbodyStr := string(body)\n\n\tif resp.StatusCode == v.ExpectedCode {\n\t\tif v.ExpectedContent != \"\" {\n\t\t\tif !strings.Contains(bodyStr, v.ExpectedContent) {\n\t\t\t\treturn \"Invalid\", \"Content mismatch\"\n\t\t\t}\n\t\t}\n\n\t\tif strings.Contains(bodyStr, \"REQUEST_DENIED\") || strings.Contains(bodyStr, \"API_KEY_INVALID\") || strings.Contains(bodyStr, \"key is invalid\") {\n\t\t\treturn \"Invalid\", \"Denied by Provider\"\n\t\t}\n\n\t\treturn \"Valid\", \"Active\"\n\t}\n\n\tif resp.StatusCode == 403 || resp.StatusCode == 401 {\n\t\treturn \"Restricted\", \"Unauthorized (Invalid or Restricted)\"\n\t}\n\n\tif resp.StatusCode == 401 {\n\t\treturn \"Invalid\", \"401 Unauthorized\"\n\t}\n\n\tif resp.StatusCode >= 500 {\n\t\treturn \"Error\", fmt.Sprintf(\"Server Error %d\", resp.StatusCode)\n\t}\n\n\treturn \"Invalid\", fmt.Sprintf(\"Status %d\", resp.StatusCode)\n}\n"}, "197a8cf9a0bc5e2b6bf07f37060926a5cd6d693e": {"t": 1785759598.531079, "n": 12778, "h": "ccf9feb1148d86b2", "c": "package secrets\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/yupiyy/bhyakugan/internal/core\"\n)\n\n// Validator defines how to check if a key is live\ntype Validator struct {\n\tMethod string\n\tURL string // Use %s placeholder for the key\n\tHeaders map[string]string\n\tExpectedCode int\n\tExpectedContent string // Substring to look for in success response\n\tBasicAuthUser bool // If true, use key as username in Basic Auth\n}\n\ntype SecretPattern struct {\n\tName string\n\tPattern *regexp.Regexp\n\tSeverity string\n\tValidator *Validator\n}\n\nvar Patterns = []SecretPattern{\n\t{\n\t\tName: \"OpenAI API Key\",\n\t\tPattern: regexp.MustCompile(`sk-[a-zA-Z0-9]{48}|sk-proj-[a-zA-Z0-9-_]{48,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.openai.com/v1/models\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Anthropic API Key\",\n\t\tPattern: regexp.MustCompile(`sk-ant-sid01-[a-zA-Z0-9-_]{93,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://api.anthropic.com/v1/messages\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"x-api-key\": \"%s\",\n\t\t\t\t\"anthropic-version\": \"2023-06-01\",\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t},\n\t\t\tExpectedCode: 400, // 400 = Key valid but request bad. 401 = Invalid.\n\t\t},\n\t},\n\t{\n\t\tName: \"HuggingFace Token\",\n\t\tPattern: regexp.MustCompile(`hf_[a-zA-Z0-9]{34}`),\n\t\tSeverity: \"High\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://huggingface.co/api/whoami-v2\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"AWS Access Key\",\n\t\tPattern: regexp.MustCompile(`AKIA[0-9A-Z]{16}`),\n\t\tSeverity: \"Info\", // Downgraded to Info (Unvalidated)\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Google API Key\",\n\t\tPattern: regexp.MustCompile(`AIza[0-9A-Za-z\\-_]{35}`),\n\t\tSeverity: \"Info\",\n\t\t// Validation to distinguish Junk vs Real Key\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://www.googleapis.com/language/translate/v2?key=%s&q=hello&target=es\",\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Stripe Live Key\",\n\t\tPattern: regexp.MustCompile(`sk_live_[0-9a-zA-Z]{24,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.stripe.com/v1/customers\",\n\t\t\tBasicAuthUser: true,\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Slack Bot Token\",\n\t\tPattern: regexp.MustCompile(`xoxb-[0-9]{11,}-[0-9]{11,}-[0-9a-zA-Z]{24}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://slack.com/api/auth.test\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\", \"Content-Type\": \"application/json\"},\n\t\t\tExpectedCode: 200,\n\t\t\tExpectedContent: \"\\\"ok\\\":true\",\n\t\t},\n\t},\n\t{\n\t\tName: \"GitHub PAT\",\n\t\tPattern: regexp.MustCompile(`ghp_[0-9a-zA-Z]{36}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.github.com/user\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"token %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Heroku API Key\",\n\t\t// Stricter Regex: Must be preceded by 'heroku'\n\t\tPattern: regexp.MustCompile(`(?i)(?:heroku).{0,20}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})`),\n\t\tSeverity: \"Medium\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://api.heroku.com/apps\",\n\t\t\tHeaders: map[string]string{\"Accept\": \"application/vnd.heroku+json; version=3\", \"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Private Key\",\n\t\tPattern: regexp.MustCompile(`(?s)-----BEGIN (?:RSA |EC |PGP )?PRIVATE KEY-----\\s*[A-Za-z0-9+/=\\r\\n]{64,}\\s*-----END (?:RSA |EC |PGP )?PRIVATE KEY-----`),\n\t\tSeverity: \"High\", // High (Unvalidated Credential)\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"CodeIgniter DB Config\",\n\t\tPattern: regexp.MustCompile(`'password'\\s*=>\\s*'[^']+'`),\n\t\tSeverity: \"Medium\", // Default to Medium (Potential Config Leak)\n\t\tValidator: nil,\n\t\t// Note: The pattern itself ensures a password assignment is present.\n\t\t// However, to be strict as requested:\n\t\t// If it matches, it means we found `'password' => '...'`.\n\t\t// We will treat this as HIGH in the detection logic if valid, but base definition starts lower to be safe.\n\t},\n\t{\n\t\tName: \"SQL Dump (Plaintext Admin)\",\n\t\tPattern: regexp.MustCompile(`(?i)INSERT\\s+INTO.*(?:user|admin|account).*(?:VALUES|\\().*`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"SQL Dump (PII Data)\",\n\t\tPattern: regexp.MustCompile(`(?i)INSERT\\s+INTO.*(?:donatur|member|customer).*(?:VALUES|\\().*`),\n\t\tSeverity: \"High\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Database Backup File\",\n\t\tPattern: regexp.MustCompile(`(?i)\\b[a-z0-9._/-]{0,120}(?:backup|dump|db)[a-z0-9._/-]{0,120}\\.sql\\b`),\n\t\tSeverity: \"Info\",\n\t\tValidator: nil,\n\t},\n}\n\n// Additional helper for SQL content verification\nfunc isSQLContent(content string) bool {\n\tupper := strings.ToUpper(content)\n\t// Check for SQL header signatures or common statements\n\tif strings.Contains(upper, \"CREATE TABLE\") ||\n\t\tstrings.Contains(upper, \"INSERT INTO\") ||\n\t\tstrings.Contains(upper, \"MYSQLDUMP\") ||\n\t\tstrings.Contains(upper, \"-- MYSQL DUMP\") ||\n\t\tstrings.Contains(upper, \"SQLITE FORMAT\") ||\n\t\tstrings.Contains(upper, \"DROP TABLE\") ||\n\t\tstrings.Contains(upper, \"ALTER TABLE\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Scan checks the response body for secrets\nfunc Scan(url string, client *http.Client, onFound func(core.Finding)) {\n\tresp, err := client.Get(url)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, _ := io.ReadAll(resp.Body)\n\tbodyStr := string(body)\n\n\tDetectInContent(bodyStr, url, onFound)\n}\n\nfunc DetectInContent(content, sourceURL string, onFound func(core.Finding)) {\n\tfor _, p := range Patterns {\n\t\tmatches := p.Pattern.FindAllStringSubmatch(content, -1)\n\t\tseen := make(map[string]bool)\n\t\tfor _, m := range matches {\n\t\t\trawKey := m[0]\n\t\t\tif len(m) > 1 {\n\t\t\t\trawKey = m[len(m)-1]\n\t\t\t}\n\t\t\tcleanKey := strings.Trim(rawKey, ` \"'=`)\n\t\t\tif p.Name == \"Private Key\" && !isLikelyRealPrivateKeyBlock(cleanKey) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Ignore placeholders / documentation tokens\n\t\t\tupperKey := strings.ToUpper(cleanKey)\n\t\t\tif strings.Contains(upperKey, \"EXAMPLE\") ||\n\t\t\t\tstrings.Contains(upperKey, \"TEST\") ||\n\t\t\t\tstrings.Contains(upperKey, \"MOCK\") ||\n\t\t\t\tstrings.Contains(upperKey, \"YOUR_\") ||\n\t\t\t\tstrings.Contains(upperKey, \"USERNAME\") ||\n\t\t\t\tstrings.Contains(upperKey, \"PASSWORD\") ||\n\t\t\t\tstrings.Contains(upperKey, \"TOKEN\") ||\n\t\t\t\tstrings.Contains(upperKey, \"<\") ||\n\t\t\t\tstrings.Contains(upperKey, \">\") {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif seen[cleanKey] {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tseen[cleanKey] = true\n\n\t\t\tdetail := fmt.Sprintf(\"Found %s pattern.\", p.Name)\n\t\t\tseverity := p.Severity\n\n\t\t\t// Special handling for Database Backup File:\n\t\t\t// If we matched the pattern (likely in the URL/Link), we MUST verify the content actually looks like SQL.\n\t\t\t// This prevents Soft 404s on .sql files from being reported.\n\t\t\tif p.Name == \"Database Backup File\" {\n\t\t\t\t// 1. If the match is just the filename in a directory listing or link,\n\t\t\t\t// we usually scan the CONTENT of that file separately (via crawling).\n\t\t\t\t// But here `content` is the body of the page we are scanning.\n\t\t\t\t// If we are scanning `index.html` and it links to `backup.sql`, that's a finding (Info).\n\t\t\t\t// BUT if we are scanning `backup.sql` itself, we need to check content.\n\n\t\t\t\t// Case A: Pattern matched inside the body (Link Discovery) -> Info\n\t\t\t\t// Case B: We are scanning the file itself (sourceURL ends in .sql) -> Content Check\n\n\t\t\t\tif strings.HasSuffix(strings.ToLower(sourceURL), \".sql\") {\n\t\t\t\t\tif !isSQLContent(content) {\n\t\t\t\t\t\tcontinue // False Positive (Soft 404 or non-SQL content)\n\t\t\t\t\t}\n\t\t\t\t\tseverity = \"Critical\" // It's a verified dump!\n\t\t\t\t\tdetail = \"Verified SQL Dump file (Header/Structure confirmed).\"\n\t\t\t\t} else {\n\t\t\t\t\t// Do not report backup.sql mention inside docs/tutorial pages.\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Special handling for CodeIgniter Config\n\t\t\tif p.Name == \"CodeIgniter DB Config\" {\n\t\t\t\t// The regex matches 'password' => '...', so we know it has a password candidate.\n\t\t\t\t// However, check if it's empty or placeholder\n\t\t\t\tif strings.Contains(cleanKey, \"''\") || strings.Contains(cleanKey, \"'password'\") { // empty password\n\t\t\t\t\tseverity = \"Medium\"\n\t\t\t\t\tdetail = \"Found DB Config pattern (Empty/Placeholder password).\"\n\t\t\t\t} else {\n\t\t\t\t\tseverity = \"High\"\n\t\t\t\t\tdetail = \"Found DB Config with potential password assignment.\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif p.Validator != nil {\n\t\t\t\t// Special handling for localhost/test\n\t\t\t\tif strings.Contains(sourceURL, \"localhost\") && strings.Contains(p.Validator.URL, \"localhost\") {\n\t\t\t\t\t// Skip validation loop\n\t\t\t\t} else {\n\t\t\t\t\tstatus, msg := verifyKey(cleanKey, p.Validator)\n\n\t\t\t\t\tswitch status {\n\t\t\t\t\tcase \"Valid\":\n\t\t\t\t\t\tseverity = \"Critical\"\n\t\t\t\t\t\tdetail = fmt.Sprintf(\"VERIFIED %s: %s (%s)\", p.Name, cleanKey, msg)\n\t\t\t\t\tcase \"Restricted\":\n\t\t\t\t\t\tseverity = \"Info\"\n\t\t\t\t\t\tdetail = fmt.Sprintf(\"VERIFIED %s (Restricted): %s (%s)\", p.Name, cleanKey, msg)\n\t\t\t\t\tcase \"Invalid\":\n\t\t\t\t\t\t// Invalid key is informational noise, skip reporting.\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tcase \"Error\":\n\t\t\t\t\t\t// Network/API errors during validation create noisy false positives.\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if severity == \"High\" {\n\t\t\t\tdetail += \" Validity not verified. No permission testing performed.\"\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[+] SECRET MATCH: %s [%s]\\n\", p.Name, severity)\n\t\t\tonFound(core.Finding{\n\t\t\t\tType: \"Secret Leak\",\n\t\t\t\tTarget: sourceURL,\n\t\t\t\tDetail: detail,\n\t\t\t\tSeverity: severity,\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc isLikelyRealPrivateKeyBlock(block string) bool {\n\tif strings.Contains(block, `replace(\"-----BEGIN PRIVATE KEY-----\"`) ||\n\t\tstrings.Contains(block, `replace(\"-----END PRIVATE KEY-----\"`) ||\n\t\tstrings.Contains(block, `REPLACE(\"-----BEGIN PRIVATE KEY-----\"`) ||\n\t\tstrings.Contains(block, `REPLACE(\"-----END PRIVATE KEY-----\"`) {\n\t\treturn false\n\t}\n\n\tre := regexp.MustCompile(`(?s)-----BEGIN (?:RSA |EC |PGP )?PRIVATE KEY-----\\s*([A-Za-z0-9+/=\\r\\n]+)\\s*-----END (?:RSA |EC |PGP )?PRIVATE KEY-----`)\n\tm := re.FindStringSubmatch(block)\n\tif len(m) < 2 {\n\t\treturn false\n\t}\n\n\tpayload := strings.TrimSpace(m[1])\n\tif payload == \"\" {\n\t\treturn false\n\t}\n\tif strings.ContainsAny(payload, `\"'\\{\\}\\[\\],;`) {\n\t\treturn false\n\t}\n\n\tcompact := strings.ReplaceAll(strings.ReplaceAll(payload, \"\\n\", \"\"), \"\\r\", \"\")\n\tif len(compact) < 128 {\n\t\treturn false\n\t}\n\n\treturn strings.Count(payload, \"\\n\") >= 2\n}\n\n// status: Valid, Restricted, Invalid, Error\nfunc verifyKey(key string, v *Validator) (string, string) {\n\tclient := &http.Client{Timeout: 5 * time.Second}\n\n\ttargetURL := v.URL\n\tif strings.Contains(targetURL, \"%s\") {\n\t\ttargetURL = fmt.Sprintf(targetURL, key)\n\t}\n\n\treq, err := http.NewRequest(v.Method, targetURL, nil)\n\tif err != nil {\n\t\treturn \"Error\", err.Error()\n\t}\n\n\tfor k, val := range v.Headers {\n\t\tif strings.Contains(val, \"%s\") {\n\t\t\treq.Header.Set(k, fmt.Sprintf(val, key))\n\t\t} else {\n\t\t\treq.Header.Set(k, val)\n\t\t}\n\t}\n\n\tif v.BasicAuthUser {\n\t\treq.SetBasicAuth(key, \"\")\n\t}\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"Error\", err.Error()\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, _ := io.ReadAll(resp.Body)\n\tbodyStr := string(body)\n\n\t// 1. Check Success\n\tif resp.StatusCode == v.ExpectedCode {\n\t\tif v.ExpectedContent != \"\" {\n\t\t\tif !strings.Contains(bodyStr, v.ExpectedContent) {\n\t\t\t\treturn \"Invalid\", \"Content mismatch\"\n\t\t\t}\n\t\t}\n\n\t\t// Additional Anti-FP: Check for Google/Standard denial keywords in body\n\t\tif strings.Contains(bodyStr, \"REQUEST_DENIED\") || strings.Contains(bodyStr, \"API_KEY_INVALID\") || strings.Contains(bodyStr, \"key is invalid\") {\n\t\t\treturn \"Invalid\", \"Denied by Provider\"\n\t\t}\n\n\t\treturn \"Valid\", \"Active\"\n\t}\n\n\t// 2. Check Restricted/Invalid\n\tif resp.StatusCode == 403 || resp.StatusCode == 401 {\n\t\treturn \"Restricted\", \"Unauthorized (Invalid or Restricted)\"\n\t}\n\n\t// 3. Check Invalid (400, 401, 404 depending on API)\n\t// For most APIs, 401 = Invalid Key. 400 = Bad Request (Could be valid key but missing params).\n\t// Anthropic exception: 400 is \"Valid Key but bad request\".\n\t// Google exception: 400 is \"Bad Request\" (Key might be invalid OR valid).\n\t// To be safe: If code is 4xx/5xx and NOT expected, assume Invalid OR Restricted.\n\n\t// Refined Logic:\n\t// If 400 -> \"Invalid\" for Google (usually INVALID_ARGUMENT or API_KEY_INVALID)\n\t// If 401 -> \"Invalid\"\n\t// If 5xx -> \"Error\"\n\n\tif resp.StatusCode == 401 {\n\t\treturn \"Invalid\", \"401 Unauthorized\"\n\t}\n\n\tif resp.StatusCode >= 500 {\n\t\treturn \"Error\", fmt.Sprintf(\"Server Error %d\", resp.StatusCode)\n\t}\n\n\treturn \"Invalid\", fmt.Sprintf(\"Status %d\", resp.StatusCode)\n}\n"}, "e151dc51674c9b92729885bc665b57fccab9038b": {"t": 1785759601.897345, "n": 11689, "h": "1b5d3c65eb18bcdf", "c": "package secrets\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/yupiyy/bhyakugan/internal/core\"\n)\n\n// Validator defines how to check if a key is live\ntype Validator struct {\n\tMethod string\n\tURL string // Use %s placeholder for the key\n\tHeaders map[string]string\n\tExpectedCode int\n\tExpectedContent string // Substring to look for in success response\n\tBasicAuthUser bool // If true, use key as username in Basic Auth\n}\n\ntype SecretPattern struct {\n\tName string\n\tPattern *regexp.Regexp\n\tSeverity string\n\tValidator *Validator \n}\n\nvar Patterns = []SecretPattern{\n\t{\n\t\tName: \"OpenAI API Key\",\n\t\tPattern: regexp.MustCompile(`sk-[a-zA-Z0-9]{48}|sk-proj-[a-zA-Z0-9-_]{48,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.openai.com/v1/models\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Anthropic API Key\",\n\t\tPattern: regexp.MustCompile(`sk-ant-sid01-[a-zA-Z0-9-_]{93,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://api.anthropic.com/v1/messages\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"x-api-key\": \"%s\",\n\t\t\t\t\"anthropic-version\": \"2023-06-01\",\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t},\n\t\t\tExpectedCode: 400, // 400 = Key valid but request bad. 401 = Invalid.\n\t\t},\n\t},\n\t{\n\t\tName: \"HuggingFace Token\",\n\t\tPattern: regexp.MustCompile(`hf_[a-zA-Z0-9]{34}`),\n\t\tSeverity: \"High\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://huggingface.co/api/whoami-v2\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"AWS Access Key\",\n\t\tPattern: regexp.MustCompile(`AKIA[0-9A-Z]{16}`),\n\t\tSeverity: \"Info\", // Downgraded to Info (Unvalidated)\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Google API Key\",\n\t\tPattern: regexp.MustCompile(`AIza[0-9A-Za-z\\-_]{35}`),\n\t\tSeverity: \"Info\",\n\t\t// Validation to distinguish Junk vs Real Key\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://www.googleapis.com/language/translate/v2?key=%s&q=hello&target=es\", \n\t\t\tExpectedCode: 200, \n\t\t},\n\t},\n\t{\n\t\tName: \"Stripe Live Key\",\n\t\tPattern: regexp.MustCompile(`sk_live_[0-9a-zA-Z]{24,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.stripe.com/v1/customers\",\n\t\t\tBasicAuthUser: true,\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Slack Bot Token\",\n\t\tPattern: regexp.MustCompile(`xoxb-[0-9]{11,}-[0-9]{11,}-[0-9a-zA-Z]{24}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://slack.com/api/auth.test\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\", \"Content-Type\": \"application/json\"},\n\t\t\tExpectedCode: 200,\n\t\t\tExpectedContent: \"\\\"ok\\\":true\",\n\t\t},\n\t},\n\t{\n\t\tName: \"GitHub PAT\",\n\t\tPattern: regexp.MustCompile(`ghp_[0-9a-zA-Z]{36}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.github.com/user\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"token %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Heroku API Key\",\n\t\t// Stricter Regex: Must be preceded by 'heroku'\n\t\tPattern: regexp.MustCompile(`(?i)(?:heroku).{0,20}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})`), \n\t\tSeverity: \"Medium\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://api.heroku.com/apps\",\n\t\t\tHeaders: map[string]string{\"Accept\": \"application/vnd.heroku+json; version=3\", \"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Private Key\",\n\t\tPattern: regexp.MustCompile(`(?s)-----BEGIN (?:RSA |EC |PGP )?PRIVATE KEY-----.*?-----END (?:RSA |EC |PGP )?PRIVATE KEY-----`),\n\t\tSeverity: \"High\", // High (Unvalidated Credential)\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"CodeIgniter DB Config\",\n\t\tPattern: regexp.MustCompile(`'password'\\s*=>\\s*'[^']+'`),\n\t\tSeverity: \"Medium\", // Default to Medium (Potential Config Leak)\n\t\tValidator: nil, \n\t\t// Note: The pattern itself ensures a password assignment is present.\n\t\t// However, to be strict as requested:\n\t\t// If it matches, it means we found `'password' => '...'`.\n\t\t// We will treat this as HIGH in the detection logic if valid, but base definition starts lower to be safe.\n\t},\n\t{\n\t\tName: \"SQL Dump (Plaintext Admin)\",\n\t\tPattern: regexp.MustCompile(`(?i)INSERT\\s+INTO.*(?:user|admin|account).*(?:VALUES|\\().*`), \n\t\tSeverity: \"Critical\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"SQL Dump (PII Data)\",\n\t\tPattern: regexp.MustCompile(`(?i)INSERT\\s+INTO.*(?:donatur|member|customer).*(?:VALUES|\\().*`),\n\t\tSeverity: \"High\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Database Backup File\",\n\t\tPattern: regexp.MustCompile(`(?i)\\b[a-z0-9._/-]{0,120}(?:backup|dump|db)[a-z0-9._/-]{0,120}\\.sql\\b`),\n\t\tSeverity: \"Info\", \n\t\tValidator: nil,\n\t},\n}\n\n// Additional helper for SQL content verification\nfunc isSQLContent(content string) bool {\n\tupper := strings.ToUpper(content)\n\t// Check for SQL header signatures or common statements\n\tif strings.Contains(upper, \"CREATE TABLE\") || \n\t strings.Contains(upper, \"INSERT INTO\") || \n\t strings.Contains(upper, \"MYSQLDUMP\") || \n\t strings.Contains(upper, \"-- MYSQL DUMP\") ||\n\t strings.Contains(upper, \"SQLITE FORMAT\") ||\n\t strings.Contains(upper, \"DROP TABLE\") ||\n\t strings.Contains(upper, \"ALTER TABLE\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Scan checks the response body for secrets\nfunc Scan(url string, client *http.Client, onFound func(core.Finding)) {\n\tresp, err := client.Get(url)\n\tif err != nil { return }\n\tdefer resp.Body.Close()\n\n\tbody, _ := io.ReadAll(resp.Body)\n\tbodyStr := string(body)\n\n\tDetectInContent(bodyStr, url, onFound)\n}\n\nfunc DetectInContent(content, sourceURL string, onFound func(core.Finding)) {\n\tfor _, p := range Patterns {\n\t\tmatches := p.Pattern.FindAllStringSubmatch(content, -1)\n\t\tseen := make(map[string]bool)\n\t\tfor _, m := range matches {\n\t\t\trawKey := m[0]\n\t\t\tif len(m) > 1 {\n\t\t\t\trawKey = m[len(m)-1]\n\t\t\t}\n\t\t\tcleanKey := strings.Trim(rawKey, ` \"'=`)\n\t\t\t\n\t\t\t// Ignore placeholders / documentation tokens\n\t\t\tupperKey := strings.ToUpper(cleanKey)\n\t\t\tif strings.Contains(upperKey, \"EXAMPLE\") ||\n\t\t\t\tstrings.Contains(upperKey, \"TEST\") ||\n\t\t\t\tstrings.Contains(upperKey, \"MOCK\") ||\n\t\t\t\tstrings.Contains(upperKey, \"YOUR_\") ||\n\t\t\t\tstrings.Contains(upperKey, \"USERNAME\") ||\n\t\t\t\tstrings.Contains(upperKey, \"PASSWORD\") ||\n\t\t\t\tstrings.Contains(upperKey, \"TOKEN\") ||\n\t\t\t\tstrings.Contains(upperKey, \"<\") ||\n\t\t\t\tstrings.Contains(upperKey, \">\") {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif seen[cleanKey] { continue }\n\t\t\tseen[cleanKey] = true\n\n\t\t\tdetail := fmt.Sprintf(\"Found %s pattern.\", p.Name)\n\t\t\tseverity := p.Severity\n\n\t\t\t// Special handling for Database Backup File:\n\t\t\t// If we matched the pattern (likely in the URL/Link), we MUST verify the content actually looks like SQL.\n\t\t\t// This prevents Soft 404s on .sql files from being reported.\n\t\t\tif p.Name == \"Database Backup File\" {\n\t\t\t\t// 1. If the match is just the filename in a directory listing or link, \n\t\t\t\t// we usually scan the CONTENT of that file separately (via crawling).\n\t\t\t\t// But here `content` is the body of the page we are scanning.\n\t\t\t\t// If we are scanning `index.html` and it links to `backup.sql`, that's a finding (Info).\n\t\t\t\t// BUT if we are scanning `backup.sql` itself, we need to check content.\n\t\t\t\t\n\t\t\t\t// Case A: Pattern matched inside the body (Link Discovery) -> Info\n\t\t\t\t// Case B: We are scanning the file itself (sourceURL ends in .sql) -> Content Check\n\t\t\t\t\n\t\t\t\tif strings.HasSuffix(strings.ToLower(sourceURL), \".sql\") {\n\t\t\t\t\tif !isSQLContent(content) {\n\t\t\t\t\t\tcontinue // False Positive (Soft 404 or non-SQL content)\n\t\t\t\t\t}\n\t\t\t\t\tseverity = \"Critical\" // It's a verified dump!\n\t\t\t\t\tdetail = \"Verified SQL Dump file (Header/Structure confirmed).\"\n\t\t\t\t} else {\n\t\t\t\t\t// Do not report backup.sql mention inside docs/tutorial pages.\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Special handling for CodeIgniter Config\n\t\t\tif p.Name == \"CodeIgniter DB Config\" {\n\t\t\t\t// The regex matches 'password' => '...', so we know it has a password candidate.\n\t\t\t\t// However, check if it's empty or placeholder\n\t\t\t\tif strings.Contains(cleanKey, \"''\") || strings.Contains(cleanKey, \"'password'\") { // empty password\n\t\t\t\t\tseverity = \"Medium\"\n\t\t\t\t\tdetail = \"Found DB Config pattern (Empty/Placeholder password).\"\n\t\t\t\t} else {\n\t\t\t\t\tseverity = \"High\"\n\t\t\t\t\tdetail = \"Found DB Config with potential password assignment.\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif p.Validator != nil {\n\t\t\t\t// Special handling for localhost/test\n\t\t\t\tif strings.Contains(sourceURL, \"localhost\") && strings.Contains(p.Validator.URL, \"localhost\") {\n\t\t\t\t\t// Skip validation loop\n\t\t\t\t} else {\n\t\t\t\t\tstatus, msg := verifyKey(cleanKey, p.Validator)\n\t\t\t\t\t\n\t\t\t\t\tswitch status {\n\t\t\t\t\tcase \"Valid\":\n\t\t\t\t\t\tseverity = \"Critical\"\n\t\t\t\t\t\tdetail = fmt.Sprintf(\"VERIFIED %s: %s (%s)\", p.Name, cleanKey, msg)\n\t\t\t\t\tcase \"Restricted\":\n\t\t\t\t\t\tseverity = \"Info\"\n\t\t\t\t\t\tdetail = fmt.Sprintf(\"VERIFIED %s (Restricted): %s (%s)\", p.Name, cleanKey, msg)\n\t\t\t\t\tcase \"Invalid\":\n\t\t\t\t\t\t// Invalid key is informational noise, skip reporting.\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tcase \"Error\":\n\t\t\t\t\t\t// Network/API errors during validation create noisy false positives.\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if severity == \"High\" {\n\t\t\t\tdetail += \" Validity not verified. No permission testing performed.\"\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[+] SECRET MATCH: %s [%s]\\n\", p.Name, severity)\n\t\t\tonFound(core.Finding{\n\t\t\t\tType: \"Secret Leak\",\n\t\t\t\tTarget: sourceURL,\n\t\t\t\tDetail: detail,\n\t\t\t\tSeverity: severity,\n\t\t\t})\n\t\t}\n\t}\n}\n\n// status: Valid, Restricted, Invalid, Error\nfunc verifyKey(key string, v *Validator) (string, string) {\n\tclient := &http.Client{Timeout: 5 * time.Second}\n\t\ntargetURL := v.URL\n\tif strings.Contains(targetURL, \"%s\") {\n\t\ttargetURL = fmt.Sprintf(targetURL, key)\n\t}\n\n\treq, err := http.NewRequest(v.Method, targetURL, nil)\n\tif err != nil { return \"Error\", err.Error() }\n\n\tfor k, val := range v.Headers {\n\t\tif strings.Contains(val, \"%s\") {\n\t\t\treq.Header.Set(k, fmt.Sprintf(val, key))\n\t\t} else {\n\t\t\treq.Header.Set(k, val)\n\t\t}\n\t}\n\n\tif v.BasicAuthUser {\n\t\treq.SetBasicAuth(key, \"\")\n\t}\n\n\tresp, err := client.Do(req)\n\tif err != nil { return \"Error\", err.Error() }\n\tdefer resp.Body.Close()\n\n\tbody, _ := io.ReadAll(resp.Body)\n\tbodyStr := string(body)\n\n\t// 1. Check Success\n\tif resp.StatusCode == v.ExpectedCode {\n\t\tif v.ExpectedContent != \"\" {\n\t\t\tif !strings.Contains(bodyStr, v.ExpectedContent) {\n\t\t\t\treturn \"Invalid\", \"Content mismatch\"\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Additional Anti-FP: Check for Google/Standard denial keywords in body\n\t\tif strings.Contains(bodyStr, \"REQUEST_DENIED\") || strings.Contains(bodyStr, \"API_KEY_INVALID\") || strings.Contains(bodyStr, \"key is invalid\") {\n\t\t\treturn \"Invalid\", \"Denied by Provider\"\n\t\t}\n\n\t\treturn \"Valid\", \"Active\"\n\t}\n\n\t// 2. Check Restricted/Invalid\n\tif resp.StatusCode == 403 || resp.StatusCode == 401 {\n\t\treturn \"Restricted\", \"Unauthorized (Invalid or Restricted)\"\n\t}\n\n\t// 3. Check Invalid (400, 401, 404 depending on API)\n\t// For most APIs, 401 = Invalid Key. 400 = Bad Request (Could be valid key but missing params).\n\t// Anthropic exception: 400 is \"Valid Key but bad request\".\n\t// Google exception: 400 is \"Bad Request\" (Key might be invalid OR valid). \n\t// To be safe: If code is 4xx/5xx and NOT expected, assume Invalid OR Restricted.\n\t\n\t// Refined Logic:\n\t// If 400 -> \"Invalid\" for Google (usually INVALID_ARGUMENT or API_KEY_INVALID)\n\t// If 401 -> \"Invalid\"\n\t// If 5xx -> \"Error\"\n\t\n\tif resp.StatusCode == 401 {\n\t\treturn \"Invalid\", \"401 Unauthorized\"\n\t}\n\t\n\tif resp.StatusCode >= 500 {\n\t\treturn \"Error\", fmt.Sprintf(\"Server Error %d\", resp.StatusCode)\n\t}\n\n\treturn \"Invalid\", fmt.Sprintf(\"Status %d\", resp.StatusCode)\n}\n"}, "66330461dc48576b9b82a02895df7ed21a3e2c6c": {"t": 1785759605.9027689, "n": 11503, "h": "e6be0af9908c9189", "c": "package secrets\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/yupiyy/bhyakugan/internal/core\"\n)\n\n// Validator defines how to check if a key is live\ntype Validator struct {\n\tMethod string\n\tURL string // Use %s placeholder for the key\n\tHeaders map[string]string\n\tExpectedCode int\n\tExpectedContent string // Substring to look for in success response\n\tBasicAuthUser bool // If true, use key as username in Basic Auth\n}\n\ntype SecretPattern struct {\n\tName string\n\tPattern *regexp.Regexp\n\tSeverity string\n\tValidator *Validator \n}\n\nvar Patterns = []SecretPattern{\n\t{\n\t\tName: \"OpenAI API Key\",\n\t\tPattern: regexp.MustCompile(`sk-[a-zA-Z0-9]{48}|sk-proj-[a-zA-Z0-9-_]{48,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.openai.com/v1/models\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Anthropic API Key\",\n\t\tPattern: regexp.MustCompile(`sk-ant-sid01-[a-zA-Z0-9-_]{93,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://api.anthropic.com/v1/messages\",\n\t\t\tHeaders: map[string]string{\n\t\t\t\t\"x-api-key\": \"%s\",\n\t\t\t\t\"anthropic-version\": \"2023-06-01\",\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t},\n\t\t\tExpectedCode: 400, // 400 = Key valid but request bad. 401 = Invalid.\n\t\t},\n\t},\n\t{\n\t\tName: \"HuggingFace Token\",\n\t\tPattern: regexp.MustCompile(`hf_[a-zA-Z0-9]{34}`),\n\t\tSeverity: \"High\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://huggingface.co/api/whoami-v2\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"AWS Access Key\",\n\t\tPattern: regexp.MustCompile(`AKIA[0-9A-Z]{16}`),\n\t\tSeverity: \"Info\", // Downgraded to Info (Unvalidated)\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Google API Key\",\n\t\tPattern: regexp.MustCompile(`AIza[0-9A-Za-z\\-_]{35}`),\n\t\tSeverity: \"Info\",\n\t\t// Validation to distinguish Junk vs Real Key\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://www.googleapis.com/language/translate/v2?key=%s&q=hello&target=es\", \n\t\t\tExpectedCode: 200, \n\t\t},\n\t},\n\t{\n\t\tName: \"Stripe Live Key\",\n\t\tPattern: regexp.MustCompile(`sk_live_[0-9a-zA-Z]{24,}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.stripe.com/v1/customers\",\n\t\t\tBasicAuthUser: true,\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Slack Bot Token\",\n\t\tPattern: regexp.MustCompile(`xoxb-[0-9]{11,}-[0-9]{11,}-[0-9a-zA-Z]{24}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://slack.com/api/auth.test\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"Bearer %s\", \"Content-Type\": \"application/json\"},\n\t\t\tExpectedCode: 200,\n\t\t\tExpectedContent: \"\\\"ok\\\":true\",\n\t\t},\n\t},\n\t{\n\t\tName: \"GitHub PAT\",\n\t\tPattern: regexp.MustCompile(`ghp_[0-9a-zA-Z]{36}`),\n\t\tSeverity: \"Critical\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"GET\",\n\t\t\tURL: \"https://api.github.com/user\",\n\t\t\tHeaders: map[string]string{\"Authorization\": \"token %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Heroku API Key\",\n\t\t// Stricter Regex: Must be preceded by 'heroku'\n\t\tPattern: regexp.MustCompile(`(?i)(?:heroku).{0,20}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})`), \n\t\tSeverity: \"Medium\",\n\t\tValidator: &Validator{\n\t\t\tMethod: \"POST\",\n\t\t\tURL: \"https://api.heroku.com/apps\",\n\t\t\tHeaders: map[string]string{\"Accept\": \"application/vnd.heroku+json; version=3\", \"Authorization\": \"Bearer %s\"},\n\t\t\tExpectedCode: 200,\n\t\t},\n\t},\n\t{\n\t\tName: \"Private Key\",\n\t\tPattern: regexp.MustCompile(`(?s)-----BEGIN (?:RSA |EC |PGP )?PRIVATE KEY-----.*?-----END (?:RSA |EC |PGP )?PRIVATE KEY-----`),\n\t\tSeverity: \"High\", // High (Unvalidated Credential)\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"CodeIgniter DB Config\",\n\t\tPattern: regexp.MustCompile(`'password'\\s*=>\\s*'[^']+'`),\n\t\tSeverity: \"Medium\", // Default to Medium (Potential Config Leak)\n\t\tValidator: nil, \n\t\t// Note: The pattern itself ensures a password assignment is present.\n\t\t// However, to be strict as requested:\n\t\t// If it matches, it means we found `'password' => '...'`.\n\t\t// We will treat this as HIGH in the detection logic if valid, but base definition starts lower to be safe.\n\t},\n\t{\n\t\tName: \"SQL Dump (Plaintext Admin)\",\n\t\tPattern: regexp.MustCompile(`(?i)INSERT\\s+INTO.*(?:user|admin|account).*(?:VALUES|\\().*`), \n\t\tSeverity: \"Critical\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"SQL Dump (PII Data)\",\n\t\tPattern: regexp.MustCompile(`(?i)INSERT\\s+INTO.*(?:donatur|member|customer).*(?:VALUES|\\().*`),\n\t\tSeverity: \"High\",\n\t\tValidator: nil,\n\t},\n\t{\n\t\tName: \"Database Backup File\",\n\t\tPattern: regexp.MustCompile(`(?i)(?:db_|backup|dump).*\\.sql`),\n\t\tSeverity: \"Info\", \n\t\tValidator: nil,\n\t},\n}\n\n// Additional helper for SQL content verification\nfunc isSQLContent(content string) bool {\n\tupper := strings.ToUpper(content)\n\t// Check for SQL header signatures or common statements\n\tif strings.Contains(upper, \"CREATE TABLE\") || \n\t strings.Contains(upper, \"INSERT INTO\") || \n\t strings.Contains(upper, \"MYSQLDUMP\") || \n\t strings.Contains(upper, \"-- MYSQL DUMP\") ||\n\t strings.Contains(upper, \"SQLITE FORMAT\") ||\n\t strings.Contains(upper, \"DROP TABLE\") ||\n\t strings.Contains(upper, \"ALTER TABLE\") {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Scan checks the response body for secrets\nfunc Scan(url string, client *http.Client, onFound func(core.Finding)) {\n\tresp, err := client.Get(url)\n\tif err != nil { return }\n\tdefer resp.Body.Close()\n\n\tbody, _ := io.ReadAll(resp.Body)\n\tbodyStr := string(body)\n\n\tDetectInContent(bodyStr, url, onFound)\n}\n\nfunc DetectInContent(content, sourceURL string, onFound func(core.Finding)) {\n\tfor _, p := range Patterns {\n\t\tmatches := p.Pattern.FindAllStringSubmatch(content, -1)\n\t\tseen := make(map[string]bool)\n\t\tfor _, m := range matches {\n\t\t\trawKey := m[0]\n\t\t\tif len(m) > 1 {\n\t\t\t\trawKey = m[len(m)-1]\n\t\t\t}\n\t\t\tcleanKey := strings.Trim(rawKey, ` \"'=`)\n\t\t\t\n\t\t\t// Ignore placeholders\n\t\t\tupperKey := strings.ToUpper(cleanKey)\n\t\t\tif strings.Contains(upperKey, \"EXAMPLE\") || strings.Contains(upperKey, \"TEST\") || strings.Contains(upperKey, \"MOCK\") {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif seen[cleanKey] { continue }\n\t\t\tseen[cleanKey] = true\n\n\t\t\tdetail := fmt.Sprintf(\"Found %s pattern.\", p.Name)\n\t\t\tseverity := p.Severity\n\n\t\t\t// Special handling for Database Backup File:\n\t\t\t// If we matched the pattern (likely in the URL/Link), we MUST verify the content actually looks like SQL.\n\t\t\t// This prevents Soft 404s on .sql files from being reported.\n\t\t\tif p.Name == \"Database Backup File\" {\n\t\t\t\t// 1. If the match is just the filename in a directory listing or link, \n\t\t\t\t// we usually scan the CONTENT of that file separately (via crawling).\n\t\t\t\t// But here `content` is the body of the page we are scanning.\n\t\t\t\t// If we are scanning `index.html` and it links to `backup.sql`, that's a finding (Info).\n\t\t\t\t// BUT if we are scanning `backup.sql` itself, we need to check content.\n\t\t\t\t\n\t\t\t\t// Case A: Pattern matched inside the body (Link Discovery) -> Info\n\t\t\t\t// Case B: We are scanning the file itself (sourceURL ends in .sql) -> Content Check\n\t\t\t\t\n\t\t\t\tif strings.HasSuffix(sourceURL, \".sql\") {\n\t\t\t\t\tif !isSQLContent(content) {\n\t\t\t\t\t\tcontinue // False Positive (Soft 404 or non-SQL content)\n\t\t\t\t\t}\n\t\t\t\t\tseverity = \"Critical\" // It's a verified dump!\n\t\t\t\t\tdetail = \"Verified SQL Dump file (Header/Structure confirmed).\"\n\t\t\t\t} else {\n\t\t\t\t\tseverity = \"Info\"\n\t\t\t\t\tdetail = fmt.Sprintf(\"Potential Database Backup File reference: %s\", cleanKey)\n\t\t\t\t}\n\t\t\t}\n\t\t\t\n\t\t\t// Special handling for CodeIgniter Config\n\t\t\tif p.Name == \"CodeIgniter DB Config\" {\n\t\t\t\t// The regex matches 'password' => '...', so we know it has a password candidate.\n\t\t\t\t// However, check if it's empty or placeholder\n\t\t\t\tif strings.Contains(cleanKey, \"''\") || strings.Contains(cleanKey, \"'password'\") { // empty password\n\t\t\t\t\tseverity = \"Medium\"\n\t\t\t\t\tdetail = \"Found DB Config pattern (Empty/Placeholder password).\"\n\t\t\t\t} else {\n\t\t\t\t\tseverity = \"High\"\n\t\t\t\t\tdetail = \"Found DB Config with potential password assignment.\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif p.Validator != nil {\n\t\t\t\t// Special handling for localhost/test\n\t\t\t\tif strings.Contains(sourceURL, \"localhost\") && strings.Contains(p.Validator.URL, \"localhost\") {\n\t\t\t\t\t// Skip validation loop\n\t\t\t\t} else {\n\t\t\t\t\tstatus, msg := verifyKey(cleanKey, p.Validator)\n\t\t\t\t\t\n\t\t\t\t\tswitch status {\n\t\t\t\t\tcase \"Valid\":\n\t\t\t\t\t\tseverity = \"Critical\"\n\t\t\t\t\t\tdetail = fmt.Sprintf(\"VERIFIED %s: %s (%s)\", p.Name, cleanKey, msg)\n\t\t\t\t\tcase \"Restricted\":\n\t\t\t\t\t\tseverity = \"Info\"\n\t\t\t\t\t\tdetail = fmt.Sprintf(\"VERIFIED %s (Restricted): %s (%s)\", p.Name, cleanKey, msg)\n\t\t\t\t\tcase \"Invalid\":\n\t\t\t\t\t\tseverity = \"Info\"\n\t\t\t\t\t\tdetail = fmt.Sprintf(\"Invalid %s found (Verification Failed): %s\", p.Name, msg)\n\t\t\t\t\tcase \"Error\":\n\t\t\t\t\t\t// Report as Low/Info if verification failed due to network\n\t\t\t\t\t\tseverity = \"Low\"\n\t\t\t\t\t\tdetail = fmt.Sprintf(\"Potential %s (Verification Failed): %s. Err: %s\", p.Name, cleanKey, msg)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if severity == \"High\" {\n\t\t\t\tdetail += \" Validity not verified. No permission testing performed.\"\n\t\t\t}\n\n\t\t\tfmt.Printf(\"[+] SECRET MATCH: %s [%s]\\n\", p.Name, severity)\n\t\t\tonFound(core.Finding{\n\t\t\t\tType: \"Secret Leak\",\n\t\t\t\tTarget: sourceURL,\n\t\t\t\tDetail: detail,\n\t\t\t\tSeverity: severity,\n\t\t\t})\n\t\t}\n\t}\n}\n\n// status: Valid, Restricted, Invalid, Error\nfunc verifyKey(key string, v *Validator) (string, string) {\n\tclient := &http.Client{Timeout: 5 * time.Second}\n\t\ntargetURL := v.URL\n\tif strings.Contains(targetURL, \"%s\") {\n\t\ttargetURL = fmt.Sprintf(targetURL, key)\n\t}\n\n\treq, err := http.NewRequest(v.Method, targetURL, nil)\n\tif err != nil { return \"Error\", err.Error() }\n\n\tfor k, val := range v.Headers {\n\t\tif strings.Contains(val, \"%s\") {\n\t\t\treq.Header.Set(k, fmt.Sprintf(val, key))\n\t\t} else {\n\t\t\treq.Header.Set(k, val)\n\t\t}\n\t}\n\n\tif v.BasicAuthUser {\n\t\treq.SetBasicAuth(key, \"\")\n\t}\n\n\tresp, err := client.Do(req)\n\tif err != nil { return \"Error\", err.Error() }\n\tdefer resp.Body.Close()\n\n\tbody, _ := io.ReadAll(resp.Body)\n\tbodyStr := string(body)\n\n\t// 1. Check Success\n\tif resp.StatusCode == v.ExpectedCode {\n\t\tif v.ExpectedContent != \"\" {\n\t\t\tif !strings.Contains(bodyStr, v.ExpectedContent) {\n\t\t\t\treturn \"Invalid\", \"Content mismatch\"\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Additional Anti-FP: Check for Google/Standard denial keywords in body\n\t\tif strings.Contains(bodyStr, \"REQUEST_DENIED\") || strings.Contains(bodyStr, \"API_KEY_INVALID\") || strings.Contains(bodyStr, \"key is invalid\") {\n\t\t\treturn \"Invalid\", \"Denied by Provider\"\n\t\t}\n\n\t\treturn \"Valid\", \"Active\"\n\t}\n\n\t// 2. Check Restricted/Invalid\n\tif resp.StatusCode == 403 || resp.StatusCode == 401 {\n\t\treturn \"Restricted\", \"Unauthorized (Invalid or Restricted)\"\n\t}\n\n\t// 3. Check Invalid (400, 401, 404 depending on API)\n\t// For most APIs, 401 = Invalid Key. 400 = Bad Request (Could be valid key but missing params).\n\t// Anthropic exception: 400 is \"Valid Key but bad request\".\n\t// Google exception: 400 is \"Bad Request\" (Key might be invalid OR valid). \n\t// To be safe: If code is 4xx/5xx and NOT expected, assume Invalid OR Restricted.\n\t\n\t// Refined Logic:\n\t// If 400 -> \"Invalid\" for Google (usually INVALID_ARGUMENT or API_KEY_INVALID)\n\t// If 401 -> \"Invalid\"\n\t// If 5xx -> \"Error\"\n\t\n\tif resp.StatusCode == 401 {\n\t\treturn \"Invalid\", \"401 Unauthorized\"\n\t}\n\t\n\tif resp.StatusCode >= 500 {\n\t\treturn \"Error\", fmt.Sprintf(\"Server Error %d\", resp.StatusCode)\n\t}\n\n\treturn \"Invalid\", fmt.Sprintf(\"Status %d\", resp.StatusCode)\n}\n"}}}}