From aac4202424d4a1ac1e8fdab451ec0d6ee33236e5 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Sat, 24 Jan 2026 11:52:53 +0000 Subject: [PATCH 1/6] Add fine-grained tokens to `GITHUB_TOKEN_PATTERNS` --- lib/analyze-action-post.js | 6 +++++- lib/init-action-post.js | 6 +++++- lib/start-proxy-action-post.js | 6 +++++- lib/upload-sarif-action-post.js | 6 +++++- src/artifact-scanner.ts | 6 +++++- 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/lib/analyze-action-post.js b/lib/analyze-action-post.js index d0e16267ca..722b8516c2 100644 --- a/lib/analyze-action-post.js +++ b/lib/analyze-action-post.js @@ -125449,9 +125449,13 @@ var path5 = __toESM(require("path")); var exec = __toESM(require_exec()); var GITHUB_TOKEN_PATTERNS = [ { - name: "Personal Access Token", + name: "Personal Access Token (Classic)", pattern: /\bghp_[a-zA-Z0-9]{36}\b/g }, + { + name: "Personal Access Token (Fine-grained)", + pattern: /\bgithub_pat_[a-zA-Z0-9_]+\b/g + }, { name: "OAuth Access Token", pattern: /\bgho_[a-zA-Z0-9]{36}\b/g diff --git a/lib/init-action-post.js b/lib/init-action-post.js index 272e6ee6f1..b3ee109176 100644 --- a/lib/init-action-post.js +++ b/lib/init-action-post.js @@ -130111,9 +130111,13 @@ var path11 = __toESM(require("path")); var exec = __toESM(require_exec()); var GITHUB_TOKEN_PATTERNS = [ { - name: "Personal Access Token", + name: "Personal Access Token (Classic)", pattern: /\bghp_[a-zA-Z0-9]{36}\b/g }, + { + name: "Personal Access Token (Fine-grained)", + pattern: /\bgithub_pat_[a-zA-Z0-9_]+\b/g + }, { name: "OAuth Access Token", pattern: /\bgho_[a-zA-Z0-9]{36}\b/g diff --git a/lib/start-proxy-action-post.js b/lib/start-proxy-action-post.js index 6d42e77685..34d5f914e0 100644 --- a/lib/start-proxy-action-post.js +++ b/lib/start-proxy-action-post.js @@ -124389,9 +124389,13 @@ var path2 = __toESM(require("path")); var exec = __toESM(require_exec()); var GITHUB_TOKEN_PATTERNS = [ { - name: "Personal Access Token", + name: "Personal Access Token (Classic)", pattern: /\bghp_[a-zA-Z0-9]{36}\b/g }, + { + name: "Personal Access Token (Fine-grained)", + pattern: /\bgithub_pat_[a-zA-Z0-9_]+\b/g + }, { name: "OAuth Access Token", pattern: /\bgho_[a-zA-Z0-9]{36}\b/g diff --git a/lib/upload-sarif-action-post.js b/lib/upload-sarif-action-post.js index c7e1156f3e..d0879bd276 100644 --- a/lib/upload-sarif-action-post.js +++ b/lib/upload-sarif-action-post.js @@ -124374,9 +124374,13 @@ var path = __toESM(require("path")); var exec = __toESM(require_exec()); var GITHUB_TOKEN_PATTERNS = [ { - name: "Personal Access Token", + name: "Personal Access Token (Classic)", pattern: /\bghp_[a-zA-Z0-9]{36}\b/g }, + { + name: "Personal Access Token (Fine-grained)", + pattern: /\bgithub_pat_[a-zA-Z0-9_]+\b/g + }, { name: "OAuth Access Token", pattern: /\bgho_[a-zA-Z0-9]{36}\b/g diff --git a/src/artifact-scanner.ts b/src/artifact-scanner.ts index d04445bf4d..06117d6d63 100644 --- a/src/artifact-scanner.ts +++ b/src/artifact-scanner.ts @@ -13,9 +13,13 @@ import { getErrorMessage } from "./util"; */ const GITHUB_TOKEN_PATTERNS = [ { - name: "Personal Access Token", + name: "Personal Access Token (Classic)", pattern: /\bghp_[a-zA-Z0-9]{36}\b/g, }, + { + name: "Personal Access Token (Fine-grained)", + pattern: /\bgithub_pat_[a-zA-Z0-9_]+\b/g, + }, { name: "OAuth Access Token", pattern: /\bgho_[a-zA-Z0-9]{36}\b/g, From 49cdf744d9616da1995b4f2135ae311b7eb7fda5 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Sat, 24 Jan 2026 11:58:10 +0000 Subject: [PATCH 2/6] Use enum for token types --- lib/analyze-action-post.js | 20 ++++++++-------- lib/init-action-post.js | 20 ++++++++-------- lib/start-proxy-action-post.js | 20 ++++++++-------- lib/upload-sarif-action-post.js | 20 ++++++++-------- src/artifact-scanner.ts | 41 ++++++++++++++++++++++++--------- 5 files changed, 70 insertions(+), 51 deletions(-) diff --git a/lib/analyze-action-post.js b/lib/analyze-action-post.js index 722b8516c2..faeef384da 100644 --- a/lib/analyze-action-post.js +++ b/lib/analyze-action-post.js @@ -125449,31 +125449,31 @@ var path5 = __toESM(require("path")); var exec = __toESM(require_exec()); var GITHUB_TOKEN_PATTERNS = [ { - name: "Personal Access Token (Classic)", + type: "Personal Access Token (Classic)" /* PersonalAccessClassic */, pattern: /\bghp_[a-zA-Z0-9]{36}\b/g }, { - name: "Personal Access Token (Fine-grained)", + type: "Personal Access Token (Fine-grained)" /* PersonalAccessFineGrained */, pattern: /\bgithub_pat_[a-zA-Z0-9_]+\b/g }, { - name: "OAuth Access Token", + type: "OAuth Access Token" /* OAuth */, pattern: /\bgho_[a-zA-Z0-9]{36}\b/g }, { - name: "User-to-Server Token", + type: "User-to-Server Token" /* UserToServer */, pattern: /\bghu_[a-zA-Z0-9]{36}\b/g }, { - name: "Server-to-Server Token", + type: "Server-to-Server Token" /* ServerToServer */, pattern: /\bghs_[a-zA-Z0-9]{36}\b/g }, { - name: "Refresh Token", + type: "Refresh Token" /* Refresh */, pattern: /\bghr_[a-zA-Z0-9]{36}\b/g }, { - name: "App Installation Access Token", + type: "App Installation Access Token" /* AppInstallationAccess */, pattern: /\bghs_[a-zA-Z0-9]{255}\b/g } ]; @@ -125481,13 +125481,13 @@ function scanFileForTokens(filePath, relativePath, logger) { const findings = []; try { const content = fs5.readFileSync(filePath, "utf8"); - for (const { name, pattern } of GITHUB_TOKEN_PATTERNS) { + for (const { type: type2, pattern } of GITHUB_TOKEN_PATTERNS) { const matches = content.match(pattern); if (matches) { for (let i = 0; i < matches.length; i++) { - findings.push({ tokenType: name, filePath: relativePath }); + findings.push({ tokenType: type2, filePath: relativePath }); } - logger.debug(`Found ${matches.length} ${name}(s) in ${relativePath}`); + logger.debug(`Found ${matches.length} ${type2}(s) in ${relativePath}`); } } return findings; diff --git a/lib/init-action-post.js b/lib/init-action-post.js index b3ee109176..0698eb017c 100644 --- a/lib/init-action-post.js +++ b/lib/init-action-post.js @@ -130111,31 +130111,31 @@ var path11 = __toESM(require("path")); var exec = __toESM(require_exec()); var GITHUB_TOKEN_PATTERNS = [ { - name: "Personal Access Token (Classic)", + type: "Personal Access Token (Classic)" /* PersonalAccessClassic */, pattern: /\bghp_[a-zA-Z0-9]{36}\b/g }, { - name: "Personal Access Token (Fine-grained)", + type: "Personal Access Token (Fine-grained)" /* PersonalAccessFineGrained */, pattern: /\bgithub_pat_[a-zA-Z0-9_]+\b/g }, { - name: "OAuth Access Token", + type: "OAuth Access Token" /* OAuth */, pattern: /\bgho_[a-zA-Z0-9]{36}\b/g }, { - name: "User-to-Server Token", + type: "User-to-Server Token" /* UserToServer */, pattern: /\bghu_[a-zA-Z0-9]{36}\b/g }, { - name: "Server-to-Server Token", + type: "Server-to-Server Token" /* ServerToServer */, pattern: /\bghs_[a-zA-Z0-9]{36}\b/g }, { - name: "Refresh Token", + type: "Refresh Token" /* Refresh */, pattern: /\bghr_[a-zA-Z0-9]{36}\b/g }, { - name: "App Installation Access Token", + type: "App Installation Access Token" /* AppInstallationAccess */, pattern: /\bghs_[a-zA-Z0-9]{255}\b/g } ]; @@ -130143,13 +130143,13 @@ function scanFileForTokens(filePath, relativePath, logger) { const findings = []; try { const content = fs12.readFileSync(filePath, "utf8"); - for (const { name, pattern } of GITHUB_TOKEN_PATTERNS) { + for (const { type: type2, pattern } of GITHUB_TOKEN_PATTERNS) { const matches = content.match(pattern); if (matches) { for (let i = 0; i < matches.length; i++) { - findings.push({ tokenType: name, filePath: relativePath }); + findings.push({ tokenType: type2, filePath: relativePath }); } - logger.debug(`Found ${matches.length} ${name}(s) in ${relativePath}`); + logger.debug(`Found ${matches.length} ${type2}(s) in ${relativePath}`); } } return findings; diff --git a/lib/start-proxy-action-post.js b/lib/start-proxy-action-post.js index 34d5f914e0..ec9aa041dd 100644 --- a/lib/start-proxy-action-post.js +++ b/lib/start-proxy-action-post.js @@ -124389,31 +124389,31 @@ var path2 = __toESM(require("path")); var exec = __toESM(require_exec()); var GITHUB_TOKEN_PATTERNS = [ { - name: "Personal Access Token (Classic)", + type: "Personal Access Token (Classic)" /* PersonalAccessClassic */, pattern: /\bghp_[a-zA-Z0-9]{36}\b/g }, { - name: "Personal Access Token (Fine-grained)", + type: "Personal Access Token (Fine-grained)" /* PersonalAccessFineGrained */, pattern: /\bgithub_pat_[a-zA-Z0-9_]+\b/g }, { - name: "OAuth Access Token", + type: "OAuth Access Token" /* OAuth */, pattern: /\bgho_[a-zA-Z0-9]{36}\b/g }, { - name: "User-to-Server Token", + type: "User-to-Server Token" /* UserToServer */, pattern: /\bghu_[a-zA-Z0-9]{36}\b/g }, { - name: "Server-to-Server Token", + type: "Server-to-Server Token" /* ServerToServer */, pattern: /\bghs_[a-zA-Z0-9]{36}\b/g }, { - name: "Refresh Token", + type: "Refresh Token" /* Refresh */, pattern: /\bghr_[a-zA-Z0-9]{36}\b/g }, { - name: "App Installation Access Token", + type: "App Installation Access Token" /* AppInstallationAccess */, pattern: /\bghs_[a-zA-Z0-9]{255}\b/g } ]; @@ -124421,13 +124421,13 @@ function scanFileForTokens(filePath, relativePath, logger) { const findings = []; try { const content = fs2.readFileSync(filePath, "utf8"); - for (const { name, pattern } of GITHUB_TOKEN_PATTERNS) { + for (const { type: type2, pattern } of GITHUB_TOKEN_PATTERNS) { const matches = content.match(pattern); if (matches) { for (let i = 0; i < matches.length; i++) { - findings.push({ tokenType: name, filePath: relativePath }); + findings.push({ tokenType: type2, filePath: relativePath }); } - logger.debug(`Found ${matches.length} ${name}(s) in ${relativePath}`); + logger.debug(`Found ${matches.length} ${type2}(s) in ${relativePath}`); } } return findings; diff --git a/lib/upload-sarif-action-post.js b/lib/upload-sarif-action-post.js index d0879bd276..aa2ab9cedd 100644 --- a/lib/upload-sarif-action-post.js +++ b/lib/upload-sarif-action-post.js @@ -124374,31 +124374,31 @@ var path = __toESM(require("path")); var exec = __toESM(require_exec()); var GITHUB_TOKEN_PATTERNS = [ { - name: "Personal Access Token (Classic)", + type: "Personal Access Token (Classic)" /* PersonalAccessClassic */, pattern: /\bghp_[a-zA-Z0-9]{36}\b/g }, { - name: "Personal Access Token (Fine-grained)", + type: "Personal Access Token (Fine-grained)" /* PersonalAccessFineGrained */, pattern: /\bgithub_pat_[a-zA-Z0-9_]+\b/g }, { - name: "OAuth Access Token", + type: "OAuth Access Token" /* OAuth */, pattern: /\bgho_[a-zA-Z0-9]{36}\b/g }, { - name: "User-to-Server Token", + type: "User-to-Server Token" /* UserToServer */, pattern: /\bghu_[a-zA-Z0-9]{36}\b/g }, { - name: "Server-to-Server Token", + type: "Server-to-Server Token" /* ServerToServer */, pattern: /\bghs_[a-zA-Z0-9]{36}\b/g }, { - name: "Refresh Token", + type: "Refresh Token" /* Refresh */, pattern: /\bghr_[a-zA-Z0-9]{36}\b/g }, { - name: "App Installation Access Token", + type: "App Installation Access Token" /* AppInstallationAccess */, pattern: /\bghs_[a-zA-Z0-9]{255}\b/g } ]; @@ -124406,13 +124406,13 @@ function scanFileForTokens(filePath, relativePath, logger) { const findings = []; try { const content = fs.readFileSync(filePath, "utf8"); - for (const { name, pattern } of GITHUB_TOKEN_PATTERNS) { + for (const { type: type2, pattern } of GITHUB_TOKEN_PATTERNS) { const matches = content.match(pattern); if (matches) { for (let i = 0; i < matches.length; i++) { - findings.push({ tokenType: name, filePath: relativePath }); + findings.push({ tokenType: type2, filePath: relativePath }); } - logger.debug(`Found ${matches.length} ${name}(s) in ${relativePath}`); + logger.debug(`Found ${matches.length} ${type2}(s) in ${relativePath}`); } } return findings; diff --git a/src/artifact-scanner.ts b/src/artifact-scanner.ts index 06117d6d63..6c98505a77 100644 --- a/src/artifact-scanner.ts +++ b/src/artifact-scanner.ts @@ -7,37 +7,56 @@ import * as exec from "@actions/exec"; import { Logger } from "./logging"; import { getErrorMessage } from "./util"; +/** + * Enumerates known types of GitHub token formats. + */ +export enum TokenType { + PersonalAccessClassic = "Personal Access Token (Classic)", + PersonalAccessFineGrained = "Personal Access Token (Fine-grained)", + OAuth = "OAuth Access Token", + UserToServer = "User-to-Server Token", + ServerToServer = "Server-to-Server Token", + Refresh = "Refresh Token", + AppInstallationAccess = "App Installation Access Token", +} + +/** A value of this type associates a token type with its pattern. */ +export interface TokenPattern { + type: TokenType; + pattern: RegExp; +} + /** * GitHub token patterns to scan for. * These patterns match various GitHub token formats. */ -const GITHUB_TOKEN_PATTERNS = [ +const GITHUB_TOKEN_PATTERNS: TokenPattern[] = [ { - name: "Personal Access Token (Classic)", + type: TokenType.PersonalAccessClassic, pattern: /\bghp_[a-zA-Z0-9]{36}\b/g, }, { - name: "Personal Access Token (Fine-grained)", + type: TokenType.PersonalAccessFineGrained, pattern: /\bgithub_pat_[a-zA-Z0-9_]+\b/g, }, { - name: "OAuth Access Token", + type: TokenType.OAuth, pattern: /\bgho_[a-zA-Z0-9]{36}\b/g, }, { - name: "User-to-Server Token", + type: TokenType.UserToServer, pattern: /\bghu_[a-zA-Z0-9]{36}\b/g, }, { - name: "Server-to-Server Token", + type: TokenType.ServerToServer, pattern: /\bghs_[a-zA-Z0-9]{36}\b/g, }, { - name: "Refresh Token", + type: TokenType.Refresh, pattern: /\bghr_[a-zA-Z0-9]{36}\b/g, }, { - name: "App Installation Access Token", + type: TokenType.AppInstallationAccess, pattern: /\bghs_[a-zA-Z0-9]{255}\b/g, }, ]; @@ -69,13 +88,13 @@ function scanFileForTokens( try { const content = fs.readFileSync(filePath, "utf8"); - for (const { name, pattern } of GITHUB_TOKEN_PATTERNS) { + for (const { type, pattern } of GITHUB_TOKEN_PATTERNS) { const matches = content.match(pattern); if (matches) { for (let i = 0; i < matches.length; i++) { - findings.push({ tokenType: name, filePath: relativePath }); + findings.push({ tokenType: type, filePath: relativePath }); } - logger.debug(`Found ${matches.length} ${name}(s) in ${relativePath}`); + logger.debug(`Found ${matches.length} ${type}(s) in ${relativePath}`); } } From 0ae8b05d08e50edcdb70d113fd46da65e300a0e4 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Sat, 24 Jan 2026 12:25:40 +0000 Subject: [PATCH 3/6] Extend unit tests to cover all token types --- src/artifact-scanner.test.ts | 104 ++++++++++++++++++++++++++--------- 1 file changed, 79 insertions(+), 25 deletions(-) diff --git a/src/artifact-scanner.test.ts b/src/artifact-scanner.test.ts index 5678d2cadd..55b07d7d31 100644 --- a/src/artifact-scanner.test.ts +++ b/src/artifact-scanner.test.ts @@ -4,36 +4,90 @@ import * as path from "path"; import test from "ava"; -import { scanArtifactsForTokens } from "./artifact-scanner"; +import { scanArtifactsForTokens, TokenType } from "./artifact-scanner"; import { getRunnerLogger } from "./logging"; -import { getRecordingLogger, LoggedMessage } from "./testing-utils"; +import { + checkExpectedLogMessages, + getRecordingLogger, + LoggedMessage, +} from "./testing-utils"; -test("scanArtifactsForTokens detects GitHub tokens in files", async (t) => { - const logger = getRunnerLogger(true); - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "scanner-test-")); +function makeTestToken(length: number = 36) { + const chars = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + return chars.repeat(Math.ceil(length / chars.length)).slice(0, length); +} - try { - // Create a test file with a fake GitHub token - const testFile = path.join(tempDir, "test.txt"); - fs.writeFileSync( - testFile, - "This is a test file with token ghp_1234567890123456789012345678901234AB", - ); +test("makeTestToken", (t) => { + t.is(makeTestToken().length, 36); + t.is(makeTestToken(255).length, 255); +}); - const error = await t.throwsAsync( - async () => await scanArtifactsForTokens([testFile], logger), - ); +const testTokens = [ + { + type: TokenType.PersonalAccessClassic, + value: `ghp_${makeTestToken()}`, + checkPattern: "Personal Access Token", + }, + { + type: TokenType.PersonalAccessFineGrained, + value: + "github_pat_1234567890ABCDEFGHIJKL_MNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHI", + checkPattern: "Personal Access Token", + }, + { + type: TokenType.OAuth, + value: `gho_${makeTestToken()}`, + }, + { + type: TokenType.UserToServer, + value: `ghu_${makeTestToken()}`, + }, + { + type: TokenType.ServerToServer, + value: `ghs_${makeTestToken()}`, + }, + { + type: TokenType.Refresh, + value: `ghr_${makeTestToken()}`, + }, + { + type: TokenType.AppInstallationAccess, + value: `ghs_${makeTestToken(255)}`, + }, +]; - t.regex( - error?.message || "", - /Found 1 potential GitHub token.*Personal Access Token/, - ); - t.regex(error?.message || "", /test\.txt/); - } finally { - // Clean up - fs.rmSync(tempDir, { recursive: true, force: true }); - } -}); +for (const { type, value, checkPattern } of testTokens) { + test(`scanArtifactsForTokens detects GitHub ${type} tokens in files`, async (t) => { + const logMessages = []; + const logger = getRecordingLogger(logMessages, { logToConsole: false }); + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "scanner-test-")); + + try { + // Create a test file with a fake GitHub token + const testFile = path.join(tempDir, "test.txt"); + fs.writeFileSync(testFile, `This is a test file with token ${value}`); + + const error = await t.throwsAsync( + async () => await scanArtifactsForTokens([testFile], logger), + ); + + t.regex( + error?.message || "", + new RegExp(`Found 1 potential GitHub token.*${checkPattern || type}`), + ); + t.regex(error?.message || "", /test\.txt/); + + checkExpectedLogMessages(t, logMessages, [ + "Starting best-effort check", + `Found 1 ${type}`, + ]); + } finally { + // Clean up + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); +} test("scanArtifactsForTokens handles files without tokens", async (t) => { const logger = getRunnerLogger(true); From 0fcbec3eec443e43e499131cf5a041b1cbee9e65 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Sat, 24 Jan 2026 12:38:14 +0000 Subject: [PATCH 4/6] Add `isAuthToken` function, with tests --- lib/analyze-action-post.js | 18 ++++++++------- lib/init-action-post.js | 18 ++++++++------- lib/start-proxy-action-post.js | 18 ++++++++------- lib/upload-sarif-action-post.js | 18 ++++++++------- src/artifact-scanner.test.ts | 37 +++++++++++++++++++++++++++++- src/artifact-scanner.ts | 40 ++++++++++++++++++++++++++------- 6 files changed, 108 insertions(+), 41 deletions(-) diff --git a/lib/analyze-action-post.js b/lib/analyze-action-post.js index faeef384da..4a0177d48f 100644 --- a/lib/analyze-action-post.js +++ b/lib/analyze-action-post.js @@ -125447,15 +125447,17 @@ var fs5 = __toESM(require("fs")); var os = __toESM(require("os")); var path5 = __toESM(require("path")); var exec = __toESM(require_exec()); +var GITHUB_PAT_CLASSIC_PATTERN = { + type: "Personal Access Token (Classic)" /* PersonalAccessClassic */, + pattern: /\bghp_[a-zA-Z0-9]{36}\b/g +}; +var GITHUB_PAT_FINE_GRAINED_PATTERN = { + type: "Personal Access Token (Fine-grained)" /* PersonalAccessFineGrained */, + pattern: /\bgithub_pat_[a-zA-Z0-9_]+\b/g +}; var GITHUB_TOKEN_PATTERNS = [ - { - type: "Personal Access Token (Classic)" /* PersonalAccessClassic */, - pattern: /\bghp_[a-zA-Z0-9]{36}\b/g - }, - { - type: "Personal Access Token (Fine-grained)" /* PersonalAccessFineGrained */, - pattern: /\bgithub_pat_[a-zA-Z0-9_]+\b/g - }, + GITHUB_PAT_CLASSIC_PATTERN, + GITHUB_PAT_FINE_GRAINED_PATTERN, { type: "OAuth Access Token" /* OAuth */, pattern: /\bgho_[a-zA-Z0-9]{36}\b/g diff --git a/lib/init-action-post.js b/lib/init-action-post.js index 0698eb017c..999dd402d7 100644 --- a/lib/init-action-post.js +++ b/lib/init-action-post.js @@ -130109,15 +130109,17 @@ var fs12 = __toESM(require("fs")); var os2 = __toESM(require("os")); var path11 = __toESM(require("path")); var exec = __toESM(require_exec()); +var GITHUB_PAT_CLASSIC_PATTERN = { + type: "Personal Access Token (Classic)" /* PersonalAccessClassic */, + pattern: /\bghp_[a-zA-Z0-9]{36}\b/g +}; +var GITHUB_PAT_FINE_GRAINED_PATTERN = { + type: "Personal Access Token (Fine-grained)" /* PersonalAccessFineGrained */, + pattern: /\bgithub_pat_[a-zA-Z0-9_]+\b/g +}; var GITHUB_TOKEN_PATTERNS = [ - { - type: "Personal Access Token (Classic)" /* PersonalAccessClassic */, - pattern: /\bghp_[a-zA-Z0-9]{36}\b/g - }, - { - type: "Personal Access Token (Fine-grained)" /* PersonalAccessFineGrained */, - pattern: /\bgithub_pat_[a-zA-Z0-9_]+\b/g - }, + GITHUB_PAT_CLASSIC_PATTERN, + GITHUB_PAT_FINE_GRAINED_PATTERN, { type: "OAuth Access Token" /* OAuth */, pattern: /\bgho_[a-zA-Z0-9]{36}\b/g diff --git a/lib/start-proxy-action-post.js b/lib/start-proxy-action-post.js index ec9aa041dd..163f3dbbba 100644 --- a/lib/start-proxy-action-post.js +++ b/lib/start-proxy-action-post.js @@ -124387,15 +124387,17 @@ var fs2 = __toESM(require("fs")); var os = __toESM(require("os")); var path2 = __toESM(require("path")); var exec = __toESM(require_exec()); +var GITHUB_PAT_CLASSIC_PATTERN = { + type: "Personal Access Token (Classic)" /* PersonalAccessClassic */, + pattern: /\bghp_[a-zA-Z0-9]{36}\b/g +}; +var GITHUB_PAT_FINE_GRAINED_PATTERN = { + type: "Personal Access Token (Fine-grained)" /* PersonalAccessFineGrained */, + pattern: /\bgithub_pat_[a-zA-Z0-9_]+\b/g +}; var GITHUB_TOKEN_PATTERNS = [ - { - type: "Personal Access Token (Classic)" /* PersonalAccessClassic */, - pattern: /\bghp_[a-zA-Z0-9]{36}\b/g - }, - { - type: "Personal Access Token (Fine-grained)" /* PersonalAccessFineGrained */, - pattern: /\bgithub_pat_[a-zA-Z0-9_]+\b/g - }, + GITHUB_PAT_CLASSIC_PATTERN, + GITHUB_PAT_FINE_GRAINED_PATTERN, { type: "OAuth Access Token" /* OAuth */, pattern: /\bgho_[a-zA-Z0-9]{36}\b/g diff --git a/lib/upload-sarif-action-post.js b/lib/upload-sarif-action-post.js index aa2ab9cedd..0d6e2e9845 100644 --- a/lib/upload-sarif-action-post.js +++ b/lib/upload-sarif-action-post.js @@ -124372,15 +124372,17 @@ var fs = __toESM(require("fs")); var os = __toESM(require("os")); var path = __toESM(require("path")); var exec = __toESM(require_exec()); +var GITHUB_PAT_CLASSIC_PATTERN = { + type: "Personal Access Token (Classic)" /* PersonalAccessClassic */, + pattern: /\bghp_[a-zA-Z0-9]{36}\b/g +}; +var GITHUB_PAT_FINE_GRAINED_PATTERN = { + type: "Personal Access Token (Fine-grained)" /* PersonalAccessFineGrained */, + pattern: /\bgithub_pat_[a-zA-Z0-9_]+\b/g +}; var GITHUB_TOKEN_PATTERNS = [ - { - type: "Personal Access Token (Classic)" /* PersonalAccessClassic */, - pattern: /\bghp_[a-zA-Z0-9]{36}\b/g - }, - { - type: "Personal Access Token (Fine-grained)" /* PersonalAccessFineGrained */, - pattern: /\bgithub_pat_[a-zA-Z0-9_]+\b/g - }, + GITHUB_PAT_CLASSIC_PATTERN, + GITHUB_PAT_FINE_GRAINED_PATTERN, { type: "OAuth Access Token" /* OAuth */, pattern: /\bgho_[a-zA-Z0-9]{36}\b/g diff --git a/src/artifact-scanner.test.ts b/src/artifact-scanner.test.ts index 55b07d7d31..c19676fb20 100644 --- a/src/artifact-scanner.test.ts +++ b/src/artifact-scanner.test.ts @@ -4,7 +4,12 @@ import * as path from "path"; import test from "ava"; -import { scanArtifactsForTokens, TokenType } from "./artifact-scanner"; +import { + GITHUB_PAT_CLASSIC_PATTERN, + isAuthToken, + scanArtifactsForTokens, + TokenType, +} from "./artifact-scanner"; import { getRunnerLogger } from "./logging"; import { checkExpectedLogMessages, @@ -23,6 +28,36 @@ test("makeTestToken", (t) => { t.is(makeTestToken(255).length, 255); }); +test("isAuthToken", (t) => { + // Undefined for strings that aren't tokens + t.is(isAuthToken("some string"), undefined); + t.is(isAuthToken("ghp_"), undefined); + t.is(isAuthToken("ghp_123"), undefined); + + // Token types for strings that are tokens. + t.is(isAuthToken(`ghp_${makeTestToken()}`), TokenType.PersonalAccessClassic); + t.is( + isAuthToken(`ghs_${makeTestToken(255)}`), + TokenType.AppInstallationAccess, + ); + t.is( + isAuthToken(`github_pat_${makeTestToken(22)}_${makeTestToken(59)}`), + TokenType.PersonalAccessFineGrained, + ); + + // With a custom pattern set + t.is( + isAuthToken(`ghp_${makeTestToken()}`, [GITHUB_PAT_CLASSIC_PATTERN]), + TokenType.PersonalAccessClassic, + ); + t.is( + isAuthToken(`github_pat_${makeTestToken(22)}_${makeTestToken(59)}`, [ + GITHUB_PAT_CLASSIC_PATTERN, + ]), + undefined, + ); +}); + const testTokens = [ { type: TokenType.PersonalAccessClassic, diff --git a/src/artifact-scanner.ts b/src/artifact-scanner.ts index 6c98505a77..329ced0cba 100644 --- a/src/artifact-scanner.ts +++ b/src/artifact-scanner.ts @@ -26,19 +26,25 @@ export interface TokenPattern { pattern: RegExp; } +/** The pattern for PATs (Classic) */ +export const GITHUB_PAT_CLASSIC_PATTERN: TokenPattern = { + type: TokenType.PersonalAccessClassic, + pattern: /\bghp_[a-zA-Z0-9]{36}\b/g, +}; + +/** The pattern for PATs (Fine-grained) */ +export const GITHUB_PAT_FINE_GRAINED_PATTERN: TokenPattern = { + type: TokenType.PersonalAccessFineGrained, + pattern: /\bgithub_pat_[a-zA-Z0-9_]+\b/g, +}; + /** * GitHub token patterns to scan for. * These patterns match various GitHub token formats. */ const GITHUB_TOKEN_PATTERNS: TokenPattern[] = [ - { - type: TokenType.PersonalAccessClassic, - pattern: /\bghp_[a-zA-Z0-9]{36}\b/g, - }, - { - type: TokenType.PersonalAccessFineGrained, - pattern: /\bgithub_pat_[a-zA-Z0-9_]+\b/g, - }, + GITHUB_PAT_CLASSIC_PATTERN, + GITHUB_PAT_FINE_GRAINED_PATTERN, { type: TokenType.OAuth, pattern: /\bgho_[a-zA-Z0-9]{36}\b/g, @@ -71,6 +77,24 @@ interface ScanResult { findings: TokenFinding[]; } +/** + * Checks whether `value` matches any token `patterns`. + * @param value The value to match against. + * @param patterns The patterns to check. + * @returns The type of the first matching pattern, or `undefined` if none match. + */ +export function isAuthToken( + value: string, + patterns: TokenPattern[] = GITHUB_TOKEN_PATTERNS, +) { + for (const { type, pattern } of patterns) { + if (pattern.test(value)) { + return type; + } + } + return undefined; +} + /** * Scans a file for GitHub tokens. * From c12cf8d49a7955ab2eb48d716481659cf130c336 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Sat, 24 Jan 2026 12:55:32 +0000 Subject: [PATCH 5/6] Move `makeTestToken` to `testing-utils` --- src/artifact-scanner.test.ts | 7 +------ src/testing-utils.ts | 6 ++++++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/artifact-scanner.test.ts b/src/artifact-scanner.test.ts index c19676fb20..137738201c 100644 --- a/src/artifact-scanner.test.ts +++ b/src/artifact-scanner.test.ts @@ -15,14 +15,9 @@ import { checkExpectedLogMessages, getRecordingLogger, LoggedMessage, + makeTestToken, } from "./testing-utils"; -function makeTestToken(length: number = 36) { - const chars = - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - return chars.repeat(Math.ceil(length / chars.length)).slice(0, length); -} - test("makeTestToken", (t) => { t.is(makeTestToken().length, 36); t.is(makeTestToken(255).length, 255); diff --git a/src/testing-utils.ts b/src/testing-utils.ts index 66a6c25fb7..bee7d1adad 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -412,3 +412,9 @@ export function createTestConfig(overrides: Partial): Config { overrides, ); } + +export function makeTestToken(length: number = 36) { + const chars = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + return chars.repeat(Math.ceil(length / chars.length)).slice(0, length); +} From 9fccf271ffa7655c072e3a14cc6dbe1075b753c6 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Sat, 24 Jan 2026 13:02:41 +0000 Subject: [PATCH 6/6] Warn if a private registry configuration uses a PAT, but has no username --- lib/start-proxy-action.js | 90 +++++++++++++++++++++++++++++++-------- src/start-proxy.test.ts | 38 ++++++++++++++++- src/start-proxy.ts | 21 +++++++++ 3 files changed, 130 insertions(+), 19 deletions(-) diff --git a/lib/start-proxy-action.js b/lib/start-proxy-action.js index 39350b8091..a6a44e031c 100644 --- a/lib/start-proxy-action.js +++ b/lib/start-proxy-action.js @@ -19578,11 +19578,11 @@ var require_exec = __commonJS({ }); }; Object.defineProperty(exports2, "__esModule", { value: true }); - exports2.exec = exec; + exports2.exec = exec3; exports2.getExecOutput = getExecOutput; var string_decoder_1 = require("string_decoder"); var tr = __importStar2(require_toolrunner()); - function exec(commandLine, args, options) { + function exec3(commandLine, args, options) { return __awaiter2(this, void 0, void 0, function* () { const commandArgs = tr.argStringToArray(commandLine); if (commandArgs.length === 0) { @@ -19616,7 +19616,7 @@ var require_exec = __commonJS({ } }; const listeners = Object.assign(Object.assign({}, options === null || options === void 0 ? void 0 : options.listeners), { stdout: stdOutListener, stderr: stdErrListener }); - const exitCode = yield exec(commandLine, args, Object.assign(Object.assign({}, options), { listeners })); + const exitCode = yield exec3(commandLine, args, Object.assign(Object.assign({}, options), { listeners })); stdout += stdoutDecoder.end(); stderr += stderrDecoder.end(); return { @@ -19704,12 +19704,12 @@ var require_platform = __commonJS({ exports2.isLinux = exports2.isMacOS = exports2.isWindows = exports2.arch = exports2.platform = void 0; exports2.getDetails = getDetails; var os_1 = __importDefault2(require("os")); - var exec = __importStar2(require_exec()); + var exec3 = __importStar2(require_exec()); var getWindowsInfo = () => __awaiter2(void 0, void 0, void 0, function* () { - const { stdout: version } = yield exec.getExecOutput('powershell -command "(Get-CimInstance -ClassName Win32_OperatingSystem).Version"', void 0, { + const { stdout: version } = yield exec3.getExecOutput('powershell -command "(Get-CimInstance -ClassName Win32_OperatingSystem).Version"', void 0, { silent: true }); - const { stdout: name } = yield exec.getExecOutput('powershell -command "(Get-CimInstance -ClassName Win32_OperatingSystem).Caption"', void 0, { + const { stdout: name } = yield exec3.getExecOutput('powershell -command "(Get-CimInstance -ClassName Win32_OperatingSystem).Caption"', void 0, { silent: true }); return { @@ -19719,7 +19719,7 @@ var require_platform = __commonJS({ }); var getMacOsInfo = () => __awaiter2(void 0, void 0, void 0, function* () { var _a, _b, _c, _d; - const { stdout } = yield exec.getExecOutput("sw_vers", void 0, { + const { stdout } = yield exec3.getExecOutput("sw_vers", void 0, { silent: true }); const version = (_b = (_a = stdout.match(/ProductVersion:\s*(.+)/)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : ""; @@ -19730,7 +19730,7 @@ var require_platform = __commonJS({ }; }); var getLinuxInfo = () => __awaiter2(void 0, void 0, void 0, function* () { - const { stdout } = yield exec.getExecOutput("lsb_release", ["-i", "-r", "-s"], { + const { stdout } = yield exec3.getExecOutput("lsb_release", ["-i", "-r", "-s"], { silent: true }); const [name, version] = stdout.trim().split("\n"); @@ -50597,7 +50597,7 @@ var require_exec2 = __commonJS({ exports2.getExecOutput = exports2.exec = void 0; var string_decoder_1 = require("string_decoder"); var tr = __importStar2(require_toolrunner2()); - function exec(commandLine, args, options) { + function exec3(commandLine, args, options) { return __awaiter2(this, void 0, void 0, function* () { const commandArgs = tr.argStringToArray(commandLine); if (commandArgs.length === 0) { @@ -50609,7 +50609,7 @@ var require_exec2 = __commonJS({ return runner.exec(); }); } - exports2.exec = exec; + exports2.exec = exec3; function getExecOutput(commandLine, args, options) { var _a, _b; return __awaiter2(this, void 0, void 0, function* () { @@ -50632,7 +50632,7 @@ var require_exec2 = __commonJS({ } }; const listeners = Object.assign(Object.assign({}, options === null || options === void 0 ? void 0 : options.listeners), { stdout: stdOutListener, stderr: stdErrListener }); - const exitCode = yield exec(commandLine, args, Object.assign(Object.assign({}, options), { listeners })); + const exitCode = yield exec3(commandLine, args, Object.assign(Object.assign({}, options), { listeners })); stdout += stdoutDecoder.end(); stderr += stderrDecoder.end(); return { @@ -50710,12 +50710,12 @@ var require_platform2 = __commonJS({ Object.defineProperty(exports2, "__esModule", { value: true }); exports2.getDetails = exports2.isLinux = exports2.isMacOS = exports2.isWindows = exports2.arch = exports2.platform = void 0; var os_1 = __importDefault2(require("os")); - var exec = __importStar2(require_exec2()); + var exec3 = __importStar2(require_exec2()); var getWindowsInfo = () => __awaiter2(void 0, void 0, void 0, function* () { - const { stdout: version } = yield exec.getExecOutput('powershell -command "(Get-CimInstance -ClassName Win32_OperatingSystem).Version"', void 0, { + const { stdout: version } = yield exec3.getExecOutput('powershell -command "(Get-CimInstance -ClassName Win32_OperatingSystem).Version"', void 0, { silent: true }); - const { stdout: name } = yield exec.getExecOutput('powershell -command "(Get-CimInstance -ClassName Win32_OperatingSystem).Caption"', void 0, { + const { stdout: name } = yield exec3.getExecOutput('powershell -command "(Get-CimInstance -ClassName Win32_OperatingSystem).Caption"', void 0, { silent: true }); return { @@ -50725,7 +50725,7 @@ var require_platform2 = __commonJS({ }); var getMacOsInfo = () => __awaiter2(void 0, void 0, void 0, function* () { var _a, _b, _c, _d; - const { stdout } = yield exec.getExecOutput("sw_vers", void 0, { + const { stdout } = yield exec3.getExecOutput("sw_vers", void 0, { silent: true }); const version = (_b = (_a = stdout.match(/ProductVersion:\s*(.+)/)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : ""; @@ -50736,7 +50736,7 @@ var require_platform2 = __commonJS({ }; }); var getLinuxInfo = () => __awaiter2(void 0, void 0, void 0, function* () { - const { stdout } = yield exec.getExecOutput("lsb_release", ["-i", "-r", "-s"], { + const { stdout } = yield exec3.getExecOutput("lsb_release", ["-i", "-r", "-s"], { silent: true }); const [name, version] = stdout.trim().split("\n"); @@ -54169,7 +54169,7 @@ var require_cacheUtils = __commonJS({ exports2.getCacheVersion = getCacheVersion; exports2.getRuntimeToken = getRuntimeToken; var core12 = __importStar2(require_core()); - var exec = __importStar2(require_exec()); + var exec3 = __importStar2(require_exec()); var glob = __importStar2(require_glob()); var io4 = __importStar2(require_io()); var crypto2 = __importStar2(require("crypto")); @@ -54249,7 +54249,7 @@ var require_cacheUtils = __commonJS({ additionalArgs.push("--version"); core12.debug(`Checking ${app} ${additionalArgs.join(" ")}`); try { - yield exec.exec(`${app}`, additionalArgs, { + yield exec3.exec(`${app}`, additionalArgs, { ignoreReturnCode: true, silent: true, listeners: { @@ -103962,6 +103962,49 @@ function getActionsLogger() { // src/start-proxy.ts var core7 = __toESM(require_core()); +// src/artifact-scanner.ts +var exec = __toESM(require_exec()); +var GITHUB_PAT_CLASSIC_PATTERN = { + type: "Personal Access Token (Classic)" /* PersonalAccessClassic */, + pattern: /\bghp_[a-zA-Z0-9]{36}\b/g +}; +var GITHUB_PAT_FINE_GRAINED_PATTERN = { + type: "Personal Access Token (Fine-grained)" /* PersonalAccessFineGrained */, + pattern: /\bgithub_pat_[a-zA-Z0-9_]+\b/g +}; +var GITHUB_TOKEN_PATTERNS = [ + GITHUB_PAT_CLASSIC_PATTERN, + GITHUB_PAT_FINE_GRAINED_PATTERN, + { + type: "OAuth Access Token" /* OAuth */, + pattern: /\bgho_[a-zA-Z0-9]{36}\b/g + }, + { + type: "User-to-Server Token" /* UserToServer */, + pattern: /\bghu_[a-zA-Z0-9]{36}\b/g + }, + { + type: "Server-to-Server Token" /* ServerToServer */, + pattern: /\bghs_[a-zA-Z0-9]{36}\b/g + }, + { + type: "Refresh Token" /* Refresh */, + pattern: /\bghr_[a-zA-Z0-9]{36}\b/g + }, + { + type: "App Installation Access Token" /* AppInstallationAccess */, + pattern: /\bghs_[a-zA-Z0-9]{255}\b/g + } +]; +function isAuthToken(value, patterns = GITHUB_TOKEN_PATTERNS) { + for (const { type: type2, pattern } of patterns) { + if (pattern.test(value)) { + return type2; + } + } + return void 0; +} + // src/defaults.json var bundleVersion = "codeql-bundle-v2.23.9"; var cliVersion = "2.23.9"; @@ -104004,6 +104047,12 @@ function parseLanguage(language) { } return void 0; } +function isPAT(value) { + return isAuthToken(value, [ + GITHUB_PAT_CLASSIC_PATTERN, + GITHUB_PAT_FINE_GRAINED_PATTERN + ]); +} var LANGUAGE_TO_REGISTRY_TYPE = { java: ["maven_repository"], csharp: ["nuget_feed"], @@ -104065,6 +104114,11 @@ function getCredentials(logger, registrySecrets, registriesCredentials, language "Invalid credentials - fields must contain only printable characters" ); } + if (!isDefined(e.username) && (isDefined(e.password) && isPAT(e.password) || isDefined(e.token) && isPAT(e.token))) { + logger.warning( + `A ${e.type} private registry is configured for ${e.host || e.url} using a GitHub Personal Access Token (PAT), but no username was provided. This may not work correctly. When configuring a private registry using a PAT, select "Username and password" and enter the username of the user who generated the PAT.` + ); + } out.push({ type: e.type, host: e.host, diff --git a/src/start-proxy.test.ts b/src/start-proxy.test.ts index edd1377c00..80b05df4a6 100644 --- a/src/start-proxy.test.ts +++ b/src/start-proxy.test.ts @@ -7,7 +7,12 @@ import { KnownLanguage } from "./languages"; import { getRunnerLogger } from "./logging"; import * as startProxyExports from "./start-proxy"; import { parseLanguage } from "./start-proxy"; -import { setupTests } from "./testing-utils"; +import { + checkExpectedLogMessages, + getRecordingLogger, + makeTestToken, + setupTests, +} from "./testing-utils"; setupTests(test); @@ -174,6 +179,37 @@ test("getCredentials throws an error when non-printable characters are used", as } }); +test("getCredentials logs a warning when a PAT is used without a username", async (t) => { + const loggedMessages = []; + const logger = getRecordingLogger(loggedMessages); + const likelyWrongCredentials = toEncodedJSON([ + { + type: "git_server", + host: "https://github.com/", + password: `ghp_${makeTestToken()}`, + }, + ]); + + const results = startProxyExports.getCredentials( + logger, + undefined, + likelyWrongCredentials, + undefined, + ); + + // The configuration should be accepted, despite the likely problem. + t.assert(results); + t.is(results.length, 1); + t.is(results[0].type, "git_server"); + t.is(results[0].host, "https://github.com/"); + t.assert(results[0].password?.startsWith("ghp_")); + + // A warning should have been logged. + checkExpectedLogMessages(t, loggedMessages, [ + "using a GitHub Personal Access Token (PAT), but no username was provided", + ]); +}); + test("parseLanguage", async (t) => { // Exact matches t.deepEqual(parseLanguage("csharp"), KnownLanguage.csharp); diff --git a/src/start-proxy.ts b/src/start-proxy.ts index 2a082ed628..d14e07fca1 100644 --- a/src/start-proxy.ts +++ b/src/start-proxy.ts @@ -1,6 +1,7 @@ import * as core from "@actions/core"; import { getApiClient } from "./api-client"; +import * as artifactScanner from "./artifact-scanner"; import * as defaults from "./defaults.json"; import { KnownLanguage } from "./languages"; import { Logger } from "./logging"; @@ -62,6 +63,13 @@ export function parseLanguage(language: string): KnownLanguage | undefined { return undefined; } +function isPAT(value: string) { + return artifactScanner.isAuthToken(value, [ + artifactScanner.GITHUB_PAT_CLASSIC_PATTERN, + artifactScanner.GITHUB_PAT_FINE_GRAINED_PATTERN, + ]); +} + const LANGUAGE_TO_REGISTRY_TYPE: Partial> = { java: ["maven_repository"], csharp: ["nuget_feed"], @@ -161,6 +169,19 @@ export function getCredentials( ); } + // If the password or token looks like a GitHub PAT, warn if no username is configured. + if ( + !isDefined(e.username) && + ((isDefined(e.password) && isPAT(e.password)) || + (isDefined(e.token) && isPAT(e.token))) + ) { + logger.warning( + `A ${e.type} private registry is configured for ${e.host || e.url} using a GitHub Personal Access Token (PAT), but no username was provided. ` + + `This may not work correctly. When configuring a private registry using a PAT, select "Username and password" and enter the username of the user ` + + `who generated the PAT.`, + ); + } + out.push({ type: e.type, host: e.host,