Skip to content

Commit

Permalink
feat: add --json-file-output option for snyk test
Browse files Browse the repository at this point in the history
  • Loading branch information
maxjeffos committed May 7, 2020
1 parent de65d60 commit aea2b8c
Show file tree
Hide file tree
Showing 12 changed files with 339 additions and 110 deletions.
5 changes: 3 additions & 2 deletions src/cli/args.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as abbrev from 'abbrev';
import { CommandResult } from './commands/types';

import debugModule = require('debug');

Expand Down Expand Up @@ -37,7 +38,7 @@ function dashToCamelCase(dash) {
// Last item is ArgsOptions, the rest are strings (positional arguments, e.g. paths)
export type MethodArgs = Array<string | ArgsOptions>;

export type Method = (...args: MethodArgs) => Promise<string>;
export type Method = (...args: MethodArgs) => Promise<CommandResult>;

export interface Args {
command: string;
Expand Down Expand Up @@ -143,7 +144,7 @@ export function args(rawArgv: string[]): Args {
argv._.unshift(tmp.shift()!);
}

let method: () => Promise<string> = cli[command];
let method: () => Promise<CommandResult> = cli[command];

if (!method) {
// if we failed to find a command, then default to an error
Expand Down
10 changes: 7 additions & 3 deletions src/cli/commands/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { AuthFailedError } from '../../../lib/errors/authentication-failed-error
import { verifyAPI } from './is-authed';
import { CustomError } from '../../../lib/errors';
import { getUtmsAsString } from '../../../lib/utm';
import { CommandResult } from '../types';

export = auth;

Expand Down Expand Up @@ -117,7 +118,10 @@ async function testAuthComplete(token: string): Promise<{ res; body }> {
});
}

async function auth(apiToken: string, via: AuthCliCommands) {
async function auth(
apiToken: string,
via: AuthCliCommands,
): Promise<CommandResult> {
let promise;
resetAttempts();
if (apiToken) {
Expand All @@ -134,9 +138,9 @@ async function auth(apiToken: string, via: AuthCliCommands) {

if (res.statusCode === 200 || res.statusCode === 201) {
snyk.config.set('api', body.api);
return (
return new CommandResult(
'\nYour account has been authenticated. Snyk is now ready to ' +
'be used.\n'
'be used.\n',
);
}
throw errorForFailedAuthAttempt(res, body);
Expand Down
64 changes: 41 additions & 23 deletions src/cli/commands/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from '../../../lib/types';
import { isLocalFolder } from '../../../lib/detect';
import { MethodArgs } from '../../args';
import { TestCommandResult } from '../../commands/types';
import {
GroupedVuln,
LegacyVulnApiResult,
Expand Down Expand Up @@ -55,9 +56,9 @@ const showVulnPathsMapping: Record<string, ShowVulnPaths> = {

// TODO: avoid using `as any` whenever it's possible

async function test(...args: MethodArgs): Promise<string> {
async function test(...args: MethodArgs): Promise<TestCommandResult> {
const resultOptions = [] as any[];
let results = [] as any[];
const results = [] as any[];
let options = ({} as any) as Options & TestOptions;

if (typeof args[args.length - 1] === 'object') {
Expand Down Expand Up @@ -157,25 +158,18 @@ async function test(...args: MethodArgs): Promise<string> {
// resultOptions is now an array of 1 or more options used for
// the tests results is now an array of 1 or more test results
// values depend on `options.json` value - string or object
if (options.json) {
results = results.map((result) => {
// add json for when thrown exception
if (result instanceof Error) {
return {
ok: false,
error: result.message,
path: (result as any).path,
};
}
return result;
});
const errorMappedResults = createErrorMappedResultsForJsonOutput(results);
// backwards compat - strip array IFF only one result
const dataToSend =
errorMappedResults.length === 1
? errorMappedResults[0]
: errorMappedResults;
const stringifiedData = JSON.stringify(dataToSend, null, 2);

// backwards compat - strip array IFF only one result
const dataToSend = results.length === 1 ? results[0] : results;
const stringifiedData = JSON.stringify(dataToSend, null, 2);

if (results.every((res) => res.ok)) {
return stringifiedData;
if (options.json) {
// if all results are ok (.ok == true) then return the json
if (errorMappedResults.every((res) => res.ok)) {
return TestCommandResult.createJsonTestCommandResult(stringifiedData);
}

const err = new Error(stringifiedData) as any;
Expand All @@ -185,7 +179,7 @@ async function test(...args: MethodArgs): Promise<string> {
const fail = shouldFail(vulnerableResults, options.failOn);
if (!fail) {
// return here to prevent failure
return stringifiedData;
return TestCommandResult.createJsonTestCommandResult(stringifiedData);
}
}
err.code = 'VULNS';
Expand All @@ -195,6 +189,7 @@ async function test(...args: MethodArgs): Promise<string> {
}

err.json = stringifiedData;
err.jsonStringifiedResults = stringifiedData;
throw err;
}

Expand Down Expand Up @@ -246,7 +241,10 @@ async function test(...args: MethodArgs): Promise<string> {
if (!fail) {
// return here to prevent throwing failure
response += chalk.bold.green(summaryMessage);
return response;
return TestCommandResult.createHumanReadableTestCommandResult(
response,
stringifiedData,
);
}
}

Expand All @@ -258,11 +256,31 @@ async function test(...args: MethodArgs): Promise<string> {
// first one
error.code = vulnerableResults[0].code || 'VULNS';
error.userMessage = vulnerableResults[0].userMessage;
error.jsonStringifiedResults = stringifiedData;
throw error;
}

response += chalk.bold.green(summaryMessage);
return response;
return TestCommandResult.createHumanReadableTestCommandResult(
response,
stringifiedData,
);
}

function createErrorMappedResultsForJsonOutput(results) {
const errorMappedResults = results.map((result) => {
// add json for when thrown exception
if (result instanceof Error) {
return {
ok: false,
error: result.message,
path: (result as any).path,
};
}
return result;
});

return errorMappedResults;
}

function shouldFail(vulnerableResults: any[], failOn: FailOn) {
Expand Down
57 changes: 57 additions & 0 deletions src/cli/commands/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
export class CommandResult {
result: string;
constructor(result: string) {
this.result = result;
}

public toString(): string {
return this.result;
}

public getDisplayResults() {
return this.result;
}
}

export abstract class TestCommandResult extends CommandResult {
protected jsonResult = '';
public getJsonResult(): string {
return this.jsonResult;
}

public static createHumanReadableTestCommandResult(
humanReadableResult: string,
jsonResult: string,
): HumanReadableTestCommandResult {
return new HumanReadableTestCommandResult(humanReadableResult, jsonResult);
}

public static createJsonTestCommandResult(
jsonResult: string,
): JsonTestCommandResult {
return new JsonTestCommandResult(jsonResult);
}
}

class HumanReadableTestCommandResult extends TestCommandResult {
protected jsonResult = '';

constructor(humanReadableResult: string, jsonResult: string) {
super(humanReadableResult);
this.jsonResult = jsonResult;
}

public getJsonResult(): string {
return this.jsonResult;
}
}

class JsonTestCommandResult extends TestCommandResult {
constructor(jsonResult: string) {
super(jsonResult);
}

public getJsonResult(): string {
return this.result;
}
}
38 changes: 37 additions & 1 deletion src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import 'source-map-support/register';
import * as Debug from 'debug';
import * as pathLib from 'path';
import { writeFileSync } from 'fs';

// assert supported node runtime version
import * as runtime from './runtime';
Expand All @@ -10,6 +11,7 @@ import * as analytics from '../lib/analytics';
import * as alerts from '../lib/alerts';
import * as sln from '../lib/sln';
import { args as argsLib, Args } from './args';
import { CommandResult, TestCommandResult } from './commands/types';
import { copy } from './copy';
import spinner = require('../lib/spinner');
import errors = require('../lib/errors/legacy-errors');
Expand All @@ -33,13 +35,15 @@ const EXIT_CODES = {
};

async function runCommand(args: Args) {
const result = await args.method(...args.options._);
const commandResult: CommandResult = await args.method(...args.options._);
const res = analytics({
args: args.options._,
command: args.command,
org: args.options.org,
});

const result = commandResult.getDisplayResults();

if (result && !args.options.quiet) {
if (args.options.copy) {
copy(result);
Expand All @@ -49,6 +53,18 @@ async function runCommand(args: Args) {
}
}

// also save the json (in error.json) to file if option is set
if (args.command === 'test') {
const jsonOutputFile = args.options['json-file-output'];
if (jsonOutputFile) {
const jsonOutputFileStr = jsonOutputFile as string;
saveJsonResultsToFile(
stripAnsi((commandResult as TestCommandResult).getJsonResult()),
jsonOutputFileStr,
);
}
}

return res;
}

Expand Down Expand Up @@ -86,6 +102,15 @@ async function handleError(args, error) {
}
}

// also save the json (in error.json) to file if `--json-file-output` option is set
const jsonOutputFile = args.options['json-file-output'];
if (jsonOutputFile && error.jsonStringifiedResults) {
saveJsonResultsToFile(
stripAnsi(error.jsonStringifiedResults),
jsonOutputFile,
);
}

const analyticsError = vulnsFound
? {
stack: error.jsonNoVulns,
Expand Down Expand Up @@ -120,6 +145,17 @@ async function handleError(args, error) {
return { res, exitCode };
}

function saveJsonResultsToFile(
stringifiedJson: string,
jsonOutputFile: string,
) {
try {
writeFileSync(jsonOutputFile, stringifiedJson);
} catch (err) {
console.error(err);
}
}

function checkRuntime() {
if (!runtime.isSupported(process.versions.node)) {
console.error(
Expand Down
Loading

0 comments on commit aea2b8c

Please sign in to comment.