1+ import { EOL } from "os" ;
12import { spawn } from "child-process-promise" ;
23import * as child_process from "child_process" ;
34import { readFile } from "fs-extra" ;
@@ -26,6 +27,7 @@ import { Logger, ProgressReporter } from "./common";
2627import { CompilationMessage } from "./pure/legacy-messages" ;
2728import { sarifParser } from "./sarif-parser" ;
2829import { dbSchemeToLanguage , walkDirectory } from "./helpers" ;
30+ import { Credentials } from "./authentication" ;
2931
3032/**
3133 * The version of the SARIF format that we are using.
@@ -156,6 +158,10 @@ interface BqrsDecodeOptions {
156158 entities ?: string [ ] ;
157159}
158160
161+ export type OnLineCallback = (
162+ line : string ,
163+ ) => Promise < string | undefined > | string | undefined ;
164+
159165/**
160166 * This class manages a cli server started by `codeql execute cli-server` to
161167 * run commands without the overhead of starting a new java
@@ -304,6 +310,7 @@ export class CodeQLCliServer implements Disposable {
304310 command : string [ ] ,
305311 commandArgs : string [ ] ,
306312 description : string ,
313+ onLine ?: OnLineCallback ,
307314 ) : Promise < string > {
308315 const stderrBuffers : Buffer [ ] = [ ] ;
309316 if ( this . commandInProcess ) {
@@ -328,6 +335,22 @@ export class CodeQLCliServer implements Disposable {
328335 await new Promise < void > ( ( resolve , reject ) => {
329336 // Start listening to stdout
330337 process . stdout . addListener ( "data" , ( newData : Buffer ) => {
338+ if ( onLine ) {
339+ void ( async ( ) => {
340+ const response = await onLine ( newData . toString ( "utf-8" ) ) ;
341+
342+ if ( ! response ) {
343+ return ;
344+ }
345+
346+ process . stdin . write ( `${ response } ${ EOL } ` ) ;
347+
348+ // Remove newData from stdoutBuffers because the data has been consumed
349+ // by the onLine callback.
350+ stdoutBuffers . splice ( stdoutBuffers . indexOf ( newData ) , 1 ) ;
351+ } ) ( ) ;
352+ }
353+
331354 stdoutBuffers . push ( newData ) ;
332355 // If the buffer ends in '0' then exit.
333356 // We don't have to check the middle as no output will be written after the null until
@@ -487,13 +510,15 @@ export class CodeQLCliServer implements Disposable {
487510 * @param commandArgs The arguments to pass to the `codeql` command.
488511 * @param description Description of the action being run, to be shown in log and error messages.
489512 * @param progressReporter Used to output progress messages, e.g. to the status bar.
513+ * @param onLine Used for responding to interactive output on stdout/stdin.
490514 * @returns The contents of the command's stdout, if the command succeeded.
491515 */
492516 runCodeQlCliCommand (
493517 command : string [ ] ,
494518 commandArgs : string [ ] ,
495519 description : string ,
496520 progressReporter ?: ProgressReporter ,
521+ onLine ?: OnLineCallback ,
497522 ) : Promise < string > {
498523 if ( progressReporter ) {
499524 progressReporter . report ( { message : description } ) ;
@@ -503,10 +528,12 @@ export class CodeQLCliServer implements Disposable {
503528 // Construct the command that actually does the work
504529 const callback = ( ) : void => {
505530 try {
506- this . runCodeQlCliInternal ( command , commandArgs , description ) . then (
507- resolve ,
508- reject ,
509- ) ;
531+ this . runCodeQlCliInternal (
532+ command ,
533+ commandArgs ,
534+ description ,
535+ onLine ,
536+ ) . then ( resolve , reject ) ;
510537 } catch ( err ) {
511538 reject ( err ) ;
512539 }
@@ -522,12 +549,13 @@ export class CodeQLCliServer implements Disposable {
522549 }
523550
524551 /**
525- * Runs a CodeQL CLI command, returning the output as JSON.
552+ * Runs a CodeQL CLI command, parsing the output as JSON.
526553 * @param command The `codeql` command to be run, provided as an array of command/subcommand names.
527554 * @param commandArgs The arguments to pass to the `codeql` command.
528555 * @param description Description of the action being run, to be shown in log and error messages.
529556 * @param addFormat Whether or not to add commandline arguments to specify the format as JSON.
530557 * @param progressReporter Used to output progress messages, e.g. to the status bar.
558+ * @param onLine Used for responding to interactive output on stdout/stdin.
531559 * @returns The contents of the command's stdout, if the command succeeded.
532560 */
533561 async runJsonCodeQlCliCommand < OutputType > (
@@ -536,6 +564,7 @@ export class CodeQLCliServer implements Disposable {
536564 description : string ,
537565 addFormat = true ,
538566 progressReporter ?: ProgressReporter ,
567+ onLine ?: OnLineCallback ,
539568 ) : Promise < OutputType > {
540569 let args : string [ ] = [ ] ;
541570 if ( addFormat )
@@ -547,6 +576,7 @@ export class CodeQLCliServer implements Disposable {
547576 args ,
548577 description ,
549578 progressReporter ,
579+ onLine ,
550580 ) ;
551581 try {
552582 return JSON . parse ( result ) as OutputType ;
@@ -559,6 +589,67 @@ export class CodeQLCliServer implements Disposable {
559589 }
560590 }
561591
592+ /**
593+ * Runs a CodeQL CLI command with authentication, parsing the output as JSON.
594+ *
595+ * This method is intended for use with commands that accept a `--github-auth-stdin` argument. This
596+ * will be added to the command line arguments automatically if an access token is available.
597+ *
598+ * When the argument is given to the command, the CLI server will prompt for the access token on
599+ * stdin. This method will automatically respond to the prompt with the access token.
600+ *
601+ * There are a few race conditions that can potentially happen:
602+ * 1. The user logs in after the command has started. In this case, no access token will be given.
603+ * 2. The user logs out after the command has started. In this case, the user will be prompted
604+ * to login again. If they cancel the login, the old access token that was present before the
605+ * command was started will be used.
606+ *
607+ * @param command The `codeql` command to be run, provided as an array of command/subcommand names.
608+ * @param commandArgs The arguments to pass to the `codeql` command.
609+ * @param description Description of the action being run, to be shown in log and error messages.
610+ * @param addFormat Whether or not to add commandline arguments to specify the format as JSON.
611+ * @param progressReporter Used to output progress messages, e.g. to the status bar.
612+ * @returns The contents of the command's stdout, if the command succeeded.
613+ */
614+ async runJsonCodeQlCliCommandWithAuthentication < OutputType > (
615+ command : string [ ] ,
616+ commandArgs : string [ ] ,
617+ description : string ,
618+ addFormat = true ,
619+ progressReporter ?: ProgressReporter ,
620+ ) : Promise < OutputType > {
621+ const credentials = await Credentials . initialize ( ) ;
622+
623+ const accessToken = await credentials . getExistingAccessToken ( ) ;
624+
625+ const extraArgs = accessToken ? [ "--github-auth-stdin" ] : [ ] ;
626+
627+ return this . runJsonCodeQlCliCommand (
628+ command ,
629+ [ ...extraArgs , ...commandArgs ] ,
630+ description ,
631+ addFormat ,
632+ progressReporter ,
633+ async ( line ) => {
634+ if ( line . startsWith ( "Enter value for --github-auth-stdin" ) ) {
635+ try {
636+ return await credentials . getAccessToken ( ) ;
637+ } catch ( e ) {
638+ // If the user cancels the authentication prompt, we still need to give a value to the CLI.
639+ // By giving a potentially invalid value, the user will just get a 401/403 when they try to access a
640+ // private package and the access token is invalid.
641+ // This code path is very rare to hit. It would only be hit if the user is logged in when
642+ // starting the command, then logging out before the getAccessToken() is called again and
643+ // then cancelling the authentication prompt.
644+ return accessToken ;
645+ }
646+ }
647+
648+ return undefined ;
649+ } ,
650+ ) ;
651+ }
652+
562653 /**
563654 * Resolve the library path and dbscheme for a query.
564655 * @param workspaces The current open workspaces
@@ -1136,7 +1227,7 @@ export class CodeQLCliServer implements Disposable {
11361227 * @param packs The `<package-scope/name[@version]>` of the packs to download.
11371228 */
11381229 async packDownload ( packs : string [ ] ) {
1139- return this . runJsonCodeQlCliCommand (
1230+ return this . runJsonCodeQlCliCommandWithAuthentication (
11401231 [ "pack" , "download" ] ,
11411232 packs ,
11421233 "Downloading packs" ,
@@ -1148,7 +1239,7 @@ export class CodeQLCliServer implements Disposable {
11481239 if ( forceUpdate ) {
11491240 args . push ( "--mode" , "update" ) ;
11501241 }
1151- return this . runJsonCodeQlCliCommand (
1242+ return this . runJsonCodeQlCliCommandWithAuthentication (
11521243 [ "pack" , "install" ] ,
11531244 args ,
11541245 "Installing pack dependencies" ,
@@ -1169,7 +1260,7 @@ export class CodeQLCliServer implements Disposable {
11691260 ...this . getAdditionalPacksArg ( workspaceFolders ) ,
11701261 ] ;
11711262
1172- return this . runJsonCodeQlCliCommand (
1263+ return this . runJsonCodeQlCliCommandWithAuthentication (
11731264 [ "pack" , "bundle" ] ,
11741265 args ,
11751266 "Bundling pack" ,
@@ -1200,7 +1291,7 @@ export class CodeQLCliServer implements Disposable {
12001291 ) : Promise < { [ pack : string ] : string } > {
12011292 // Uses the default `--mode use-lock`, which creates the lock file if it doesn't exist.
12021293 const results : { [ pack : string ] : string } =
1203- await this . runJsonCodeQlCliCommand (
1294+ await this . runJsonCodeQlCliCommandWithAuthentication (
12041295 [ "pack" , "resolve-dependencies" ] ,
12051296 [ dir ] ,
12061297 "Resolving pack dependencies" ,
0 commit comments