snyk
Version:
snyk library and cli utility
412 lines (351 loc) • 11.6 kB
text/typescript
// must be set before we import 'global-agent/bootstrap'
process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE = '';
process.env.HTTPS_PROXY =
process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '';
process.env.HTTP_PROXY = process.env.HTTP_PROXY ?? process.env.http_proxy ?? '';
process.env.NO_PROXY = process.env.NO_PROXY ?? process.env.no_proxy ?? '';
import 'global-agent/bootstrap';
import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs';
import { spawnSync } from 'child_process';
import * as https from 'https';
import { createHash } from 'crypto';
import * as Sentry from '@sentry/node';
export const versionFile = path.join(__dirname, 'generated', 'version');
export const shasumFile = path.join(__dirname, 'generated', 'sha256sums.txt');
const binaryDeploymentsFilePath = path.join(
__dirname,
'generated',
'binary-deployments.json',
);
export const integrationName = 'TS_BINARY_WRAPPER';
export class WrapperConfiguration {
private version: string;
private binaryName: string;
private expectedSha256sum: string;
public constructor(
version: string,
binaryName: string,
expectedSha256sum: string,
) {
this.version = version;
this.binaryName = binaryName;
this.expectedSha256sum = expectedSha256sum;
}
public getVersion(): string {
return this.version;
}
public getBinaryName(): string {
return this.binaryName;
}
public getDownloadLocations(): { downloadUrl: string; backupUrl: string } {
const baseUrl = 'https://downloads.snyk.io/cli';
const backupUrl = 'https://static.snyk.io/cli';
return {
downloadUrl: `${baseUrl}/v${this.version}/${this.binaryName}`,
backupUrl: `${backupUrl}/v${this.version}/${this.binaryName}`,
};
}
public getLocalLocation(): string {
const currentFolder = __dirname;
return path.join(currentFolder, this.binaryName);
}
public getShasumFile(): string {
return this.expectedSha256sum;
}
}
const logErrorWithTimeStamps = (...args) => {
console.error(`${new Date().toISOString()}:`, ...args);
};
export function determineBinaryName(platform: string, arch: string): string {
let osname = platform;
let archname = arch;
switch (osname) {
case 'win32':
osname = 'windows';
break;
case 'linux': {
let isAlpine = false;
try {
const result = spawnSync('cat /etc/os-release', { shell: true });
isAlpine = result.stdout.toString().toLowerCase().includes('id=alpine');
} catch {
isAlpine = false;
}
if (isAlpine) {
osname = 'alpine';
}
break;
}
}
switch (arch) {
case 'x64':
case 'amd64':
archname = 'amd64';
break;
}
const supportedPlatforms = require(binaryDeploymentsFilePath);
const binaryName = supportedPlatforms[osname][archname];
if (binaryName === undefined) {
const defaultErrorMsg =
' The current platform (' +
osname +
' ' +
archname +
') is not supported by Snyk.\n' +
' You may want to consider using Docker to run Snyk, for details see: https://docs.snyk.io/snyk-cli/install-the-snyk-cli#snyk-cli-in-a-docker-image\n' +
' If you experience errors please check http://support.snyk.io/.';
throw Error(getWarningMessage(defaultErrorMsg));
}
return binaryName;
}
export function getCurrentVersion(filename: string): string {
try {
const version = fs.readFileSync(filename);
return version.toString().trim();
} catch {
return '';
}
}
export function getCurrentSha256sum(
binaryName: string,
filename: string,
): string {
try {
const allsums = fs.readFileSync(filename).toString();
const re = new RegExp('^([a-zA-Z0-9]+)[\\s\\*]+' + binaryName + '$', 'mig');
const result = re.exec(allsums);
if (result) {
return result[1];
}
} catch {
//
}
return 'unknown-shasum-' + binaryName;
}
export function getCurrentConfiguration(): WrapperConfiguration {
const binaryName = determineBinaryName(os.platform(), os.arch());
const version = getCurrentVersion(versionFile);
const expectedSha256sum = getCurrentSha256sum(binaryName, shasumFile);
return new WrapperConfiguration(version, binaryName, expectedSha256sum);
}
export function getCliArguments(inputArgv: string[]): string[] {
const cliArguments = inputArgv.slice(2);
return cliArguments;
}
export function debugEnabled(cliArguments: string[]): boolean {
let debugIndex = cliArguments.indexOf('--debug');
if (debugIndex < 0) {
debugIndex = cliArguments.indexOf('-d');
}
return debugIndex >= 0;
}
export function runWrapper(executable: string, cliArguments: string[]): number {
interface SpawnError extends Error {
errno: number;
code: string;
syscall: string;
path: string;
spawnargs: string[];
}
const debug = debugEnabled(cliArguments);
if (debug) {
logErrorWithTimeStamps('Executing: ' + executable);
}
const res = spawnSync(executable, cliArguments, {
shell: false,
stdio: 'inherit',
env: {
...process.env,
SNYK_INTEGRATION_NAME: integrationName,
SNYK_INTEGRATION_VERSION: getCurrentVersion(versionFile),
SNYK_INTEGRATION_ENVIRONMENT: 'node',
SNYK_INTEGRATION_ENVIRONMENT_VERSION: process.versions.node,
},
});
if (res.status !== null) {
if (debug) {
logErrorWithTimeStamps(res);
}
return res.status;
} else {
logErrorWithTimeStamps(res);
if (!formatErrorMessage((res.error as SpawnError).code)) {
logErrorWithTimeStamps(
'Failed to spawn child process. (' + executable + ')',
);
}
return 2;
}
}
export function getWarningMessage(message: string): string {
return `\n------------------------------- Warning -------------------------------\n${message}\n------------------------------- Warning -------------------------------\n`;
}
export function formatErrorMessage(message: string): boolean {
const eaccesWarning =
"You don't have the permissions to install Snyk. Please try the following options:\n" +
'* If you are installing with increased privileges (for example sudo), try adding --unsafe-perm as a parameter to npm install\n' +
'* If you run NPM <= 6, please upgrade to a later version.\n' +
'If the problems persist please check http://support.snyk.io/.';
const certificateError =
'If you are running Snyk in an environment that intercepts SSL traffic, please specify\n' +
'your custom CA certificates via the NODE_EXTRA_CA_CERTS environment variable.\n' +
'See https://nodejs.org/api/cli.html#node_extra_ca_certsfile for additional information.';
const degradedCLIWarning =
'You are currently running a degraded version of the Snyk CLI.\n' +
'As a result, some features of the CLI will be unavailable.\n' +
'For information on how to resolve this, please see this article: https://docs.snyk.io/snyk-cli/installing-snyk-cli-as-a-binary-via-npm\n' +
'For any assistance, please check http://support.snyk.io/.';
let warning = '';
if (message.includes('EACCES')) {
warning = eaccesWarning;
} else if (message.includes('certificate')) {
warning = certificateError;
} else if (message.includes('legacy-cli')) {
warning = degradedCLIWarning;
} else {
return false;
}
logErrorWithTimeStamps(getWarningMessage(warning));
return true;
}
export function downloadExecutable(
downloadUrl: string,
filename: string,
filenameShasum: string,
): Promise<Error | undefined> {
return new Promise<Error | undefined>(function (resolve) {
logErrorWithTimeStamps('Starting download');
const options = new URL(`${downloadUrl}?utm_source=${integrationName}`);
const temp = path.join(__dirname, Date.now().toString());
const fileStream = fs.createWriteStream(temp);
const shasum = createHash('sha256').setEncoding('hex');
const cleanupAfterError = (error: Error) => {
try {
fs.unlinkSync(temp);
} catch (e) {
// ignoring any error during cleaning up after an error
}
resolve(error);
};
// shasum events
shasum.on('error', cleanupAfterError);
// filestream events
fileStream.on('error', cleanupAfterError).on('close', () => {
const actualShasum = shasum.read();
const debugMessage =
'Shasums:\n- actual: ' +
actualShasum +
'\n- expected: ' +
filenameShasum;
if (filenameShasum && actualShasum != filenameShasum) {
cleanupAfterError(Error('Shasum comparison failed!\n' + debugMessage));
} else {
logErrorWithTimeStamps(debugMessage);
// finally rename the file and change permissions
fs.renameSync(temp, filename);
fs.chmodSync(filename, 0o755);
logErrorWithTimeStamps('Downloaded successfull! ');
}
resolve(undefined);
});
logErrorWithTimeStamps(
"Downloading from '" + options.toString() + "' to '" + filename + "'",
);
const req = https.get(options, (res) => {
// response events
res.on('error', cleanupAfterError).on('end', () => {
shasum.end();
fileStream.end();
});
// pipe data
res.pipe(fileStream);
res.pipe(shasum);
});
req.on('error', cleanupAfterError).on('response', (incoming) => {
if (
incoming.statusCode &&
!(200 <= incoming.statusCode && incoming.statusCode < 300)
) {
req.destroy();
cleanupAfterError(
Error(
'Download failed! Server Response: ' +
incoming.statusCode +
' ' +
incoming.statusMessage +
' (' +
downloadUrl +
')',
),
);
}
});
req.end();
});
}
export async function downloadWithBackup(
downloadUrl: string,
backupUrl: string,
filename: string,
filenameShasum: string,
): Promise<Error | undefined> {
try {
const error = await downloadExecutable(
downloadUrl,
filename,
filenameShasum,
);
if (error) {
logErrorWithTimeStamps(error);
logErrorWithTimeStamps(
`Failed to download from ${downloadUrl}! Trying to download from ${backupUrl} location...`,
);
const backupError = await downloadExecutable(
backupUrl,
filename,
filenameShasum,
);
logErrorWithTimeStamps(backupError);
return backupError;
}
} catch (err) {
// Handle any unexpected errors
logErrorWithTimeStamps('An unexpected error occurred:', err);
throw err; // Rethrow if you want to propagate the error upwards
}
}
export async function logError(
context: string,
err: Error,
printToConsole = true,
): Promise<void> {
if (isAnalyticsEnabled()) {
// init error reporting
const version = getCurrentVersion(versionFile);
Sentry.init({
dsn: 'https://3e845233db8c4f43b4c4b9245f1d7bd6@o30291.ingest.sentry.io/4504599528079360',
release: version,
});
// report error
const sentryError = new Error('[' + context + '] ' + err.message);
sentryError.stack = err.stack;
Sentry.captureException(sentryError);
await Sentry.close();
}
// finally log the error to the console as well
if (printToConsole) {
logErrorWithTimeStamps('\n' + err);
formatErrorMessage(err.message);
}
}
export function isAnalyticsEnabled(): boolean {
if (
process.env.snyk_disable_analytics == '1' ||
process.env.SNYK_DISABLE_ANALYTICS == '1'
) {
return false;
}
return true;
}