diff --git a/lib/analyze-action-post.js b/lib/analyze-action-post.js index d0e16267ca..4a0177d48f 100644 --- a/lib/analyze-action-post.js +++ b/lib/analyze-action-post.js @@ -125447,29 +125447,35 @@ 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 = [ + GITHUB_PAT_CLASSIC_PATTERN, + GITHUB_PAT_FINE_GRAINED_PATTERN, { - name: "Personal Access Token", - pattern: /\bghp_[a-zA-Z0-9]{36}\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 } ]; @@ -125477,13 +125483,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 272e6ee6f1..999dd402d7 100644 --- a/lib/init-action-post.js +++ b/lib/init-action-post.js @@ -130109,29 +130109,35 @@ 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 = [ + GITHUB_PAT_CLASSIC_PATTERN, + GITHUB_PAT_FINE_GRAINED_PATTERN, { - name: "Personal Access Token", - pattern: /\bghp_[a-zA-Z0-9]{36}\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 } ]; @@ -130139,13 +130145,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 6d42e77685..163f3dbbba 100644 --- a/lib/start-proxy-action-post.js +++ b/lib/start-proxy-action-post.js @@ -124387,29 +124387,35 @@ 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 = [ + GITHUB_PAT_CLASSIC_PATTERN, + GITHUB_PAT_FINE_GRAINED_PATTERN, { - name: "Personal Access Token", - pattern: /\bghp_[a-zA-Z0-9]{36}\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 } ]; @@ -124417,13 +124423,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/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/lib/upload-sarif-action-post.js b/lib/upload-sarif-action-post.js index c7e1156f3e..0d6e2e9845 100644 --- a/lib/upload-sarif-action-post.js +++ b/lib/upload-sarif-action-post.js @@ -124372,29 +124372,35 @@ 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 = [ + GITHUB_PAT_CLASSIC_PATTERN, + GITHUB_PAT_FINE_GRAINED_PATTERN, { - name: "Personal Access Token", - pattern: /\bghp_[a-zA-Z0-9]{36}\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 } ]; @@ -124402,13 +124408,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.test.ts b/src/artifact-scanner.test.ts index 5678d2cadd..137738201c 100644 --- a/src/artifact-scanner.test.ts +++ b/src/artifact-scanner.test.ts @@ -4,37 +4,121 @@ import * as path from "path"; import test from "ava"; -import { scanArtifactsForTokens } from "./artifact-scanner"; +import { + GITHUB_PAT_CLASSIC_PATTERN, + isAuthToken, + scanArtifactsForTokens, + TokenType, +} from "./artifact-scanner"; import { getRunnerLogger } from "./logging"; -import { getRecordingLogger, LoggedMessage } from "./testing-utils"; +import { + checkExpectedLogMessages, + getRecordingLogger, + LoggedMessage, + makeTestToken, +} 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-")); +test("makeTestToken", (t) => { + t.is(makeTestToken().length, 36); + t.is(makeTestToken(255).length, 255); +}); - 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("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); - const error = await t.throwsAsync( - async () => await scanArtifactsForTokens([testFile], logger), - ); + // 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, + ); - 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 }); - } + // 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, + 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)}`, + }, +]; + +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); const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "scanner-test-")); diff --git a/src/artifact-scanner.ts b/src/artifact-scanner.ts index d04445bf4d..329ced0cba 100644 --- a/src/artifact-scanner.ts +++ b/src/artifact-scanner.ts @@ -7,33 +7,62 @@ 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; +} + +/** 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 = [ +const GITHUB_TOKEN_PATTERNS: TokenPattern[] = [ + GITHUB_PAT_CLASSIC_PATTERN, + GITHUB_PAT_FINE_GRAINED_PATTERN, { - name: "Personal Access Token", - pattern: /\bghp_[a-zA-Z0-9]{36}\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, }, ]; @@ -48,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. * @@ -65,13 +112,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}`); } } 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, 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); +}