From 94eed6aae7d7f7e8bc8ff0b2d359adaa4dc89acb Mon Sep 17 00:00:00 2001 From: Jesse Quinn Date: Mon, 20 Jul 2026 14:01:37 -0700 Subject: [PATCH] feat: detect GitLab tokens and Azure client secrets Adds two built-in detection rules: - gitlab-token: GitLab personal access, deploy, runner, pipeline trigger, feed, and service account tokens (glpat-/gldt-/glrt-/ glptt-/glft-/glsoat- prefixes), including the newer routable token format with dot-separated segments. - azure-client-secret: Microsoft Entra ID (Azure AD) application client secrets, keyed on the distinctive "<3 chars>Q~" shape with a 31-34 char tail, using lookarounds since the charset includes non-word characters. GitHub fine-grained PATs (github_pat_) were already covered by the existing github-token rule; this adds test coverage for them and documents them in the README. --- README.md | 4 ++- dist/scanner/detectors.js | 23 ++++++++---- src/scanner/detectors.ts | 23 ++++++++---- tests/rules.test.ts | 75 ++++++++++++++++++++++++++++++--------- 4 files changed, 95 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 5165354..4195ea3 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,9 @@ Once restarted, the plugin will automatically scan all prompts before they reach ✅ **Secrets** - OpenAI API keys (`sk-...`, `sk-proj-...`) - AWS credentials -- GitHub tokens +- GitHub tokens (classic and fine-grained PATs) +- GitLab tokens (personal access, deploy, runner, pipeline trigger, service account) +- Azure client secrets (Microsoft Entra ID) - Stripe keys - JWT tokens - Bearer tokens diff --git a/dist/scanner/detectors.js b/dist/scanner/detectors.js index d080095..d4cd9a3 100644 --- a/dist/scanner/detectors.js +++ b/dist/scanner/detectors.js @@ -172,13 +172,13 @@ export const BUILTIN_RULES = [ enabled: true, }, { - id: 'gitlab-personal-access-token', - title: 'GitLab Personal Access Token', - description: 'Detects GitLab personal access tokens (default glpat- prefix)', - severity: 'high', + id: 'gitlab-token', + title: 'GitLab Token', + description: 'Detects GitLab personal access, deploy, runner, pipeline trigger, and service account tokens', + severity: 'critical', category: 'secret', - pattern: /\bglpat-[A-Za-z0-9_-]{20,64}\b/g, - examples: ['glpat-EXAMPLExxxxEXAMPLExxxx'], + pattern: /\bgl(?:pat|ptt|dt|rt|soat|ft)-[A-Za-z0-9_-]{20,}(?:\.[A-Za-z0-9_-]{2,})*\b/g, + examples: ['glpat-abcdefghij1234567890', 'glpat-EXAMPLEabcd1234567890.01.0example12'], redactionStrategy: 'token-replace', enabled: true, }, @@ -253,6 +253,17 @@ export const BUILTIN_RULES = [ redactionStrategy: 'token-replace', enabled: true, }, + { + id: 'azure-client-secret', + title: 'Azure Client Secret', + description: 'Detects Microsoft Entra ID (Azure AD) application client secrets', + severity: 'critical', + category: 'secret', + pattern: /(? { }); }); +describe('github-token rule', () => { + const githubRule = BUILTIN_RULES.find((rule) => rule.id === 'github-token') as DetectionRule; + const scanner = new PrivacyScanner([githubRule]); + + test.each([ + 'ghp_AbCdEfGhIjKlMnOpQrStUvWxYz0123456789', + 'github_pat_11ABCDEFG0abcdefghijklm_AbCdEfGhIjKlMnOpQrStUvWxYz0123456789abcdefghijklmnopqrstuvw', + ])('matches realistic token: %s', (token: string) => { + const result = scanner.scan(`my token is ${token}`); + expect(result.findings.some((finding) => finding.ruleId === 'github-token')).toBe(true); + }); + + test('does not match prose mentioning github_pat or short placeholders', () => { + const result = scanner.scan('set github_pat_here and ghp_abc123 in your env'); + expect(result.findings).toHaveLength(0); + }); +}); + +describe('gitlab-token rule', () => { + const gitlabRule = BUILTIN_RULES.find((rule) => rule.id === 'gitlab-token') as DetectionRule; + const scanner = new PrivacyScanner([gitlabRule]); + + test.each([ + 'glpat-abcdefghij1234567890', + 'glpat-EXAMPLEabcd1234567890.01.0example12', + 'gldt-AbCdEfGhIjKlMnOpQrSt', + 'glrt-t1_AbCdEfGhIjKlMnOpQr', + ])('matches realistic token: %s', (token: string) => { + const result = scanner.scan(`CI_JOB_TOKEN=${token}`); + expect(result.findings.some((finding) => finding.ruleId === 'gitlab-token')).toBe(true); + }); + + test('does not match prose or short placeholder values', () => { + const result = scanner.scan('create a glpat-style token; glpat-abc123 is a placeholder'); + expect(result.findings).toHaveLength(0); + }); +}); + +describe('azure-client-secret rule', () => { + const azureRule = BUILTIN_RULES.find((rule) => rule.id === 'azure-client-secret') as DetectionRule; + const scanner = new PrivacyScanner([azureRule]); + + test.each([ + 'q_x8Q~AbCdEfGhIjKlMnOpQrStUvWxYz.12345~a', + 'Iq18Q~yfZ2K7Vt.wNxE4pDb-Mh9cRj3sGl6uT', + ])('matches realistic secret: %s', (secret: string) => { + const result = scanner.scan(`AZURE_CLIENT_SECRET=${secret}`); + expect(result.findings.some((finding) => finding.ruleId === 'azure-client-secret')).toBe(true); + }); + + test('does not match GUIDs or generic base64-like strings', () => { + const result = scanner.scan( + 'client_id 4f9d2b1a-7c3e-4a5b-9d8f-1e2a3b4c5d6e value dGhpc2lzYWxvbmdiYXNlNjRzdHJpbmc0MGNoYXJz' + ); + expect(result.findings).toHaveLength(0); + }); +}); + describe('bearer-token rule', () => { const bearerRule = BUILTIN_RULES.find((rule) => rule.id === 'bearer-token') as DetectionRule; const scanner = new PrivacyScanner([bearerRule]); @@ -226,23 +284,6 @@ describe('slack-token rule', () => { }); }); -describe('gitlab-personal-access-token rule', () => { - const scanner = ruleScanner('gitlab-personal-access-token'); - - test('matches a realistic glpat- token', () => { - const result = scanner.scan('export GITLAB_TOKEN=glpat-EXAMPLExxxxEXAMPLExxxx'); - expect(result.findings.some((f) => f.ruleId === 'gitlab-personal-access-token')).toBe(true); - }); - - test.each(['glpat-xxxx', 'set GITLAB_TOKEN to your glpat- value'])( - 'does not match placeholders: %s', - (text: string) => { - const result = scanner.scan(text); - expect(result.findings).toHaveLength(0); - } - ); -}); - describe('gitlab-ci-job-token rule', () => { const scanner = ruleScanner('gitlab-ci-job-token');