diff --git a/package.json b/package.json index 29070f6..32528ab 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,11 @@ "type": "string", "default": "https://api.gitguardian.com", "markdownDescription": "You can override the value here for On Premise installations" + }, + "gitguardian.apiKey": { + "type": "string", + "default": "", + "markdownDescription": "Your API Key" } } }, diff --git a/src/extension.ts b/src/extension.ts index d0e7f16..72ab673 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,4 +1,5 @@ import { + ggshieldApiKey, ggshieldAuthStatus, ggshieldScanFile, ignoreLastFound, @@ -7,8 +8,9 @@ import { showAPIQuota, } from "./lib/ggshield-api"; import { - createDefaultConfiguration, + getConfiguration, GGShieldConfiguration, + setApiKey, } from "./lib/ggshield-configuration"; import { parseGGShieldResults } from "./lib/ggshield-results-parser"; import { @@ -121,7 +123,7 @@ function registerQuotaViewCommands(view: GitGuardianQuotaWebviewProvider) { export function activate(context: ExtensionContext) { // Check if ggshield if available const outputChannel = window.createOutputChannel("GitGuardian"); - let configuration = createDefaultConfiguration(context); + let configuration = getConfiguration(context); let authStatus: boolean = false; const ggshieldResolver = new GGShieldResolver( outputChannel, @@ -165,7 +167,10 @@ export function activate(context: ExtensionContext) { updateStatusBarItem(StatusBarStatus.unauthenticated, statusBar); } else { commands.executeCommand('setContext', 'isAuthenticated', true); - } + updateStatusBarItem(StatusBarStatus.ready, statusBar); + const ggshieldApi = ggshieldApiKey(configuration); + setApiKey(configuration, ggshieldApi); + } }) .then(async () => { // Check if git is installed @@ -233,6 +238,8 @@ export function activate(context: ExtensionContext) { authStatus = true; updateStatusBarItem(StatusBarStatus.ready, statusBar); commands.executeCommand('setContext', 'isAuthenticated', true); + const ggshieldApi = ggshieldApiKey(configuration); + setApiKey(configuration, ggshieldApi); ggshieldViewProvider.refresh(); ggshieldQuotaViewProvider.refresh(); } else { diff --git a/src/lib/ggshield-api.ts b/src/lib/ggshield-api.ts index 8059039..87baabb 100644 --- a/src/lib/ggshield-api.ts +++ b/src/lib/ggshield-api.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import { SpawnOptionsWithoutStdio, SpawnSyncOptionsWithStringEncoding, @@ -21,16 +22,26 @@ export function runGGShieldCommand( configuration: GGShieldConfiguration, args: string[] ): SpawnSyncReturns { - const { ggshieldPath, apiUrl } = configuration; + const { ggshieldPath, apiUrl, apiKey } = configuration; + let env: { + GITGUARDIAN_API_URL: string; + GG_USER_AGENT: string; + GITGUARDIAN_API_KEY?: string; // Note the ? to indicate this property is optional + } = { + GITGUARDIAN_API_URL: apiUrl, + GG_USER_AGENT: "gitguardian-vscode", + }; + + if (apiKey) { + env = { + ...env, + // eslint-disable-next-line @typescript-eslint/naming-convention + GITGUARDIAN_API_KEY: apiKey, + }; + } let options: SpawnSyncOptionsWithStringEncoding = { cwd: os.tmpdir(), - env: { - // eslint-disable-next-line @typescript-eslint/naming-convention - GITGUARDIAN_API_URL: apiUrl, - // eslint-disable-next-line @typescript-eslint/naming-convention - GG_USER_AGENT: "gitguardian-vscode", - }, encoding: "utf-8", windowsHide: true, }; @@ -178,7 +189,7 @@ export async function loginGGShield( configuration: GGShieldConfiguration, outputChannel: any ): Promise { - const { ggshieldPath, apiUrl } = configuration; + const { ggshieldPath, apiUrl, apiKey } = configuration; let options: SpawnOptionsWithoutStdio = { cwd: os.tmpdir(), @@ -230,7 +241,35 @@ export function ggshieldAuthStatus( console.log(proc.stderr); return false; } else { + if (proc.stdout.includes("unhealthy")) { + return false; + } console.log(proc.stdout); return true; } } + +export function ggshieldApiKey( + configuration: GGShieldConfiguration, +): string | undefined { + const proc = runGGShieldCommand(configuration, ["config", "list"]); + if (proc.stderr || proc.error) { + console.log(proc.stderr); + return undefined; + } else { + console.log(proc.stdout); + const apiUrl = configuration.apiUrl; + const re = /api/; + + const regexInstanceSection = `\\[${apiUrl.replace(re, "dashboard")}\\]([\\s\\S]*?)(?=\\[|$)`; + const instanceSectionMatch = proc.stdout.match(regexInstanceSection); + + if (instanceSectionMatch) { + const instanceSection = instanceSectionMatch[0]; + const regexToken = /token:\s([a-zA-Z0-9]+)/; + const matchToken = instanceSection.match(regexToken); + + return matchToken ? matchToken[1].trim() : undefined; + } + } +} \ No newline at end of file diff --git a/src/lib/ggshield-configuration.ts b/src/lib/ggshield-configuration.ts index d03a02f..bfd8022 100644 --- a/src/lib/ggshield-configuration.ts +++ b/src/lib/ggshield-configuration.ts @@ -1,5 +1,5 @@ import { getBinaryAbsolutePath } from "./ggshield-resolver-utils"; -import { ExtensionContext, workspace } from "vscode"; +import { ConfigurationTarget, ExtensionContext, workspace } from "vscode"; import * as os from "os"; const apiUrlDefault = "https://api.gitguardian.com/"; @@ -7,10 +7,12 @@ const apiUrlDefault = "https://api.gitguardian.com/"; export class GGShieldConfiguration { ggshieldPath: string; apiUrl: string; + apiKey: string; - constructor(ggshieldPath: string = "", apiUrl: string = "") { + constructor(ggshieldPath: string = "", apiUrl: string = "", apiKey: string = "") { this.ggshieldPath = ggshieldPath; this.apiUrl = apiUrl; + this.apiKey = apiKey; } } @@ -20,26 +22,28 @@ export class GGShieldConfiguration { * TODO: Check with Mathieu if this behaviour is expected * @returns {GGShieldConfiguration} from the extension settings */ -export function getSettingsConfiguration(): GGShieldConfiguration | undefined { +export function getConfiguration( + context: ExtensionContext +): GGShieldConfiguration { const config = workspace.getConfiguration("gitguardian"); const ggshieldPath: string | undefined = config.get("GGShieldPath"); const apiUrl: string | undefined = config.get("apiUrl"); + const apiKey: string | undefined = config.get("apiKey"); - if (!ggshieldPath) { - return undefined; - } return new GGShieldConfiguration( - ggshieldPath, - apiUrl ? apiUrl : apiUrlDefault + ggshieldPath ? ggshieldPath : getBinaryAbsolutePath(os.platform(), os.arch(), context), + apiUrl ? apiUrl : apiUrlDefault, + apiKey ? apiKey : "" ); } -export function createDefaultConfiguration( - context: ExtensionContext -): GGShieldConfiguration { - return new GGShieldConfiguration( - getBinaryAbsolutePath(os.platform(), os.arch(), context), - "https://api.gitguardian.com/" - ); -} +export function setApiKey(configuration: GGShieldConfiguration, apiKey: string | undefined): void { + const config = workspace.getConfiguration("gitguardian"); + if (!apiKey) { + throw new Error("Missing API Key"); + } + + configuration.apiKey = apiKey; + config.update("apiKey", apiKey, ConfigurationTarget.Global); +} \ No newline at end of file diff --git a/src/lib/ggshield-resolver.ts b/src/lib/ggshield-resolver.ts index 197b450..206b7cf 100644 --- a/src/lib/ggshield-resolver.ts +++ b/src/lib/ggshield-resolver.ts @@ -1,6 +1,6 @@ import * as vscode from "vscode"; import { - getSettingsConfiguration, + getConfiguration, GGShieldConfiguration, } from "./ggshield-configuration"; import { runGGShieldCommand } from "./ggshield-api"; @@ -32,44 +32,20 @@ export class GGShieldResolver { * @returns {Promise} A promise that resolves once the `ggshield` path is determined. */ async checkGGShieldConfiguration(): Promise { - let settingsConfiguration = getSettingsConfiguration(); - if (settingsConfiguration) { - try { - await this.useSettingsConfiguration(settingsConfiguration); - this.channel.appendLine( - `Using ggshield at: ${this.configuration.ggshieldPath}, to change this go to settings.` - ); - return; - } catch (error) { - this.channel.appendLine( - `Failed to use ggshield version from settings. - You can remove it from settings, and use the bundled version instead.` - ); - window.showErrorMessage( - `Failed to use ggshield version from settings.` - ); - throw error; - } - } else { - try { - await this.checkBundledGGShield(); - this.channel.appendLine( - `Using bundled ggshield at: ${this.configuration.ggshieldPath}, to change this go to settings.` - ); - return; - } catch (error) { - this.channel.appendLine( - `ggshield binary not found: this architecture is not supported ${ - (os.arch(), os.platform()) - }` - ); - window.showErrorMessage( - `ggshield binary not found: this architecture is not supported ${ - (os.arch(), os.platform()) - }` - ); - throw error; - } + try { + await this.testConfiguration(this.configuration); + this.channel.appendLine( + `Using ggshield at: ${this.configuration.ggshieldPath}, to change this go to settings.` + ); + return; + } catch (error) { + this.channel.appendLine( + `Failed to use ggshield version ${this.configuration.ggshieldPath}.` + ); + window.showErrorMessage( + `Failed to use ggshield.` + ); + throw error; } } @@ -78,7 +54,7 @@ export class GGShieldResolver { * * @returns {Promise} A promise that resolves if the configuration is valid. */ - async useSettingsConfiguration( + async testConfiguration( configuration: GGShieldConfiguration ): Promise { let proc = runGGShieldCommand(configuration, ["--version"]);