Add capability to specify auth from env var or stdin

This commit adds two new ways of specifying GitHub auth:

1. from the GITHUB_TOKEN environment variable
2. from standard input

This commit does not include any documentation changes and the
descriptions of new command line options will need to be tweaked.
This commit is contained in:
Andrew Eisenberg 2021-02-12 14:31:38 -08:00 committed by Andrew Eisenberg
parent 1d92248672
commit 88714e3a60
9 changed files with 267 additions and 20 deletions

18
lib/runner.js generated
View file

@ -82,7 +82,8 @@ program
.description("Initializes CodeQL")
.requiredOption("--repository <repository>", "Repository name. (Required)")
.requiredOption("--github-url <url>", "URL of GitHub instance. (Required)")
.requiredOption("--github-auth <auth>", "GitHub Apps token or personal access token. (Required)")
.option("--github-auth <auth>", "GitHub Apps token or personal access token. This option is insecure and deprecated, please use `--github-auth-stdin` instead.")
.option("--github-auth-stdin", "Read GitHub Apps token or personal access token from stdin.")
.option("--external-repository-token <token>", "A token for fetching external config files and queries if they reside in a private repository.")
.option("--languages <languages>", "Comma-separated list of languages to analyze. Otherwise detects and analyzes all supported languages from the repo.")
.option("--queries <queries>", "Comma-separated list of additional queries to run. This overrides the same setting in a configuration file.")
@ -104,8 +105,9 @@ program
logger.info(`Cleaning temp directory ${tempDir}`);
fs.rmdirSync(tempDir, { recursive: true });
fs.mkdirSync(tempDir, { recursive: true });
const auth = await util_1.getGitHubAuth(logger, cmd.githubAuth, cmd.githubAuthStdin);
const apiDetails = {
auth: cmd.githubAuth,
auth,
externalRepoAuth: cmd.externalRepositoryToken,
url: util_1.parseGithubUrl(cmd.githubUrl),
};
@ -209,7 +211,8 @@ program
.requiredOption("--commit <commit>", "SHA of commit that was analyzed. (Required)")
.requiredOption("--ref <ref>", "Name of ref that was analyzed. (Required)")
.requiredOption("--github-url <url>", "URL of GitHub instance. (Required)")
.requiredOption("--github-auth <auth>", "GitHub Apps token or personal access token. (Required)")
.option("--github-auth <auth>", "GitHub Apps token or personal access token. This option is insecure and deprecated, please use `--github-auth-stdin` instead.")
.option("--github-auth-stdin", "Read GitHub Apps token or personal access token from stdin.")
.option("--checkout-path <path>", "Checkout path. Default is the current working directory.")
.option("--no-upload", "Do not upload results after analysis.")
.option("--output-dir <dir>", "Directory to output SARIF files to. Default is in the temp directory.")
@ -229,8 +232,9 @@ program
throw new Error("Config file could not be found at expected location. " +
"Was the 'init' command run with the same '--temp-dir' argument as this command.");
}
const auth = await util_1.getGitHubAuth(logger, cmd.githubAuth, cmd.githubAuthStdin);
const apiDetails = {
auth: cmd.githubAuth,
auth,
url: util_1.parseGithubUrl(cmd.githubUrl),
};
await analyze_1.runAnalyze(outputDir, util_1.getMemoryFlag(cmd.ram), util_1.getAddSnippetsFlag(cmd.addSnippets), util_1.getThreadsFlag(cmd.threads, logger), config, logger);
@ -254,13 +258,15 @@ program
.requiredOption("--commit <commit>", "SHA of commit that was analyzed. (Required)")
.requiredOption("--ref <ref>", "Name of ref that was analyzed. (Required)")
.requiredOption("--github-url <url>", "URL of GitHub instance. (Required)")
.requiredOption("--github-auth <auth>", "GitHub Apps token or personal access token. (Required)")
.option("--github-auth <auth>", "GitHub Apps token or personal access token. This option is insecure and deprecated, please use `--github-auth-stdin` instead.")
.option("--github-auth-stdin", "Read GitHub Apps token or personal access token from stdin.")
.option("--checkout-path <path>", "Checkout path. Default is the current working directory.")
.option("--debug", "Print more verbose output", false)
.action(async (cmd) => {
const logger = logging_1.getRunnerLogger(cmd.debug);
const auth = await util_1.getGitHubAuth(logger, cmd.githubAuth, cmd.githubAuthStdin);
const apiDetails = {
auth: cmd.githubAuth,
auth,
url: util_1.parseGithubUrl(cmd.githubUrl),
};
try {

File diff suppressed because one or more lines are too long

51
lib/util.js generated
View file

@ -257,4 +257,55 @@ function apiVersionInRange(version, minimumVersion, maximumVersion) {
return undefined;
}
exports.apiVersionInRange = apiVersionInRange;
/**
* Retrieves the github auth token for use with the runner. There are
* three possible locations for the token:
*
* 1. from the cli (considered insecure)
* 2. from stdin
* 3. from the GITHUB_TOKEN environment variable
*
* If both 1 & 2 are specified, then an error is thrown.
* If 1 & 3 or 2 & 3 are specified, then the environment variable is ignored.
*
* @param githubAuth a github app token or PAT
* @param fromStdIn read the github app token or PAT from stdin up to, but excluding the first whitespace
* @param readable the readable stream to use for getting the token (defaults to stdin)
*
* @return a promise resolving to the auth token.
*/
async function getGitHubAuth(logger, githubAuth, fromStdIn, readable = process.stdin) {
if (githubAuth && fromStdIn) {
throw new Error("Cannot specify both `--github-auth` and `--github-auth-stdin`. Please use `--github-auth-stdin`, which is more secure.");
}
if (githubAuth) {
logger.warning("Using `--github-auth` via the CLI is insecure. Use `--github-auth-stdin` instead.");
return githubAuth;
}
if (fromStdIn) {
return new Promise((resolve, reject) => {
let token = "";
readable.on("data", (data) => {
token += data.toString("utf8");
});
readable.on("end", () => {
token = token.split(/\s+/)[0].trim();
if (token) {
resolve(token);
}
else {
reject(new Error("Standard input is empty"));
}
});
readable.on("error", (err) => {
reject(err);
});
});
}
if (process.env.GITHUB_TOKEN) {
return process.env.GITHUB_TOKEN;
}
throw new Error("No GitHub authentication token was specified. Please provide a token via the GITHUB_TOKEN environment variable, or by adding the `--github-auth-stdin` flag and passing the token via standard input.");
}
exports.getGitHubAuth = getGitHubAuth;
//# sourceMappingURL=util.js.map

File diff suppressed because one or more lines are too long

31
lib/util.test.js generated
View file

@ -12,6 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
Object.defineProperty(exports, "__esModule", { value: true });
const fs = __importStar(require("fs"));
const os = __importStar(require("os"));
const stream = __importStar(require("stream"));
const github = __importStar(require("@actions/github"));
const ava_1 = __importDefault(require("ava"));
const sinon_1 = __importDefault(require("sinon"));
@ -172,4 +173,34 @@ ava_1.default("getGitHubVersion", async (t) => {
});
t.deepEqual({ type: util.GitHubVariant.DOTCOM }, v3);
});
ava_1.default("getGitHubAuth", async (t) => {
const msgs = [];
const mockLogger = {
warning: (msg) => msgs.push(msg),
};
// eslint-disable-next-line @typescript-eslint/no-floating-promises
t.throwsAsync(async () => util.getGitHubAuth(mockLogger, "abc", true));
process.env.GITHUB_TOKEN = "123";
t.is("123", await util.getGitHubAuth(mockLogger, undefined, undefined));
t.is(msgs.length, 0);
t.is("abc", await util.getGitHubAuth(mockLogger, "abc", undefined));
t.is(msgs.length, 1); // warning expected
msgs.length = 0;
await mockStdInForAuth(t, mockLogger, "def", "def");
await mockStdInForAuth(t, mockLogger, "def", "", "def");
await mockStdInForAuth(t, mockLogger, "def", "def\n some extra garbage", "ghi");
await mockStdInForAuth(t, mockLogger, "defghi", "def", "ghi\n123");
await mockStdInForAuthExpectError(t, mockLogger, "");
await mockStdInForAuthExpectError(t, mockLogger, "", " ", "abc");
await mockStdInForAuthExpectError(t, mockLogger, " def\n some extra garbage", "ghi");
t.is(msgs.length, 0);
});
async function mockStdInForAuth(t, mockLogger, expected, ...text) {
const stdin = stream.Readable.from(text);
t.is(expected, await util.getGitHubAuth(mockLogger, undefined, true, stdin));
}
async function mockStdInForAuthExpectError(t, mockLogger, ...text) {
const stdin = stream.Readable.from(text);
await t.throwsAsync(async () => util.getGitHubAuth(mockLogger, undefined, true, stdin));
}
//# sourceMappingURL=util.test.js.map

File diff suppressed because one or more lines are too long

View file

@ -20,6 +20,7 @@ import {
getMemoryFlag,
getThreadsFlag,
parseGithubUrl,
getGitHubAuth,
} from "./util";
const program = new Command();
@ -96,6 +97,7 @@ interface InitArgs {
repository: string;
githubUrl: string;
githubAuth: string;
githubAuthStdin: boolean;
externalRepositoryToken: string | undefined;
debug: boolean;
}
@ -105,9 +107,13 @@ program
.description("Initializes CodeQL")
.requiredOption("--repository <repository>", "Repository name. (Required)")
.requiredOption("--github-url <url>", "URL of GitHub instance. (Required)")
.requiredOption(
.option(
"--github-auth <auth>",
"GitHub Apps token or personal access token. (Required)"
"GitHub Apps token or personal access token. This option is insecure and deprecated, please use `--github-auth-stdin` instead."
)
.option(
"--github-auth-stdin",
"Read GitHub Apps token or personal access token from stdin."
)
.option(
"--external-repository-token <token>",
@ -153,8 +159,14 @@ program
fs.rmdirSync(tempDir, { recursive: true });
fs.mkdirSync(tempDir, { recursive: true });
const auth = await getGitHubAuth(
logger,
cmd.githubAuth,
cmd.githubAuthStdin
);
const apiDetails = {
auth: cmd.githubAuth,
auth,
externalRepoAuth: cmd.externalRepositoryToken,
url: parseGithubUrl(cmd.githubUrl),
};
@ -315,6 +327,7 @@ interface AnalyzeArgs {
ref: string;
githubUrl: string;
githubAuth: string;
githubAuthStdin: boolean;
checkoutPath: string | undefined;
upload: boolean;
outputDir: string | undefined;
@ -335,9 +348,13 @@ program
)
.requiredOption("--ref <ref>", "Name of ref that was analyzed. (Required)")
.requiredOption("--github-url <url>", "URL of GitHub instance. (Required)")
.requiredOption(
.option(
"--github-auth <auth>",
"GitHub Apps token or personal access token. (Required)"
"GitHub Apps token or personal access token. This option is insecure and deprecated, please use `--github-auth-stdin` instead."
)
.option(
"--github-auth-stdin",
"Read GitHub Apps token or personal access token from stdin."
)
.option(
"--checkout-path <path>",
@ -379,8 +396,14 @@ program
);
}
const auth = await getGitHubAuth(
logger,
cmd.githubAuth,
cmd.githubAuthStdin
);
const apiDetails = {
auth: cmd.githubAuth,
auth,
url: parseGithubUrl(cmd.githubUrl),
};
@ -421,6 +444,7 @@ interface UploadArgs {
commit: string;
ref: string;
githubUrl: string;
githubAuthStdin: boolean;
githubAuth: string;
checkoutPath: string | undefined;
debug: boolean;
@ -442,9 +466,13 @@ program
)
.requiredOption("--ref <ref>", "Name of ref that was analyzed. (Required)")
.requiredOption("--github-url <url>", "URL of GitHub instance. (Required)")
.requiredOption(
.option(
"--github-auth <auth>",
"GitHub Apps token or personal access token. (Required)"
"GitHub Apps token or personal access token. This option is insecure and deprecated, please use `--github-auth-stdin` instead."
)
.option(
"--github-auth-stdin",
"Read GitHub Apps token or personal access token from stdin."
)
.option(
"--checkout-path <path>",
@ -453,8 +481,13 @@ program
.option("--debug", "Print more verbose output", false)
.action(async (cmd: UploadArgs) => {
const logger = getRunnerLogger(cmd.debug);
const auth = await getGitHubAuth(
logger,
cmd.githubAuth,
cmd.githubAuthStdin
);
const apiDetails = {
auth: cmd.githubAuth,
auth,
url: parseGithubUrl(cmd.githubUrl),
};
try {

View file

@ -1,12 +1,13 @@
import * as fs from "fs";
import * as os from "os";
import * as stream from "stream";
import * as github from "@actions/github";
import test from "ava";
import test, { ExecutionContext } from "ava";
import sinon from "sinon";
import * as api from "./api-client";
import { getRunnerLogger } from "./logging";
import { getRunnerLogger, Logger } from "./logging";
import { setupTests } from "./testing-utils";
import * as util from "./util";
@ -244,3 +245,62 @@ test("getGitHubVersion", async (t) => {
});
t.deepEqual({ type: util.GitHubVariant.DOTCOM }, v3);
});
test("getGitHubAuth", async (t) => {
const msgs: string[] = [];
const mockLogger = ({
warning: (msg: string) => msgs.push(msg),
} as unknown) as Logger;
// eslint-disable-next-line @typescript-eslint/no-floating-promises
t.throwsAsync(async () => util.getGitHubAuth(mockLogger, "abc", true));
process.env.GITHUB_TOKEN = "123";
t.is("123", await util.getGitHubAuth(mockLogger, undefined, undefined));
t.is(msgs.length, 0);
t.is("abc", await util.getGitHubAuth(mockLogger, "abc", undefined));
t.is(msgs.length, 1); // warning expected
msgs.length = 0;
await mockStdInForAuth(t, mockLogger, "def", "def");
await mockStdInForAuth(t, mockLogger, "def", "", "def");
await mockStdInForAuth(
t,
mockLogger,
"def",
"def\n some extra garbage",
"ghi"
);
await mockStdInForAuth(t, mockLogger, "defghi", "def", "ghi\n123");
await mockStdInForAuthExpectError(t, mockLogger, "");
await mockStdInForAuthExpectError(t, mockLogger, "", " ", "abc");
await mockStdInForAuthExpectError(
t,
mockLogger,
" def\n some extra garbage",
"ghi"
);
t.is(msgs.length, 0);
});
async function mockStdInForAuth(
t: ExecutionContext<any>,
mockLogger: Logger,
expected: string,
...text: string[]
) {
const stdin = stream.Readable.from(text) as any;
t.is(expected, await util.getGitHubAuth(mockLogger, undefined, true, stdin));
}
async function mockStdInForAuthExpectError(
t: ExecutionContext<unknown>,
mockLogger: Logger,
...text: string[]
) {
const stdin = stream.Readable.from(text) as any;
await t.throwsAsync(async () =>
util.getGitHubAuth(mockLogger, undefined, true, stdin)
);
}

View file

@ -1,6 +1,7 @@
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { Readable } from "stream";
import * as core from "@actions/core";
import * as semver from "semver";
@ -311,3 +312,68 @@ export function apiVersionInRange(
}
return undefined;
}
/**
* Retrieves the github auth token for use with the runner. There are
* three possible locations for the token:
*
* 1. from the cli (considered insecure)
* 2. from stdin
* 3. from the GITHUB_TOKEN environment variable
*
* If both 1 & 2 are specified, then an error is thrown.
* If 1 & 3 or 2 & 3 are specified, then the environment variable is ignored.
*
* @param githubAuth a github app token or PAT
* @param fromStdIn read the github app token or PAT from stdin up to, but excluding the first whitespace
* @param readable the readable stream to use for getting the token (defaults to stdin)
*
* @return a promise resolving to the auth token.
*/
export async function getGitHubAuth(
logger: Logger,
githubAuth: string | undefined,
fromStdIn: boolean | undefined,
readable = process.stdin as Readable
): Promise<string> {
if (githubAuth && fromStdIn) {
throw new Error(
"Cannot specify both `--github-auth` and `--github-auth-stdin`. Please use `--github-auth-stdin`, which is more secure."
);
}
if (githubAuth) {
logger.warning(
"Using `--github-auth` via the CLI is insecure. Use `--github-auth-stdin` instead."
);
return githubAuth;
}
if (fromStdIn) {
return new Promise((resolve, reject) => {
let token = "";
readable.on("data", (data) => {
token += data.toString("utf8");
});
readable.on("end", () => {
token = token.split(/\s+/)[0].trim();
if (token) {
resolve(token);
} else {
reject(new Error("Standard input is empty"));
}
});
readable.on("error", (err) => {
reject(err);
});
});
}
if (process.env.GITHUB_TOKEN) {
return process.env.GITHUB_TOKEN;
}
throw new Error(
"No GitHub authentication token was specified. Please provide a token via the GITHUB_TOKEN environment variable, or by adding the `--github-auth-stdin` flag and passing the token via standard input."
);
}