Skip to content

Commit

Permalink
Add a CLI interface to the upload-sarif action
Browse files Browse the repository at this point in the history
  • Loading branch information
Robert Brignull committed Aug 11, 2020
1 parent bcf676e commit 6d7a135
Show file tree
Hide file tree
Showing 15 changed files with 356 additions and 90 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/cli/

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"description": "CodeQL action",
"scripts": {
"build": "tsc",
"build-cli": "webpack --mode production",
"test": "ava src/** --serial --verbose",
"lint": "tslint -p . -c tslint.json 'src/**/*.ts'",
"removeNPMAbsolutePaths": "removeNPMAbsolutePaths . --force"
Expand All @@ -23,6 +24,7 @@
"@actions/github": "^2.2.0",
"@actions/http-client": "^1.0.8",
"@actions/tool-cache": "^1.5.5",
"commander": "6.0.0",
"console-log-level": "^1.4.1",
"file-url": "^3.0.0",
"fs": "0.0.1-security",
Expand Down Expand Up @@ -52,6 +54,9 @@
"sinon": "^9.0.2",
"tslint": "^6.1.0",
"tslint-eslint-rules": "^5.4.0",
"typescript": "^3.7.5"
"ts-loader": "8.0.2",
"typescript": "^3.7.5",
"webpack": "4.44.1",
"webpack-cli": "3.3.12"
}
}
35 changes: 32 additions & 3 deletions src/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,45 @@ import * as core from "@actions/core";
import * as github from "@actions/github";
import consoleLogLevel from "console-log-level";

import { isLocalRun } from "./util";
import { getRequiredEnvParam, isLocalRun } from "./util";

export const getApiClient = function(allowLocalRun = false) {
export const getApiClient = function(githubAuth: string, githubApiUrl: string, allowLocalRun = false) {
if (isLocalRun() && !allowLocalRun) {
throw new Error('Invalid API call in local run');
}
return new github.GitHub(
core.getInput('token'),
{
auth: parseAuth(githubAuth),
baseUrl: githubApiUrl,
userAgent: "CodeQL Action",
log: consoleLogLevel({ level: "debug" })
});
};

// Parses the user input as either a single token,
// or a username and password / PAT.
function parseAuth(auth: string): string {
// Check if it's a username:password pair
const c = auth.indexOf(':');
if (c !== -1) {
return 'basic ' + Buffer.from(auth).toString('base64');
// return {
// username: auth.substring(0, c),
// password: auth.substring(c + 1),
// on2fa: async () => { throw new Error(''); }
// };
}

// Otherwise use the token as it is
return auth;
}

// Temporary function to aid in the transition to running on and off of github actions.
// Once all code has been coverted this function should be removed or made canonical
// and called only from the action entrypoints.
export function getActionsApiClient(allowLocalRun = false) {
return getApiClient(
core.getInput('token'),
getRequiredEnvParam('GITHUB_API_URL'),
allowLocalRun);
}
82 changes: 82 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Command } from 'commander';
import * as path from 'path';

import { getCLILogger } from './logging';
import { parseRepositoryNwo } from './repository';
import * as upload_lib from './upload-lib';

const program = new Command();
program.version('0.0.1');

interface UploadArgs {
sarifFile: string;
repository: string;
commit: string;
ref: string;
analysisKey: string;
githubUrl: string;
githubAuth: string;
analysisName: string | undefined;
checkoutPath: string | undefined;
environment: string | undefined;
}

function parseGithubApiUrl(inputUrl: string): string {
try {
const url = new URL(inputUrl);

// If we detect this is trying to be to github.com
// then return with a fixed canonical URL.
if (url.hostname === 'github.com' || url.hostname === 'api.github.com') {
return 'https://api.github.com';
}

// Add the API path if it's not already present.
if (url.pathname.indexOf('/api/v3') === -1) {
url.pathname = path.join(url.pathname, 'api', 'v3');
}

return url.toString();

} catch (e) {
throw new Error(`"${inputUrl}" is not a valid URL`);
}
}

program
.command('upload')
.description('Uploads a SARIF file, or all SARIF files from a directory, to code scanning')
.requiredOption('--sarif-file <file>', 'SARIF file to upload')
.requiredOption('--repository <repository>', 'Repository name')
.requiredOption('--commit <commit>', 'SHA of commit that was analyzed')
.requiredOption('--ref <ref>', 'Name of ref that was analyzed')
.requiredOption('--analysis-key <key>', 'Identifies the analysis, for use matching up equivalent analyses on different commits')
.requiredOption('--github-url <url>', 'URL of GitHub instance')
.requiredOption('--github-auth <auth>', 'GitHub Apps token, or of the form "username:token" if using a personal access token')
.option('--checkout-path <path>', 'Checkout path (default: current working directory)')
.option('--analysis-name <name>', 'Display name of the analysis (default: same as analysis-key')
.option('--environment <env>', 'Environment (default: empty)')
.action(async (cmd: UploadArgs) => {
const logger = getCLILogger();
try {
await upload_lib.upload(
cmd.sarifFile,
parseRepositoryNwo(cmd.repository),
cmd.commit,
cmd.ref,
cmd.analysisKey,
cmd.analysisName || cmd.analysisKey,
undefined,
cmd.checkoutPath || process.cwd(),
cmd.environment,
cmd.githubAuth,
parseGithubApiUrl(cmd.githubUrl),
'cli',
logger);
} catch (e) {
logger.error("Upload failed");
logger.error(e);
}
});

