2020-08-11 12:42:42 +01:00
import { Command } from 'commander' ;
2020-08-25 16:19:15 +01:00
import * as fs from 'fs' ;
import * as os from 'os' ;
2020-08-11 12:42:42 +01:00
import * as path from 'path' ;
2020-08-25 16:19:15 +01:00
import { runAnalyze } from './analyze' ;
2020-08-27 14:04:09 +01:00
import { determineAutobuildLanguage , runAutobuild } from './autobuild' ;
2020-08-25 16:19:15 +01:00
import { CodeQL , getCodeQL } from './codeql' ;
2020-08-27 14:22:16 +01:00
import { Config , getConfig } from './config-utils' ;
2020-08-25 16:19:15 +01:00
import { initCodeQL , initConfig , runInit } from './init' ;
2020-08-28 17:22:26 +01:00
import { Language , parseLanguage } from './languages' ;
2020-08-24 14:21:03 +01:00
import { getRunnerLogger } from './logging' ;
2020-08-11 12:42:42 +01:00
import { parseRepositoryNwo } from './repository' ;
import * as upload_lib from './upload-lib' ;
2020-09-01 14:13:10 +01:00
import { getMemoryFlag , getThreadsFlag } from './util' ;
2020-08-11 12:42:42 +01:00
const program = new Command ( ) ;
program . version ( '0.0.1' ) ;
2020-08-25 16:19:15 +01:00
function parseGithubUrl ( inputUrl : string ) : string {
2020-08-11 12:42:42 +01:00
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' ) {
2020-08-25 16:19:15 +01:00
return 'https://github.com' ;
2020-08-11 12:42:42 +01:00
}
2020-08-25 16:19:15 +01:00
// Remove the API prefix if it's present
if ( url . pathname . indexOf ( '/api/v3' ) !== - 1 ) {
url . pathname = url . pathname . substring ( 0 , url . pathname . indexOf ( '/api/v3' ) ) ;
2020-08-11 12:42:42 +01:00
}
return url . toString ( ) ;
} catch ( e ) {
throw new Error ( ` " ${ inputUrl } " is not a valid URL ` ) ;
}
}
2020-08-25 16:19:15 +01:00
function getTempDir ( userInput : string | undefined ) : string {
2020-08-28 09:27:28 +01:00
const tempDir = path . join ( userInput || process . cwd ( ) , 'codeql-runner' ) ;
2020-08-25 16:19:15 +01:00
if ( ! fs . existsSync ( tempDir ) ) {
fs . mkdirSync ( tempDir , { recursive : true } ) ;
}
return tempDir ;
}
2020-08-27 13:39:07 +01:00
function getToolsDir ( userInput : string | undefined ) : string {
const toolsDir = userInput || path . join ( os . homedir ( ) , 'codeql-runner-tools' ) ;
2020-08-25 16:19:15 +01:00
if ( ! fs . existsSync ( toolsDir ) ) {
fs . mkdirSync ( toolsDir , { recursive : true } ) ;
}
return toolsDir ;
}
2020-08-28 17:22:26 +01:00
const codeqlEnvJsonFilename = 'codeql-env.json' ;
// Imports the environment from codeqlEnvJsonFilename if not already present
function importTracerEnvironment ( config : Config ) {
if ( ! ( 'ODASA_TRACER_CONFIGURATION' in process . env ) ) {
const jsonEnvFile = path . join ( config . tempDir , codeqlEnvJsonFilename ) ;
const env = JSON . parse ( fs . readFileSync ( jsonEnvFile ) . toString ( 'utf-8' ) ) ;
Object . keys ( env ) . forEach ( key = > process . env [ key ] = env [ key ] ) ;
2020-08-27 14:22:16 +01:00
}
}
2020-09-02 18:00:46 +01:00
// Allow the user to specify refs in full refs/heads/branch format
// or just the short branch name and prepend "refs/heads/" to it.
function parseRef ( userInput : string ) : string {
if ( userInput . startsWith ( 'refs/' ) ) {
return userInput ;
} else {
return 'refs/heads/' + userInput ;
}
}
2020-08-25 16:19:15 +01:00
interface InitArgs {
languages : string | undefined ;
queries : string | undefined ;
configFile : string | undefined ;
codeqlPath : string | undefined ;
tempDir : string | undefined ;
toolsDir : string | undefined ;
checkoutPath : string | undefined ;
2020-08-27 10:46:26 +01:00
repository : string ;
2020-08-25 16:19:15 +01:00
githubUrl : string ;
githubAuth : string ;
2020-08-27 14:26:44 +01:00
debug : boolean ;
2020-08-25 16:19:15 +01:00
}
program
. command ( 'init' )
. description ( 'Initializes CodeQL' )
2020-08-28 09:35:16 +01:00
. 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 ( '--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.' )
. option ( '--config-file <file>' , 'Path to config file.' )
2020-08-25 16:19:15 +01:00
. option ( '--codeql-path <path>' , 'Path to a copy of the CodeQL CLI executable to use. Otherwise downloads a copy.' )
2020-08-28 09:35:16 +01:00
. option ( '--temp-dir <dir>' , 'Directory to use for temporary files. Default is "./codeql-runner".' )
. option ( '--tools-dir <dir>' , 'Directory to use for CodeQL tools and other files to store between runs. Default is a subdirectory of the home directory.' )
. option ( '--checkout-path <path>' , 'Checkout path. Default is the current working directory.' )
2020-08-27 14:26:44 +01:00
. option ( '--debug' , 'Print more verbose output' , false )
2020-08-25 16:19:15 +01:00
. action ( async ( cmd : InitArgs ) = > {
2020-08-27 14:26:44 +01:00
const logger = getRunnerLogger ( cmd . debug ) ;
2020-08-25 16:19:15 +01:00
try {
const tempDir = getTempDir ( cmd . tempDir ) ;
2020-08-27 13:39:07 +01:00
const toolsDir = getToolsDir ( cmd . toolsDir ) ;
2020-08-25 16:19:15 +01:00
// Wipe the temp dir
2020-08-27 16:45:41 +01:00
logger . info ( ` Cleaning temp directory ${ tempDir } ` ) ;
2020-08-25 16:19:15 +01:00
fs . rmdirSync ( tempDir , { recursive : true } ) ;
fs . mkdirSync ( tempDir , { recursive : true } ) ;
let codeql : CodeQL ;
if ( cmd . codeqlPath !== undefined ) {
codeql = getCodeQL ( cmd . codeqlPath ) ;
} else {
codeql = await initCodeQL (
undefined ,
cmd . githubAuth ,
parseGithubUrl ( cmd . githubUrl ) ,
tempDir ,
toolsDir ,
'runner' ,
logger ) ;
}
const config = await initConfig (
cmd . languages ,
cmd . queries ,
cmd . configFile ,
2020-08-27 10:46:26 +01:00
parseRepositoryNwo ( cmd . repository ) ,
2020-08-25 16:19:15 +01:00
tempDir ,
toolsDir ,
codeql ,
cmd . checkoutPath || process . cwd ( ) ,
cmd . githubAuth ,
parseGithubUrl ( cmd . githubUrl ) ,
logger ) ;
2020-08-26 13:20:46 +01:00
const tracerConfig = await runInit ( codeql , config ) ;
2020-08-27 16:34:09 +01:00
if ( tracerConfig === undefined ) {
return ;
}
2020-08-26 13:20:46 +01:00
2020-08-28 17:22:26 +01:00
// Always output a json file of the env that can be consumed programatically
const jsonEnvFile = path . join ( config . tempDir , codeqlEnvJsonFilename ) ;
fs . writeFileSync ( jsonEnvFile , JSON . stringify ( tracerConfig . env ) ) ;
2020-08-27 16:34:09 +01:00
if ( process . platform === 'win32' ) {
const batEnvFile = path . join ( config . tempDir , 'codeql-env.bat' ) ;
const batEnvFileContents = Object . entries ( tracerConfig . env )
. map ( ( [ key , value ] ) = > ` Set ${ key } = ${ value } ` )
. join ( '\n' ) ;
fs . writeFileSync ( batEnvFile , batEnvFileContents ) ;
2020-08-26 13:20:46 +01:00
2020-08-27 16:34:09 +01:00
const powershellEnvFile = path . join ( config . tempDir , 'codeql-env.sh' ) ;
const powershellEnvFileContents = Object . entries ( tracerConfig . env )
. map ( ( [ key , value ] ) = > ` $ env: ${ key } =" ${ value } " ` )
. join ( '\n' ) ;
fs . writeFileSync ( powershellEnvFile , powershellEnvFileContents ) ;
2020-08-26 13:20:46 +01:00
2020-08-28 17:22:26 +01:00
logger . info ( ` \ nCodeQL environment output to " ${ jsonEnvFile } ", " ${ batEnvFileContents } " and " ${ powershellEnvFile } ". ` +
2020-08-27 16:34:09 +01:00
` Please export these variables to future processes so the build can be traced. ` +
` If using cmd/batch run "call ${ batEnvFileContents } " ` +
` or if using PowerShell run "cat ${ powershellEnvFile } | Invoke-Expression". ` ) ;
2020-08-26 13:20:46 +01:00
2020-08-27 16:34:09 +01:00
} else {
// Assume that anything that's not windows is using a unix-style shell
2020-08-28 17:22:26 +01:00
const shEnvFile = path . join ( config . tempDir , 'codeql-env.sh' ) ;
const shEnvFileContents = Object . entries ( tracerConfig . env )
2020-08-27 16:34:09 +01:00
// Some vars contain ${LIB} that we do not want to be expanded when executing this script
. map ( ( [ key , value ] ) = > ` export ${ key } =" ${ value . replace ( '$' , '\\$' ) } " ` )
. join ( '\n' ) ;
2020-08-28 17:22:26 +01:00
fs . writeFileSync ( shEnvFile , shEnvFileContents ) ;
2020-08-27 16:34:09 +01:00
2020-08-28 17:22:26 +01:00
logger . info ( ` \ nCodeQL environment output to " ${ jsonEnvFile } " and " ${ shEnvFile } ". ` +
2020-08-27 16:34:09 +01:00
` Please export these variables to future processes so the build can be traced, ` +
2020-08-28 17:22:26 +01:00
` for example by running ". ${ shEnvFile } ". ` ) ;
2020-08-26 13:20:46 +01:00
}
2020-08-25 16:19:15 +01:00
} catch ( e ) {
logger . error ( 'Init failed' ) ;
logger . error ( e ) ;
process . exitCode = 1 ;
}
} ) ;
interface AutobuildArgs {
language : string ;
tempDir : string | undefined ;
2020-08-27 14:26:44 +01:00
debug : boolean ;
2020-08-25 16:19:15 +01:00
}
program
. command ( 'autobuild' )
. description ( 'Attempts to automatically build code' )
2020-08-28 09:35:16 +01:00
. option ( '--language <language>' , 'The language to build. Otherwise will detect the dominant compiled language.' )
. option ( '--temp-dir <dir>' , 'Directory to use for temporary files. Default is "./codeql-runner".' )
2020-08-27 14:26:44 +01:00
. option ( '--debug' , 'Print more verbose output' , false )
2020-08-25 16:19:15 +01:00
. action ( async ( cmd : AutobuildArgs ) = > {
2020-08-27 14:26:44 +01:00
const logger = getRunnerLogger ( cmd . debug ) ;
2020-08-25 16:19:15 +01:00
try {
2020-08-27 14:04:09 +01:00
const config = await getConfig ( getTempDir ( cmd . tempDir ) , logger ) ;
2020-08-28 09:43:25 +01:00
if ( config === undefined ) {
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." ) ;
}
2020-08-28 17:22:26 +01:00
importTracerEnvironment ( config ) ;
2020-08-27 14:04:09 +01:00
let language : Language | undefined = undefined ;
if ( cmd . language !== undefined ) {
language = parseLanguage ( cmd . language ) ;
if ( language === undefined || ! config . languages . includes ( language ) ) {
throw new Error ( ` " ${ cmd . language } " is not a recognised language. ` +
` Known languages in this project are ${ config . languages . join ( ', ' ) } . ` ) ;
}
} else {
language = determineAutobuildLanguage ( config , logger ) ;
}
if ( language !== undefined ) {
await runAutobuild ( language , config , logger ) ;
2020-08-25 16:19:15 +01:00
}
} catch ( e ) {
logger . error ( 'Autobuild failed' ) ;
logger . error ( e ) ;
process . exitCode = 1 ;
}
} ) ;
interface AnalyzeArgs {
repository : string ;
commit : string ;
ref : string ;
githubUrl : string ;
githubAuth : string ;
checkoutPath : string | undefined ;
upload : boolean ;
outputDir : string | undefined ;
2020-09-01 14:13:10 +01:00
ram : string | undefined ;
threads : string | undefined ;
2020-08-25 16:19:15 +01:00
tempDir : string | undefined ;
2020-08-27 14:26:44 +01:00
debug : boolean ;
2020-08-25 16:19:15 +01:00
}
program
. command ( 'analyze' )
. description ( 'Finishes extracting code and runs CodeQL queries' )
2020-08-28 09:35:16 +01:00
. requiredOption ( '--repository <repository>' , 'Repository name. (Required)' )
. 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 ( '--checkout-path <path>' , 'Checkout path. Default is the current working directory.' )
. option ( '--no-upload' , 'Do not upload results after analysis.' , false )
. option ( '--output-dir <dir>' , 'Directory to output SARIF files to. Default is in the temp directory.' )
2020-09-01 14:13:10 +01:00
. option ( '--ram <ram>' , 'Amount of memory to use when running queries. Default is to use all available memory.' )
. option ( '--threads <threads>' , 'Number of threads to use when running queries. ' +
'Default is to use all available cores.' )
2020-08-28 09:35:16 +01:00
. option ( '--temp-dir <dir>' , 'Directory to use for temporary files. Default is "./codeql-runner".' )
2020-08-27 14:26:44 +01:00
. option ( '--debug' , 'Print more verbose output' , false )
2020-08-25 16:19:15 +01:00
. action ( async ( cmd : AnalyzeArgs ) = > {
2020-08-27 14:26:44 +01:00
const logger = getRunnerLogger ( cmd . debug ) ;
2020-08-25 16:19:15 +01:00
try {
const tempDir = getTempDir ( cmd . tempDir ) ;
const outputDir = cmd . outputDir || path . join ( tempDir , 'codeql-sarif' ) ;
2020-08-27 14:04:09 +01:00
const config = await getConfig ( getTempDir ( cmd . tempDir ) , logger ) ;
2020-08-28 09:43:25 +01:00
if ( config === undefined ) {
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." ) ;
}
2020-08-25 16:19:15 +01:00
await runAnalyze (
parseRepositoryNwo ( cmd . repository ) ,
cmd . commit ,
2020-09-02 18:00:46 +01:00
parseRef ( cmd . ref ) ,
2020-08-25 16:19:15 +01:00
undefined ,
undefined ,
undefined ,
cmd . checkoutPath || process . cwd ( ) ,
undefined ,
cmd . githubAuth ,
parseGithubUrl ( cmd . githubUrl ) ,
cmd . upload ,
'runner' ,
outputDir ,
2020-09-01 14:13:10 +01:00
getMemoryFlag ( cmd . ram ) ,
getThreadsFlag ( cmd . threads , logger ) ,
2020-08-27 14:04:09 +01:00
config ,
2020-08-25 16:19:15 +01:00
logger ) ;
} catch ( e ) {
2020-08-27 16:34:09 +01:00
logger . error ( 'Analyze failed' ) ;
2020-08-25 16:19:15 +01:00
logger . error ( e ) ;
process . exitCode = 1 ;
}
} ) ;
interface UploadArgs {
sarifFile : string ;
repository : string ;
commit : string ;
ref : string ;
githubUrl : string ;
githubAuth : string ;
checkoutPath : string | undefined ;
2020-08-27 14:26:44 +01:00
debug : boolean ;
2020-08-25 16:19:15 +01:00
}
2020-08-11 12:42:42 +01:00
program
. command ( 'upload' )
. description ( 'Uploads a SARIF file, or all SARIF files from a directory, to code scanning' )
2020-08-28 09:35:16 +01:00
. requiredOption ( '--sarif-file <file>' , 'SARIF file to upload, or a directory containing multiple SARIF files. (Required)' )
. requiredOption ( '--repository <repository>' , 'Repository name. (Required)' )
. 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 ( '--checkout-path <path>' , 'Checkout path. Default is the current working directory.' )
2020-08-27 14:26:44 +01:00
. option ( '--debug' , 'Print more verbose output' , false )
2020-08-11 12:42:42 +01:00
. action ( async ( cmd : UploadArgs ) = > {
2020-08-27 14:26:44 +01:00
const logger = getRunnerLogger ( cmd . debug ) ;
2020-08-11 12:42:42 +01:00
try {
await upload_lib . upload (
cmd . sarifFile ,
parseRepositoryNwo ( cmd . repository ) ,
cmd . commit ,
2020-09-02 18:00:46 +01:00
parseRef ( cmd . ref ) ,
2020-08-11 13:56:23 +01:00
undefined ,
undefined ,
2020-08-11 12:42:42 +01:00
undefined ,
cmd . checkoutPath || process . cwd ( ) ,
2020-08-11 13:56:23 +01:00
undefined ,
2020-08-11 12:42:42 +01:00
cmd . githubAuth ,
2020-08-25 16:19:15 +01:00
parseGithubUrl ( cmd . githubUrl ) ,
2020-08-24 14:21:03 +01:00
'runner' ,
2020-08-11 12:42:42 +01:00
logger ) ;
} catch ( e ) {
2020-08-12 18:00:01 +01:00
logger . error ( 'Upload failed' ) ;
2020-08-11 12:42:42 +01:00
logger . error ( e ) ;
2020-08-12 18:00:01 +01:00
process . exitCode = 1 ;
2020-08-11 12:42:42 +01:00
}
} ) ;
program . parse ( process . argv ) ;