import { execSync } from 'child_process' ; import crypto from 'crypto' ; import { ComponentDetails , IAqlSearchResult , IArtifact , IDetailsResponse , IGraphRequestModel , IGraphResponse , ISummaryRequestModel , ISummaryResponse , IUsageFeature , JfrogClient } from 'jfrog-client-js' ; import * as keytar from 'keytar' ; import * as semver from 'semver' ; import Set from 'typescript-collections/dist/lib/Set' ; import * as vscode from 'vscode' ; import { ExtensionComponent } from '../extensionComponent' ; import { LogManager } from '../log/logManager' ; import { ConnectionUtils } from './connectionUtils' ; /**
 * Manage the Xray credentials and perform connection with Xray server.
 */ export class ConnectionManager implements ExtensionComponent { private static readonly XRAY_USERNAME_KEY : string = 'jfrog.xray.username' ; private static readonly PLATFORM_URL_KEY : string = 'jfrog.xray.platformUrl' ; private static readonly XRAY_URL_KEY : string = 'jfrog.xray.url' ; private static readonly RT_URL_KEY : string = 'jfrog.rt.url' ; private static readonly SERVICE_ID : string = 'com.jfrog.xray.vscode' ; private static readonly ACCESS_TOKEN_FS_KEY : string = 'vscode_jfrog_token' ; public static readonly STORE_CONNECTION_ENV : string = 'JFROG_IDE_STORE_CONNECTION' ; public static readonly USERNAME_ENV : string = 'JFROG_IDE_USERNAME' ; public static readonly PASSWORD_ENV : string = 'JFROG_IDE_PASSWORD' ; public static readonly ACCESS_TOKEN_ENV : string = 'JFROG_IDE_ACCESS_TOKEN' ; public static readonly URL_ENV : string = 'JFROG_IDE_URL' ; private static readonly MINIMAL_JFROG_CLI_VERSION_FOR_DEFAULT_EXPORT : any = semver . coerce ( '2.6.1' ) ; private _context ! : vscode . ExtensionContext ; private _accessToken : string = '' ; private _username : string = '' ; private _password : string = '' ; private _xrayUrl : string = '' ; private _rtUrl : string = '' ; private _url : string = '' ; constructor ( private _logManager ) { } public async activate ( context ) { this . _context = context ; await this . populateCredentials ( false ) ; this . updateConnectionIcon ( ) ; return this ; } public async connect ( ) { if ( await this . populateCredentials ( true ) ) { this . updateConnectionIcon ( ) ; return true ; } return false ; } public async disconnect ( ) { return await vscode . window . withProgress ( < vscode . ProgressOptions > { location : vscode . ProgressLocation . Window , title : 'Delete Xray connection details...' } , async ( ) : Promise < boolean > => { await this . deleteCredentialsFromFileSystem ( ) ; this . deleteCredentialsFromMemory ( ) ; this . updateConnectionIcon ( ) ; return true ; } ) ; } public areXrayCredentialsSet ( ) { return ! ! ( ( this . _url || this . _xrayUrl ) && ( ( this . _username && this . _password ) || this . _accessToken ) ) ; } public areCompleteCredentialsSet ( ) { return ! ! ( this . _xrayUrl && this . _rtUrl && ( ( this . _username && this . _password ) || this . _accessToken ) ) ; } param prompt returns public async populateCredentials ( prompt ) { return ( ( await this . tryCredentialsFromKeyStore ( ) ) || ( await this . tryCredentialsFromEnv ( ) ) || ( await this . tryCredentialsFromJfrogCli ( ) ) || ( prompt && ( await this . tryCredentialsFromPrmopt ( ) ) ) ) ; } private async verifyNewCredentials ( prompt ) { if ( ! this . areXrayCredentialsSet ( ) ) { return false ; } if ( ! ( await ConnectionUtils . checkXrayConnectionAndPermissions ( this . _xrayUrl , this . _username , this . _password , this . _accessToken , prompt , this . _logManager ) ) ) { return false ; } if ( this . areCompleteCredentialsSet ( ) ) { return ConnectionUtils . checkArtifactoryConnection ( this . _rtUrl , this . _username , this . _password , this . _accessToken , prompt , this . _logManager ) ; } return true ; } private async promptAll ( ) { if ( ! ( await this . promptUrls ( ) ) ) { return false ; } if ( await this . promptAccessToken ( ) ) { return true ; } return ( await this . promptUsername ( ) ) && ( await this . promptPassword ( ) ) ; } public async readCredentialsFromJfrogCli ( ) { if ( ! ( await this . verifyJfrogCliInstalledAndVersion ( ) ) ) { return false ; } return await this . getJfrogCliDefaultServerConfiguration ( ) ; } private async verifyJfrogCliInstalledAndVersion ( ) { try { let version = await this . getJfrogCliVersion ( ) ; if ( ! version ) { return false ; } let cliSemver = new semver . SemVer ( version ) ; if ( cliSemver . compare ( ConnectionManager . MINIMAL_JFROG_CLI_VERSION_FOR_DEFAULT_EXPORT ) < 0 ) { this . _logManager . logMessage ( 'JFrog CLI version is too low to support credentials extraction (needed: ' + ConnectionManager . MINIMAL_JFROG_CLI_VERSION_FOR_DEFAULT_EXPORT + ', actual: ' + version + ')' , 'DEBUG' ) ; return false ; } } catch ( error ) { this . _logManager . logMessage ( 'Unexpected error when trying to read credentials from JFrog CLI: ' + error , 'DEBUG' ) ; return false ; } return true ; } private async getJfrogCliVersion ( ) { try { const versionPrefix = 'jf version ' ; let output = execSync ( 'jf -v' ) . toString ( ) ; if ( ! output . startsWith ( versionPrefix ) ) { this . _logManager . logMessage ( 'Unexpected output to JFrog CLI version command: ' + output , 'DEBUG' ) ; return '' ; } return output . replace ( versionPrefix , '' ) . trim ( ) ; } catch ( error ) { this . _logManager . logMessage ( 'Could not find a JFrog CLI installation: ' + error , 'DEBUG' ) ; return '' ; } } private async getJfrogCliDefaultServerConfiguration ( ) { try { let output = execSync ( 'jf c export' ) . toString ( ) ; let confStr = Buffer . from ( output , 'base64' ) . toString ( 'ascii' ) ; let conf = JSON . parse ( confStr ) ; this . _url = conf [ 'url' ] || '' ; this . _xrayUrl = conf [ 'xrayUrl' ] || '' ; this . _rtUrl = conf [ 'artifactoryUrl' ] || '' ; const username = conf [ 'user' ] || '' ; const password = conf [ 'password' ] || '' ; if ( username !== '' && password !== '' ) { this . _username = username ; this . _password = password ; } else { this . _accessToken = conf [ 'accessToken' ] || '' ; } if ( this . areCompleteCredentialsSet ( ) ) { this . _logManager . logMessage ( 'Successfuly obtained credentials from JFrog CLI' , 'DEBUG' ) ; return true ; } return false ; } catch ( error ) { this . _logManager . logMessage ( 'Error encountered while reading credentials from JFrog CLI: ' + error , 'DEBUG' ) ; return false ; } } public get url ( ) { return this . _url ; } public get xrayUrl ( ) { return this . _xrayUrl ; } public get rtUrl ( ) { return this . _rtUrl ; } public get username ( ) { return this . _username ; } public get password ( ) { return this . _password ; } public get accessToken ( ) { return this . _accessToken ; } /**
     * Resolve Xray and JFrog platform URLs from the input url.
     * If URL is <platform-url>, the derived Xray URL is <platform-url/xray>, and the Artifactory URL is <platform-url/artifactory>.
     * If URL is <platform-url>/xray the derived platform URL is <platform-url>.
     * If URL leads to an Xray URL not under JFrog platform (like in Artifactory 6), leave the derive platform URL empty.
     */ private async resolveUrls ( ) { if ( await ConnectionUtils . isPlatformUrl ( this . _url , this . _username , this . _password , this . _accessToken ) ) { this . _xrayUrl = this . getXrayUrlFromPlatform ( ) ; this . _rtUrl = this . getRtUrlFromPlatform ( ) ; } else if ( this . _url ) { this . _xrayUrl = this . _url ; if ( this . _url . endsWith ( '/xray' ) || this . _url . endsWith ( '/xray/' ) ) { this . _url = this . _url . substring ( 0 , this . _url . lastIndexOf ( '/xray' ) ) ; this . _rtUrl = this . getRtUrlFromPlatform ( ) ; if ( ! ( await ConnectionUtils . validateArtifactoryConnection ( this . _rtUrl , this . _username , this . _password , this . _accessToken ) ) ) { this . _url = '' ; this . _rtUrl = '' ; } } else { this . _url = '' ; this . _rtUrl = '' ; } } this . _logManager . logMessage ( 'Resolved JFrog platform URL: ' + this . _url , 'DEBUG' ) ; this . _logManager . logMessage ( 'Resolved Xray URL: ' + this . _xrayUrl , 'DEBUG' ) ; this . _logManager . logMessage ( 'Resolved Artifactory URL: ' + this . _rtUrl , 'DEBUG' ) ; } private getServiceUrlFromPlatform ( platformUrl , service ) { platformUrl += platformUrl . endsWith ( '/' ) ? '' : '/' ; return platformUrl + service ; } private getRtUrlFromPlatform ( ) { return this . getServiceUrlFromPlatform ( this . _url , 'artifactory' ) ; } private getXrayUrlFromPlatform ( ) { return this . getServiceUrlFromPlatform ( this . _url , 'xray' ) ; } private async tryCredentialsFromKeyStore ( ) { this . _logManager . logMessage ( 'Trying to read credentials from KeyStore...' , 'DEBUG' ) ; const credentialsSet = ( await this . setUrlsFromFilesystem ( ) ) && ( ( ( await this . setUsernameFromFilesystem ( ) ) && ( await this . setPasswordFromKeyStore ( ) ) ) || ( await this . setAccessTokenFromKeyStore ( ) ) ) ; if ( ! credentialsSet ) { this . deleteCredentialsFromMemory ( ) ; return false ; } await this . resolveUrls ( ) ; return true ; } private async tryCredentialsFromEnv ( ) { if ( ! ( await this . getCredentialsFromEnv ( ) ) ) { this . deleteCredentialsFromMemory ( ) ; return false ; } if ( ! ( await this . verifyNewCredentials ( false ) ) ) { this . deleteCredentialsFromMemory ( ) ; return false ; } if ( process . env [ ConnectionManager . STORE_CONNECTION_ENV ] ?. toUpperCase ( ) === 'TRUE' ) { await this . storeConnection ( ) ; } return true ; } private async tryCredentialsFromJfrogCli ( ) { this . _logManager . logMessage ( 'Trying to read credentials from JFrog CLI...' , 'DEBUG' ) ; if ( ! ( await this . readCredentialsFromJfrogCli ( ) ) ) { this . deleteCredentialsFromMemory ( ) ; return false ; } if ( ! ( await this . verifyNewCredentials ( false ) ) ) { this . deleteCredentialsFromMemory ( ) ; return false ; } await this . storeConnection ( ) ; return true ; } private async tryCredentialsFromPrmopt ( ) { this . _logManager . logMessage ( 'Prompting for credentials...' , 'DEBUG' ) ; if ( ! ( await this . promptAll ( ) ) ) { return false ; } const valid = await vscode . window . withProgress ( < vscode . ProgressOptions > { location : vscode . ProgressLocation . Window , title : 'Checking connection with Xray server...' } , async ( ) : Promise < boolean > => { return await this . verifyNewCredentials ( true ) ; } ) ; if ( ! valid ) { this . deleteCredentialsFromMemory ( ) ; return false ; } await this . storeConnection ( ) ; return true ; } public async getCredentialsFromEnv ( ) { this . _logManager . logMessage ( 'Trying to read credentials from env...' , 'DEBUG' ) ; this . _url = process . env [ ConnectionManager . URL_ENV ] || '' ; this . _username = process . env [ ConnectionManager . USERNAME_ENV ] || '' ; this . _password = process . env [ ConnectionManager . PASSWORD_ENV ] || '' ; this . _accessToken = process . env [ ConnectionManager . ACCESS_TOKEN_ENV ] || '' ; let credentialsSet = this . areXrayCredentialsSet ( ) ; if ( ! credentialsSet ) { return false ; } await this . resolveUrls ( ) ; return true ; } private async setUrlsFromFilesystem ( ) { this . _url = ( await this . _context . globalState . get ( ConnectionManager . PLATFORM_URL_KEY ) ) || '' ; this . _xrayUrl = ( await this . _context . globalState . get ( ConnectionManager . XRAY_URL_KEY ) ) || '' ; this . _rtUrl = ( await this . _context . globalState . get ( ConnectionManager . RT_URL_KEY ) ) || '' ; return ! ! this . _url || ! ! this . _xrayUrl ; } private async promptUrls ( ) { await this . promptPlatformUrl ( ) ; await this . promptArtifactoryUrl ( ) ; await this . promptXrayUrl ( ) ; return ! ! this . _xrayUrl ; } private async promptPlatformUrl ( ) { this . _url = ( await vscode . window . showInputBox ( { prompt : 'Enter JFrog Platform URL' , value : this . _url , ignoreFocusOut : true , placeHolder : 'Example: https://acme.jfrog.io (Leave empty to configure Xray and Artifactory separately)' , validateInput : ConnectionUtils . validateUrl } ) ) || '' ; return ! ! this . _url ; } private async promptArtifactoryUrl ( ) { this . _rtUrl = ( await vscode . window . showInputBox ( { prompt : 'Enter JFrog Artifactory URL' , value : this . _url ? this . getRtUrlFromPlatform ( ) : '' , ignoreFocusOut : true , placeHolder : "Example: https://acme.jfrog.io/artifactory (Leave empty if you'e like to use only local Xray scans)" , validateInput : ConnectionUtils . validateUrl } ) ) || '' ; return ! ! this . _rtUrl ; } private async promptXrayUrl ( ) { this . _xrayUrl = ( await vscode . window . showInputBox ( { prompt : 'Enter JFrog Xray URL' , value : this . _url ? this . getXrayUrlFromPlatform ( ) : '' , ignoreFocusOut : true , validateInput : ConnectionUtils . validateXrayUrl } ) ) || '' ; return ! ! this . _xrayUrl ; } private async storePlatformUrl ( ) { await this . _context . globalState . update ( ConnectionManager . PLATFORM_URL_KEY , this . _url ) ; } private async storeXrayUrl ( ) { await this . _context . globalState . update ( ConnectionManager . XRAY_URL_KEY , this . _xrayUrl ) ; } private async storeRtUrl ( ) { await this . _context . globalState . update ( ConnectionManager . RT_URL_KEY , this . _rtUrl ) ; } private async setUsernameFromFilesystem ( ) { this . _username = ( await this . _context . globalState . get ( ConnectionManager . XRAY_USERNAME_KEY ) ) || '' ; return ! ! this . _username ; } private async promptUsername ( ) { this . _username = ( await vscode . window . showInputBox ( { prompt : 'Enter username' , value : this . _username , ignoreFocusOut : true , validateInput : ConnectionUtils . validateFieldNotEmpty } ) ) || '' ; return ! ! this . _username ; } private async storeUsername ( ) { await this . _context . globalState . update ( ConnectionManager . XRAY_USERNAME_KEY , this . _username ) ; } private async setPasswordFromKeyStore ( ) { this . _password = await this . getSecretFromKeyStore ( this . _username ) ; return ! ! this . _password ; } private async setAccessTokenFromKeyStore ( ) { this . _accessToken = await this . getSecretFromKeyStore ( ConnectionManager . ACCESS_TOKEN_FS_KEY ) ; return ! ! this . _accessToken ; } param keyPair returns private async getSecretFromKeyStore ( keyPair ) { return ( ( await keytar . getPassword ( ConnectionManager . SERVICE_ID , this . createAccountId ( this . _url , keyPair ) ) ) || ( await keytar . getPassword ( ConnectionManager . SERVICE_ID , this . createAccountId ( this . _xrayUrl , keyPair ) ) ) || '' ) ; } private async deletePasswordFromKeyStore ( ) { if ( ! this . _password ) { return true ; } return await this . deleteSecretFromKeyStore ( this . _username , 'password' ) ; } private async deleteAccessTokenFromKeyStore ( ) { if ( ! this . _accessToken ) { return true ; } return await this . deleteSecretFromKeyStore ( ConnectionManager . ACCESS_TOKEN_FS_KEY , 'access token' ) ; } private async deleteSecretFromKeyStore ( keyPair , secretName ) { let ok = await keytar . deletePassword ( ConnectionManager . SERVICE_ID , this . createAccountId ( this . _xrayUrl , keyPair ) ) ; if ( ! ok ) { this . _logManager . logMessage ( 'Failed to delete the ' + secretName + ' from the system secrets manager' , 'WARN' ) ; return false ; } return true ; } private async promptPassword ( ) { this . _password = ( await vscode . window . showInputBox ( { prompt : 'Enter password' , password : true , ignoreFocusOut : true , validateInput : ConnectionUtils . validateFieldNotEmpty } ) ) || '' ; return ! ! this . _password ; } private async storePassword ( ) { if ( ! this . _password ) { return ; } await keytar . setPassword ( ConnectionManager . SERVICE_ID , this . createAccountId ( this . _xrayUrl , this . _username ) , this . _password ) ; } private async promptAccessToken ( ) { this . _accessToken = ( await vscode . window . showInputBox ( { prompt : 'Enter JFrog access token (Leave blank for username and password)' , password : true , ignoreFocusOut : true } ) ) || '' ; return ! ! this . _accessToken ; } private async storeAccessToken ( ) { if ( ! this . _accessToken ) { return ; } await keytar . setPassword ( ConnectionManager . SERVICE_ID , this . createAccountId ( this . _xrayUrl , ConnectionManager . ACCESS_TOKEN_FS_KEY ) , this . _accessToken ) ; } param url param username returns private createAccountId ( url , username ) { return crypto . createHash ( 'sha256' ) . update ( url + username ) . digest ( 'hex' ) ; } private updateConnectionIcon ( ) { vscode . commands . executeCommand ( 'setContext' , 'areCredentialsSet' , this . areXrayCredentialsSet ( ) ) ; } /**
     * Store URLs and username in VS-Code global state.
     * Store Xray password and access token in Keychain.
     */ private async storeConnection ( ) { await this . storeXrayUrl ( ) ; await this . storeRtUrl ( ) ; await this . storePlatformUrl ( ) ; await this . storeUsername ( ) ; await this . storePassword ( ) ; await this . storeAccessToken ( ) ; } private deleteCredentialsFromMemory ( ) { this . _accessToken = '' ; this . _password = '' ; this . _username = '' ; this . _url = '' ; this . _xrayUrl = '' ; this . _rtUrl = '' ; } private async deleteCredentialsFromFileSystem ( ) { let passOk = await this . deletePasswordFromKeyStore ( ) ; let tokenOk = await this . deleteAccessTokenFromKeyStore ( ) ; await Promise . all ( [ this . _context . globalState . update ( ConnectionManager . XRAY_URL_KEY , undefined ) , this . _context . globalState . update ( ConnectionManager . RT_URL_KEY , undefined ) , this . _context . globalState . update ( ConnectionManager . PLATFORM_URL_KEY , undefined ) , this . _context . globalState . update ( ConnectionManager . XRAY_USERNAME_KEY , undefined ) ] ) ; return passOk && tokenOk ; } param componentsToScan param checkCanceled param project returns public async scanGraph ( componentsToScan , checkCanceled , project ) { if ( ! this . areXrayCredentialsSet ( ) ) { await this . populateCredentials ( false ) ; } let graphRequest = { component_id : 'vscode-project' , nodes : < IGraphRequestModel [ ] > componentsToScan . toArray ( ) } as IGraphRequestModel ; return await this . createJfrogClient ( ) . xray ( ) . scan ( ) . graph ( graphRequest , checkCanceled , project ) ; } param componentDetails returns public async summaryComponent ( componentDetails ) { if ( ! this . areXrayCredentialsSet ( ) ) { await this . populateCredentials ( false ) ; } let summaryRequest = { component_details : componentDetails } ; let summaryResponse = await this . createJfrogClient ( ) . xray ( ) . summary ( ) . component ( summaryRequest ) ; return Promise . resolve ( summaryResponse . artifacts ) ; } public async searchArtifactsByAql ( aql ) { return this . createJfrogClient ( ) . artifactory ( ) . search ( ) . aqlSearch ( aql ) ; } public async downloadArtifact ( artifactPath ) { return this . createJfrogClient ( ) . artifactory ( ) . download ( ) . downloadArtifact ( artifactPath ) ; } public async downloadBuildDetails ( buildName , buildNumber , projectKey ) { return this . createJfrogClient ( ) . xray ( ) . details ( ) . build ( buildName , buildNumber , projectKey ) ; } public createJfrogClient ( ) { return ConnectionUtils . createJfrogClient ( this . _url , this . _rtUrl , this . _xrayUrl , this . _username , this . _password , this . _accessToken ) ; } public async sendUsageReport ( featureArray ) { const usagePrefix = 'Usage Report: ' ; if ( ! this . areCompleteCredentialsSet ( ) ) { this . _logManager . logMessage ( usagePrefix + 'Artifactory is not configured. Skipping usage report sending...' , 'DEBUG' ) ; } try { await this . createJfrogClient ( ) . artifactory ( ) . system ( ) . reportUsage ( ConnectionUtils . USER_AGENT , featureArray ) ; } catch ( error ) { this . _logManager . logMessage ( usagePrefix + 'Failed sending usage report: ' + error , 'DEBUG' ) ; return ; } this . _logManager . logMessage ( usagePrefix + 'Usage report sent successfully.' , 'DEBUG' ) ; } }