Download src/services/teamMemorySync/secretScanner.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 9.46 kB
-
https://huggingface.co/chenbhao/codev/resolve/main/src/services/teamMemorySync/secretScanner.ts
- Command line
-
hf download hf://chenbhao/codev/src/services/teamMemorySync/secretScanner.ts
-
curl -L -o secretScanner.ts https://huggingface.co/chenbhao/codev/resolve/main/src/services/teamMemorySync/secretScanner.ts
9.46 kB
| /** | |
| * Client-side secret scanner for team memory (PSR M22174). | |
| * | |
| * Scans content for credentials before upload so secrets never leave the | |
| * user's machine. Uses a curated subset of high-confidence rules from | |
| * gitleaks (https://github.com/gitleaks/gitleaks, MIT license) β only | |
| * rules with distinctive prefixes that have near-zero false-positive | |
| * rates are included. Generic keyword-context rules are omitted. | |
| * | |
| * Rule IDs and regexes sourced directly from the public gitleaks config: | |
| * https://github.com/gitleaks/gitleaks/blob/master/config/gitleaks.toml | |
| * | |
| * JS regex notes: | |
| * - gitleaks uses Go regex; inline (?i) and mode groups (?-i:...) are | |
| * not portable to JS. Affected rules are rewritten with explicit | |
| * character classes ([a-zA-Z0-9] instead of (?i)[a-z0-9]). | |
| * - Trailing boundary alternations like (?:[\x60'"\s;]|\\[nr]|$) from | |
| * Go regex are kept (JS $ matches end-of-string in default mode). | |
| */ | |
| import { capitalize } from '../../utils/stringUtils.js' | |
| type SecretRule = { | |
| /** Gitleaks rule ID (kebab-case), used in labels and analytics */ | |
| id: string | |
| /** Regex source, lazily compiled on first scan */ | |
| source: string | |
| /** Optional JS regex flags (most rules are case-sensitive by default) */ | |
| flags?: string | |
| } | |
| export type SecretMatch = { | |
| /** Gitleaks rule ID that matched (e.g., "github-pat", "aws-access-token") */ | |
| ruleId: string | |
| /** Human-readable label derived from the rule ID */ | |
| label: string | |
| } | |
| // βββ Curated rules ββββββββββββββββββββββββββββββββββββββββββββββ | |
| // High-confidence patterns from gitleaks with distinctive prefixes. | |
| // Ordered roughly by likelihood of appearing in dev-team content. | |
| // Anthropic API key prefix, assembled at runtime so the literal byte | |
| // sequence isn't present in the external bundle (excluded-strings check). | |
| // join() is not constant-folded by the minifier. | |
| const ANT_KEY_PFX = ['sk', 'ant', 'api'].join('-') | |
| const SECRET_RULES: SecretRule[] = [ | |
| // β Cloud providers β | |
| { | |
| id: 'aws-access-token', | |
| source: '\\b((?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16})\\b', | |
| }, | |
| { | |
| id: 'gcp-api-key', | |
| source: '\\b(AIza[\\w-]{35})(?:[\\x60\'"\\s;]|\\\\[nr]|$)', | |
| }, | |
| { | |
| id: 'azure-ad-client-secret', | |
| source: | |
| '(?:^|[\\\\\'"\\x60\\s>=:(,)])([a-zA-Z0-9_~.]{3}\\dQ~[a-zA-Z0-9_~.-]{31,34})(?:$|[\\\\\'"\\x60\\s<),])', | |
| }, | |
| { | |
| id: 'digitalocean-pat', | |
| source: '\\b(dop_v1_[a-f0-9]{64})(?:[\\x60\'"\\s;]|\\\\[nr]|$)', | |
| }, | |
| { | |
| id: 'digitalocean-access-token', | |
| source: '\\b(doo_v1_[a-f0-9]{64})(?:[\\x60\'"\\s;]|\\\\[nr]|$)', | |
| }, | |
| // β AI APIs β | |
| { | |
| id: 'anthropic-api-key', | |
| source: `\\b(${ANT_KEY_PFX}03-[a-zA-Z0-9_\\-]{93}AA)(?:[\\x60'"\\s;]|\\\\[nr]|$)`, | |
| }, | |
| { | |
| id: 'anthropic-admin-api-key', | |
| source: | |
| '\\b(sk-ant-admin01-[a-zA-Z0-9_\\-]{93}AA)(?:[\\x60\'"\\s;]|\\\\[nr]|$)', | |
| }, | |
| { | |
| id: 'openai-api-key', | |
| source: | |
| '\\b(sk-(?:proj|svcacct|admin)-(?:[A-Za-z0-9_-]{74}|[A-Za-z0-9_-]{58})T3BlbkFJ(?:[A-Za-z0-9_-]{74}|[A-Za-z0-9_-]{58})\\b|sk-[a-zA-Z0-9]{20}T3BlbkFJ[a-zA-Z0-9]{20})(?:[\\x60\'"\\s;]|\\\\[nr]|$)', | |
| }, | |
| { | |
| id: 'huggingface-access-token', | |
| // gitleaks: hf_(?i:[a-z]{34}) β JS: hf_[a-zA-Z]{34} | |
| source: '\\b(hf_[a-zA-Z]{34})(?:[\\x60\'"\\s;]|\\\\[nr]|$)', | |
| }, | |
| // β Version control β | |
| { | |
| id: 'github-pat', | |
| source: 'ghp_[0-9a-zA-Z]{36}', | |
| }, | |
| { | |
| id: 'github-fine-grained-pat', | |
| source: 'github_pat_\\w{82}', | |
| }, | |
| { | |
| id: 'github-app-token', | |
| source: '(?:ghu|ghs)_[0-9a-zA-Z]{36}', | |
| }, | |
| { | |
| id: 'github-oauth', | |
| source: 'gho_[0-9a-zA-Z]{36}', | |
| }, | |
| { | |
| id: 'github-refresh-token', | |
| source: 'ghr_[0-9a-zA-Z]{36}', | |
| }, | |
| { | |
| id: 'gitlab-pat', | |
| source: 'glpat-[\\w-]{20}', | |
| }, | |
| { | |
| id: 'gitlab-deploy-token', | |
| source: 'gldt-[0-9a-zA-Z_\\-]{20}', | |
| }, | |
| // β Communication β | |
| { | |
| id: 'slack-bot-token', | |
| source: 'xoxb-[0-9]{10,13}-[0-9]{10,13}[a-zA-Z0-9-]*', | |
| }, | |
| { | |
| id: 'slack-user-token', | |
| source: 'xox[pe](?:-[0-9]{10,13}){3}-[a-zA-Z0-9-]{28,34}', | |
| }, | |
| { | |
| id: 'slack-app-token', | |
| source: 'xapp-\\d-[A-Z0-9]+-\\d+-[a-z0-9]+', | |
| flags: 'i', | |
| }, | |
| { | |
| id: 'twilio-api-key', | |
| source: 'SK[0-9a-fA-F]{32}', | |
| }, | |
| { | |
| id: 'sendgrid-api-token', | |
| // gitleaks: SG\.(?i)[a-z0-9=_\-\.]{66} β JS: case-insensitive via flag | |
| source: '\\b(SG\\.[a-zA-Z0-9=_\\-.]{66})(?:[\\x60\'"\\s;]|\\\\[nr]|$)', | |
| }, | |
| // β Dev tooling β | |
| { | |
| id: 'npm-access-token', | |
| source: '\\b(npm_[a-zA-Z0-9]{36})(?:[\\x60\'"\\s;]|\\\\[nr]|$)', | |
| }, | |
| { | |
| id: 'pypi-upload-token', | |
| source: 'pypi-AgEIcHlwaS5vcmc[\\w-]{50,1000}', | |
| }, | |
| { | |
| id: 'databricks-api-token', | |
| source: '\\b(dapi[a-f0-9]{32}(?:-\\d)?)(?:[\\x60\'"\\s;]|\\\\[nr]|$)', | |
| }, | |
| { | |
| id: 'hashicorp-tf-api-token', | |
| // gitleaks: (?i)[a-z0-9]{14}\.(?-i:atlasv1)\.[a-z0-9\-_=]{60,70} | |
| // β JS: case-insensitive hex+alnum prefix, literal "atlasv1", case-insensitive suffix | |
| source: '[a-zA-Z0-9]{14}\\.atlasv1\\.[a-zA-Z0-9\\-_=]{60,70}', | |
| }, | |
| { | |
| id: 'pulumi-api-token', | |
| source: '\\b(pul-[a-f0-9]{40})(?:[\\x60\'"\\s;]|\\\\[nr]|$)', | |
| }, | |
| { | |
| id: 'postman-api-token', | |
| // gitleaks: PMAK-(?i)[a-f0-9]{24}\-[a-f0-9]{34} β JS: use [a-fA-F0-9] | |
| source: | |
| '\\b(PMAK-[a-fA-F0-9]{24}-[a-fA-F0-9]{34})(?:[\\x60\'"\\s;]|\\\\[nr]|$)', | |
| }, | |
| // β Observability β | |
| { | |
| id: 'grafana-api-key', | |
| source: | |
| '\\b(eyJrIjoi[A-Za-z0-9+/]{70,400}={0,3})(?:[\\x60\'"\\s;]|\\\\[nr]|$)', | |
| }, | |
| { | |
| id: 'grafana-cloud-api-token', | |
| source: '\\b(glc_[A-Za-z0-9+/]{32,400}={0,3})(?:[\\x60\'"\\s;]|\\\\[nr]|$)', | |
| }, | |
| { | |
| id: 'grafana-service-account-token', | |
| source: | |
| '\\b(glsa_[A-Za-z0-9]{32}_[A-Fa-f0-9]{8})(?:[\\x60\'"\\s;]|\\\\[nr]|$)', | |
| }, | |
| { | |
| id: 'sentry-user-token', | |
| source: '\\b(sntryu_[a-f0-9]{64})(?:[\\x60\'"\\s;]|\\\\[nr]|$)', | |
| }, | |
| { | |
| id: 'sentry-org-token', | |
| source: | |
| '\\bsntrys_eyJpYXQiO[a-zA-Z0-9+/]{10,200}(?:LCJyZWdpb25fdXJs|InJlZ2lvbl91cmwi|cmVnaW9uX3VybCI6)[a-zA-Z0-9+/]{10,200}={0,2}_[a-zA-Z0-9+/]{43}', | |
| }, | |
| // β Payment / commerce β | |
| { | |
| id: 'stripe-access-token', | |
| source: | |
| '\\b((?:sk|rk)_(?:test|live|prod)_[a-zA-Z0-9]{10,99})(?:[\\x60\'"\\s;]|\\\\[nr]|$)', | |
| }, | |
| { | |
| id: 'shopify-access-token', | |
| source: 'shpat_[a-fA-F0-9]{32}', | |
| }, | |
| { | |
| id: 'shopify-shared-secret', | |
| source: 'shpss_[a-fA-F0-9]{32}', | |
| }, | |
| // β Crypto β | |
| { | |
| id: 'private-key', | |
| source: | |
| '-----BEGIN[ A-Z0-9_-]{0,100}PRIVATE KEY(?: BLOCK)?-----[\\s\\S-]{64,}?-----END[ A-Z0-9_-]{0,100}PRIVATE KEY(?: BLOCK)?-----', | |
| flags: 'i', | |
| }, | |
| ] | |
| // Lazily compiled pattern cache β compile once on first scan. | |
| let compiledRules: Array<{ id: string; re: RegExp }> | null = null | |
| function getCompiledRules(): Array<{ id: string; re: RegExp }> { | |
| if (compiledRules === null) { | |
| compiledRules = SECRET_RULES.map(r => ({ | |
| id: r.id, | |
| re: new RegExp(r.source, r.flags), | |
| })) | |
| } | |
| return compiledRules | |
| } | |
| /** | |
| * Convert a gitleaks rule ID (kebab-case) to a human-readable label. | |
| * e.g., "github-pat" β "GitHub PAT", "aws-access-token" β "AWS Access Token" | |
| */ | |
| function ruleIdToLabel(ruleId: string): string { | |
| // Words where the canonical capitalization differs from title case | |
| const specialCase: Record<string, string> = { | |
| aws: 'AWS', | |
| gcp: 'GCP', | |
| api: 'API', | |
| pat: 'PAT', | |
| ad: 'AD', | |
| tf: 'TF', | |
| oauth: 'OAuth', | |
| npm: 'NPM', | |
| pypi: 'PyPI', | |
| jwt: 'JWT', | |
| github: 'GitHub', | |
| gitlab: 'GitLab', | |
| openai: 'OpenAI', | |
| digitalocean: 'DigitalOcean', | |
| huggingface: 'HuggingFace', | |
| hashicorp: 'HashiCorp', | |
| sendgrid: 'SendGrid', | |
| } | |
| return ruleId | |
| .split('-') | |
| .map(part => specialCase[part] ?? capitalize(part)) | |
| .join(' ') | |
| } | |
| /** | |
| * Scan a string for potential secrets. | |
| * | |
| * Returns one match per rule that fired (deduplicated by rule ID). The | |
| * actual matched text is intentionally NOT returned β we never log or | |
| * display secret values. | |
| */ | |
| export function scanForSecrets(content: string): SecretMatch[] { | |
| const matches: SecretMatch[] = [] | |
| const seen = new Set<string>() | |
| for (const rule of getCompiledRules()) { | |
| if (seen.has(rule.id)) { | |
| continue | |
| } | |
| if (rule.re.test(content)) { | |
| seen.add(rule.id) | |
| matches.push({ | |
| ruleId: rule.id, | |
| label: ruleIdToLabel(rule.id), | |
| }) | |
| } | |
| } | |
| return matches | |
| } | |
| /** | |
| * Get a human-readable label for a gitleaks rule ID. | |
| * Falls back to kebab-to-Title conversion for unknown IDs. | |
| */ | |
| export function getSecretLabel(ruleId: string): string { | |
| return ruleIdToLabel(ruleId) | |
| } | |
| /** | |
| * Redact any matched secrets in-place with [REDACTED]. | |
| * Unlike scanForSecrets, this returns the content with spans replaced | |
| * so the surrounding text can still be written to disk safely. | |
| */ | |
| let redactRules: RegExp[] | null = null | |
| export function redactSecrets(content: string): string { | |
| redactRules ??= SECRET_RULES.map( | |
| r => new RegExp(r.source, (r.flags ?? '').replace('g', '') + 'g'), | |
| ) | |
| for (const re of redactRules) { | |
| // Replace only the captured group, not the full match β patterns include | |
| // boundary chars (space, quote, ;) outside the group that must survive. | |
| content = content.replace(re, (match, g1) => | |
| typeof g1 === 'string' ? match.replace(g1, '[REDACTED]') : '[REDACTED]', | |
| ) | |
| } | |
| return content | |
| } | |