program.parse(process.argv);
2 changes: 1 addition & 1 deletion src/codeql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ async function getCodeQLBundleDownloadURL(): Promise<string> {
}
let [repositoryOwner, repositoryName] = repository.split("/");
try {
const release = await api.getApiClient().repos.getReleaseByTag({
const release = await api.getActionsApiClient().repos.getReleaseByTag({
owner: repositoryOwner,
repo: repositoryName,
tag: CODEQL_BUNDLE_VERSION
Expand Down
4 changes: 2 additions & 2 deletions src/config-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ async function getLanguagesInRepo(): Promise<Language[]> {
let repo = repo_nwo[1];

core.debug(`GitHub repo ${owner} ${repo}`);
const response = await api.getApiClient(true).repos.listLanguages({
const response = await api.getActionsApiClient(true).repos.listLanguages({
owner,
repo
});
Expand Down Expand Up @@ -689,7 +689,7 @@ async function getRemoteConfig(configFile: string): Promise<UserConfig> {
throw new Error(getConfigFileRepoFormatInvalidMessage(configFile));
}

const response = await api.getApiClient(true).repos.getContents({
const response = await api.getActionsApiClient(true).repos.getContents({
owner: pieces.groups.owner,
repo: pieces.groups.repo,
path: pieces.groups.path,
Expand Down
17 changes: 16 additions & 1 deletion src/finalize-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import * as path from 'path';

import { getCodeQL, isScannedLanguage } from './codeql';
import * as configUtils from './config-utils';
import { getActionsLogger } from './logging';
import { parseRepositoryNwo } from './repository';
import * as sharedEnv from './shared-environment';
import * as upload_lib from './upload-lib';
import * as util from './util';
Expand Down Expand Up @@ -144,7 +146,20 @@ async function run() {
queriesStats = await runQueries(databaseFolder, sarifFolder, config);

if ('true' === core.getInput('upload')) {
uploadStats = await upload_lib.upload(sarifFolder);
uploadStats = await upload_lib.upload(
sarifFolder,
parseRepositoryNwo(util.getRequiredEnvParam('GITHUB_REPOSITORY')),
await util.getCommitOid(),
util.getRef(),
await util.getAnalysisKey(),
util.getRequiredEnvParam('GITHUB_WORKFLOW'),
util.getWorkflowRunID(),
core.getInput('checkout_path'),
core.getInput('matrix'),
core.getInput('token'),
util.getRequiredEnvParam('GITHUB_API_URL'),
'actions',
getActionsLogger());
}

} catch (error) {
Expand Down
26 changes: 26 additions & 0 deletions src/logging.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as core from '@actions/core';

export interface Logger {
debug: (message: string) => void;
info: (message: string) => void;
warning: (message: string) => void;
error: (message: string) => void;

startGroup: (name: string) => void;
endGroup: () => void;
}

export function getActionsLogger(): Logger {
return core;
}

export function getCLILogger(): Logger {
return {
debug: console.debug,
info: console.info,
warning: console.warn,
error: console.error,
startGroup: () => undefined,
endGroup: () => undefined,
};
}
16 changes: 16 additions & 0 deletions src/repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// A repository name with owner, parsed into its two parts
export interface RepositoryNwo {
owner: string;
repo: string;
}

export function parseRepositoryNwo(input: string): RepositoryNwo {
const parts = input.split('/');
if (parts.length !== 2) {
throw new Error(`"${input}" is not a valid repository name`);
}
return {
owner: parts[0],
repo: parts[1],
};
}
4 changes: 4 additions & 0 deletions src/testing-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export function setupTests(test: TestInterface<any>) {
// process.env only has strings fields, so a shallow copy is fine.
t.context.env = {};
Object.assign(t.context.env, process.env);

// Any test that runs code that expects to only be run on actions
// will depend on various environment variables.
process.env['GITHUB_API_URL'] = 'https://github.localhost/api/v3';
});

typedTest.afterEach.always(t => {
Expand Down
5 changes: 3 additions & 2 deletions src/upload-lib.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import test from 'ava';

import { getCLILogger } from './logging';
import {setupTests} from './testing-utils';
import * as uploadLib from './upload-lib';

setupTests(test);

test('validateSarifFileSchema - valid', t => {
const inputFile = __dirname + '/../src/testdata/valid-sarif.sarif';
t.notThrows(() => uploadLib.validateSarifFileSchema(inputFile));
t.notThrows(() => uploadLib.validateSarifFileSchema(inputFile, getCLILogger()));
});

test('validateSarifFileSchema - invalid', t => {
const inputFile = __dirname + '/../src/testdata/invalid-sarif.sarif';
t.throws(() => uploadLib.validateSarifFileSchema(inputFile));
t.throws(() => uploadLib.validateSarifFileSchema(inputFile, getCLILogger()));
});
Loading

0 comments on commit 6d7a135

Please sign in to comment.