diff --git a/src/lib/analytics-sources.ts b/src/lib/analytics-sources.ts index 8e116b2a36..4872639d24 100644 --- a/src/lib/analytics-sources.ts +++ b/src/lib/analytics-sources.ts @@ -11,6 +11,7 @@ const debug = require('debug')('snyk'); import * as fs from 'fs'; import { ArgsOptions } from '../cli/args'; +import { join } from 'path'; export const INTEGRATION_NAME_HEADER = 'SNYK_INTEGRATION_NAME'; export const INTEGRATION_VERSION_HEADER = 'SNYK_INTEGRATION_VERSION'; @@ -50,10 +51,13 @@ enum TrackedIntegration { } export const getIntegrationName = (args: ArgsOptions[]): string => { + const maybeHomebrew = isHomebrew() ? 'HOMEBREW' : ''; const maybeScoop = isScoop() ? 'SCOOP' : ''; + const integrationName = String( args[0]?.integrationName || // Integration details passed through CLI flag process.env[INTEGRATION_NAME_HEADER] || + maybeHomebrew || maybeScoop || '', ).toUpperCase(); @@ -114,3 +118,27 @@ export function validateScoopManifestFile(snykExecutablePath: string): boolean { } return false; } + +export function isHomebrew(): boolean { + const currentProcessPath = process.execPath; + const isHomebrewPath = currentProcessPath.includes('/Cellar/snyk/'); + if (isHomebrewPath) { + return validateHomebrew(currentProcessPath); + } else { + return false; + } +} + +export function validateHomebrew(snykExecutablePath: string): boolean { + try { + const expectedFormulaFilePath = join( + snykExecutablePath, + '../../.brew/snyk.rb', + ); + const formulaFileExists = fs.existsSync(expectedFormulaFilePath); + return formulaFileExists; + } catch (error) { + debug('Error checking for Homebrew Formula file', error); + } + return false; +} diff --git a/test/analytics-sources.spec.ts b/test/analytics-sources.spec.ts index f4511dd26f..b9285a342a 100644 --- a/test/analytics-sources.spec.ts +++ b/test/analytics-sources.spec.ts @@ -4,9 +4,13 @@ import { INTEGRATION_NAME_HEADER, INTEGRATION_VERSION_HEADER, isScoop, + isHomebrew, + validateHomebrew, validateScoopManifestFile, } from '../src/lib/analytics-sources'; +import * as fs from 'fs'; + const emptyArgs = []; const defaultArgsParams = { _: [], @@ -21,16 +25,13 @@ beforeEach(() => { describe('analytics-sources - scoop detection', () => { it('detects if snyk is installed via scoop', () => { const originalExecPath = process.execPath; - try { - process.execPath = - process.cwd() + '/test/fixtures/scoop/good-manifest/snyk-win.exe'; - expect(isScoop()).toBe(true); + process.execPath = + process.cwd() + '/test/fixtures/scoop/good-manifest/snyk-win.exe'; + expect(isScoop()).toBe(true); - process.execPath = '/test/fixtures/scoop/bad-manifest/snyk-win.exe'; - expect(isScoop()).toBe(false); - } finally { - process.execPath = originalExecPath; - } + process.execPath = '/test/fixtures/scoop/bad-manifest/snyk-win.exe'; + expect(isScoop()).toBe(false); + process.execPath = originalExecPath; }); it('validates scoop manifest file', () => { @@ -47,6 +48,51 @@ describe('analytics-sources - scoop detection', () => { }); }); +describe('analytics-sources - Homebrew detection', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('detects if snyk is installed via Homebrew', () => { + const originalExecPath = process.execPath; + process.execPath = process.cwd() + '/usr/local/Cellar/snyk/v1.413.2/bin'; + const fileExistsSpy = jest.spyOn(fs, 'existsSync').mockReturnValue(true); + expect(isHomebrew()).toBe(true); + expect(fileExistsSpy).toHaveBeenCalledTimes(1); + process.execPath = originalExecPath; + }); + + it('returns false if formula not in Homebrew path', () => { + const originalExecPath = process.execPath; + process.execPath = + process.cwd() + '/usr/local/some-other-location/snyk/v1.413.2/bin'; + const fileExistsSpy = jest.spyOn(fs, 'existsSync'); + expect(isHomebrew()).toBe(false); + expect(fileExistsSpy).not.toHaveBeenCalled(); + process.execPath = originalExecPath; + }); + + it('returns false if formula files does not exist', () => { + const originalExecPath = process.execPath; + process.execPath = process.cwd() + '/usr/local/Cellar/snyk/v1.413.2/bin'; + const fileExistsSpy = jest.spyOn(fs, 'existsSync').mockReturnValue(false); + expect(isHomebrew()).toBe(false); + expect(fileExistsSpy).toHaveBeenCalledTimes(1); + process.execPath = originalExecPath; + }); + + it('validates Homebrew formula file exists', () => { + let snykExecPath = + process.cwd() + '/test/fixtures/homebrew/Cellar/snyk/vX/bin/snyk'; // relies on fixture at /test/fixtures/homebrew/Cellar/vX/.brew/snyk.rb + expect(validateHomebrew(snykExecPath)).toBe(true); + + snykExecPath = + process.cwd() + + '/test/fixtures/homebrew/no-exist/Cellar/snyk/vX/bin/snyk'; + expect(validateHomebrew(snykExecPath)).toBe(false); + }); +}); + describe('analytics-sources - getIntegrationName', () => { it('integration name is empty by default', () => { expect(getIntegrationName(emptyArgs)).toBe(''); @@ -83,13 +129,18 @@ describe('analytics-sources - getIntegrationName', () => { it('integration name SCOOP when snyk is installed with scoop', () => { const originalExecPath = process.execPath; - try { - process.execPath = - process.cwd() + '/test/fixtures/scoop/good-manifest/snyk-win.exe'; - expect(getIntegrationName(emptyArgs)).toBe('SCOOP'); - } finally { - process.execPath = originalExecPath; - } + process.execPath = + process.cwd() + '/test/fixtures/scoop/good-manifest/snyk-win.exe'; + expect(getIntegrationName(emptyArgs)).toBe('SCOOP'); + process.execPath = originalExecPath; + }); + + it('integration name HOMEBREW when snyk is installed with Homebrew', () => { + const originalExecPath = process.execPath; + process.execPath = + process.cwd() + '/test/fixtures/homebrew/Cellar/snyk/vX/bin/snyk'; // relies on fixture at /test/fixtures/homebrew/Cellar/vX/.brew/snyk.rb + expect(getIntegrationName(emptyArgs)).toBe('HOMEBREW'); + process.execPath = originalExecPath; }); }); diff --git a/test/fixtures/homebrew/Cellar/snyk/vX/.brew/snyk.rb b/test/fixtures/homebrew/Cellar/snyk/vX/.brew/snyk.rb new file mode 100644 index 0000000000..2c46b62b5c --- /dev/null +++ b/test/fixtures/homebrew/Cellar/snyk/vX/.brew/snyk.rb @@ -0,0 +1 @@ +# fake Formula file