Skip to content

Commit

Permalink
feat: Added summary section to IaC test output
Browse files Browse the repository at this point in the history
  • Loading branch information
ofekatr committed Apr 5, 2022
1 parent e41e727 commit 5638320
Show file tree
Hide file tree
Showing 9 changed files with 209 additions and 75 deletions.
1 change: 1 addition & 0 deletions src/cli/commands/test/iac-local-execution/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ export async function test(
failures: isLocalFolder(pathToScan)
? failedFiles.map(removeFileContent)
: undefined,
ignoreCount,
};
} finally {
cleanLocalCache();
Expand Down
1 change: 1 addition & 0 deletions src/cli/commands/test/iac-local-execution/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ export enum IaCErrorCodes {
export interface TestReturnValue {
results: TestResult | TestResult[];
failures?: IacFileInDirectory[];
ignoreCount: number;
}

// https://github.com/opencontainers/image-spec/blob/main/manifest.md#image-manifest
Expand Down
149 changes: 84 additions & 65 deletions src/cli/commands/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
import { isIacShareResultsOptions } from './iac-local-execution/assert-iac-options-flag';
import { assertIaCOptionsFlags } from './iac-local-execution/assert-iac-options-flag';
import { hasFeatureFlag } from '../../../lib/feature-flags';
import { formatIacTestSummary } from '../../../lib/formatters/iac-output/v2';

const debug = Debug('snyk-test');
const SEPARATOR = '\n-------------------------------------------------------\n';
Expand Down Expand Up @@ -103,6 +104,7 @@ export default async function test(

// Holds an array of scanned file metadata for output.
let iacScanFailures: IacFileInDirectory[] | undefined;
let iacIgnoredIssuesCount = 0;
let iacOutputMeta: IacOutputMeta | undefined;

// Promise waterfall to test all other paths sequentially
Expand All @@ -118,14 +120,20 @@ export default async function test(
try {
if (options.iac) {
assertIaCOptionsFlags(process.argv);
const { results, failures } = await iacTest(path, testOpts);
const { results, failures, ignoreCount } = await iacTest(
path,
testOpts,
);

iacOutputMeta = {
orgName: results[0]?.org,
projectName: results[0]?.projectName,
gitRemoteUrl: results[0]?.meta?.gitRemoteUrl,
};

res = results;
iacScanFailures = failures;
iacIgnoredIssuesCount += ignoreCount;
} else {
res = await snyk.test(path, testOpts);
}
Expand Down Expand Up @@ -229,9 +237,8 @@ export default async function test(
throw err;
}

const isNewIacOutputSupported = options.iac
? await hasFeatureFlag('iacCliOutput', options)
: false;
const isNewIacOutputSupported =
!!options.iac && !!(await hasFeatureFlag('iacCliOutput', options));

let response = results
.map((result, i) => {
Expand Down Expand Up @@ -267,78 +274,90 @@ export default async function test(
}
}

if (results.length > 1) {
const projects = results.length === 1 ? 'project' : 'projects';
summaryMessage =
`\n\n\nTested ${results.length} ${projects}` +
summariseVulnerableResults(vulnerableResults, options) +
summariseErrorResults(errorResultsLength) +
'\n';
}
if (iacOutputMeta && isNewIacOutputSupported) {
const iacTestSummary = `${EOL.repeat(2)}${formatIacTestSummary(
{
results,
ignoreCount: iacIgnoredIssuesCount,
},
iacOutputMeta,
)}${EOL}`;

response += iacTestSummary;
} else {
if (results.length > 1) {
const projects = results.length === 1 ? 'project' : 'projects';
summaryMessage =
`\n\n\nTested ${results.length} ${projects}` +
summariseVulnerableResults(vulnerableResults, options) +
summariseErrorResults(errorResultsLength) +
'\n';
}

if (notSuccess) {
response += chalk.bold.red(summaryMessage);
const error = new Error(response) as any;
// take the code of the first problem to go through error
// translation
// HACK as there can be different errors, and we pass only the
// first one
error.code = errorResults[0].code;
error.userMessage = errorResults[0].userMessage;
error.strCode = errorResults[0].strCode;
throw error;
}
if (notSuccess) {
response += chalk.bold.red(summaryMessage);
const error = new Error(response) as any;
// take the code of the first problem to go through error
// translation
// HACK as there can be different errors, and we pass only the
// first one
error.code = errorResults[0].code;
error.userMessage = errorResults[0].userMessage;
error.strCode = errorResults[0].strCode;
throw error;
}

if (foundVulnerabilities) {
if (options.failOn) {
const fail = shouldFail(vulnerableResults, options.failOn);
if (!fail) {
// return here to prevent throwing failure
response += chalk.bold.green(summaryMessage);
response += EOL + EOL;
response += getProtectUpgradeWarningForPaths(
packageJsonPathsWithSnykDepForProtect,
);
if (foundVulnerabilities) {
if (options.failOn) {
const fail = shouldFail(vulnerableResults, options.failOn);
if (!fail) {
// return here to prevent throwing failure
response += chalk.bold.green(summaryMessage);
response += EOL + EOL;
response += getProtectUpgradeWarningForPaths(
packageJsonPathsWithSnykDepForProtect,
);

return TestCommandResult.createHumanReadableTestCommandResult(
response,
stringifiedJsonData,
stringifiedSarifData,
);
return TestCommandResult.createHumanReadableTestCommandResult(
response,
stringifiedJsonData,
stringifiedSarifData,
);
}
}
}

response += chalk.bold.red(summaryMessage);
response += chalk.bold.red(summaryMessage);

response += EOL + EOL;
const foundSpotlightVulnIds = containsSpotlightVulnIds(results);
const spotlightVulnsMsg = notificationForSpotlightVulns(
foundSpotlightVulnIds,
);
response += spotlightVulnsMsg;
response += EOL + EOL;
const foundSpotlightVulnIds = containsSpotlightVulnIds(results);
const spotlightVulnsMsg = notificationForSpotlightVulns(
foundSpotlightVulnIds,
);
response += spotlightVulnsMsg;

if (isIacShareResultsOptions(options)) {
response += chalk.bold.white(shareResultsOutput(iacOutputMeta!)) + EOL;
}

if (isIacShareResultsOptions(options)) {
response += chalk.bold.white(shareResultsOutput(iacOutputMeta!)) + EOL;
const error = new Error(response) as any;
// take the code of the first problem to go through error
// translation
// HACK as there can be different errors, and we pass only the
// first one
error.code = vulnerableResults[0].code || 'VULNS';
error.userMessage = vulnerableResults[0].userMessage;
error.jsonStringifiedResults = stringifiedJsonData;
error.sarifStringifiedResults = stringifiedSarifData;
throw error;
}

const error = new Error(response) as any;
// take the code of the first problem to go through error
// translation
// HACK as there can be different errors, and we pass only the
// first one
error.code = vulnerableResults[0].code || 'VULNS';
error.userMessage = vulnerableResults[0].userMessage;
error.jsonStringifiedResults = stringifiedJsonData;
error.sarifStringifiedResults = stringifiedSarifData;
throw error;
response += chalk.bold.green(summaryMessage);
response += EOL + EOL;
response += getProtectUpgradeWarningForPaths(
packageJsonPathsWithSnykDepForProtect,
);
}

response += chalk.bold.green(summaryMessage);
response += EOL + EOL;
response += getProtectUpgradeWarningForPaths(
packageJsonPathsWithSnykDepForProtect,
);

if (isIacShareResultsOptions(options)) {
response += chalk.bold.white(shareResultsOutput(iacOutputMeta!)) + EOL;
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/formatters/iac-output/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function getIacDisplayedOutput(
isNewIacOutputSupported?: boolean,
): string {
return isNewIacOutputSupported
? v2.getIacDisplayedOutput(iacTest, testedInfoText, meta, prefix)
? v2.getIacDisplayedOutput(iacTest, prefix)
: v1.getIacDisplayedOutput(iacTest, testedInfoText, meta, prefix);
}

Expand Down
13 changes: 13 additions & 0 deletions src/lib/formatters/iac-output/v2/color-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import chalk, { Chalk, ColorSupport } from 'chalk';
import { SEVERITY } from '../../../snyk-test/common';

export const severityColor: {
[severity in SEVERITY]: Chalk & {
supportsColor: ColorSupport;
};
} = {
critical: chalk.hex('#AB191A'),
high: chalk.hex('#CE501A'),
medium: chalk.hex('#D68000'),
low: chalk.hex('#88879E'),
};
11 changes: 3 additions & 8 deletions src/lib/formatters/iac-output/v2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { IacFileInDirectory } from '../../../../lib/types';

import { getSeverityValue } from '../../get-severity-value';

export { formatIacTestSummary } from './test-summary';

const debug = Debug('iac-output');

function formatIacIssue(
Expand Down Expand Up @@ -47,8 +49,6 @@ function formatIacIssue(

export function getIacDisplayedOutput(
iacTest: IacTestResponse,
testedInfoText: string,
meta: string,
prefix: string,
): string {
const issuesTextArray = [
Expand All @@ -74,12 +74,7 @@ export function getIacDisplayedOutput(
issuesInfoOutput.push(issuesTextArray.join('\n'));
}

let body = issuesInfoOutput.join('\n\n') + '\n\n' + meta;

const vulnCountText = `found ${issues.length} issues`;
const summary = testedInfoText + ', ' + chalk.red.bold(vulnCountText);

body = body + '\n\n' + summary;
const body = issuesInfoOutput.join('\n\n');

return prefix + body;
}
Expand Down
97 changes: 97 additions & 0 deletions src/lib/formatters/iac-output/v2/test-summary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import chalk from 'chalk';
import { EOL } from 'os';
import { rightPadWithSpaces } from '../../../right-pad';
import { SEVERITY } from '../../../snyk-test/common';
import { color, icon } from '../../../theme';
import { IacOutputMeta } from '../../../types';
import { severityColor } from './color-utils';
import { IacTestData } from './types';

const PAD_LENGTH = 19; // chars to align
const INDENT = ' ';

export function formatIacTestSummary(
testData: IacTestData,
outputMeta: IacOutputMeta,
): string {
const title = chalk.bold.white('Test Summary');
const summarySections: string[] = [title];

summarySections.push(formatTestMetaSection(outputMeta));

summarySections.push(formatCountsSection(testData));

return summarySections.join(EOL.repeat(2));
}

function formatTestMetaSection(iacMeta: IacOutputMeta): string {
const metaSectionProperties: [string, string][] = [];

if (iacMeta.orgName) {
metaSectionProperties.push(['Organization', iacMeta.orgName]);
}

const metaSection = metaSectionProperties
.map(([key, value]) =>
rightPadWithSpaces(`${INDENT}${key}: ${value}`, PAD_LENGTH),
)
.join(EOL);

return metaSection;
}

function formatCountsSection(testData: IacTestData): string {
const filesWithIssues = testData.results.filter(
(result) => result.result.cloudConfigResults.length,
).length;
const filesWithoutIssues = testData.results.length - filesWithIssues;

const countsSectionProperties: string[] = [];

countsSectionProperties.push(
`${chalk.bold(
color.status.success(icon.VALID),
)} Files without issues: ${chalk.white.bold(`${filesWithoutIssues}`)}`,
);

countsSectionProperties.push(
`${chalk.bold(
color.status.error(icon.ISSUE),
)} Files with issues: ${chalk.white.bold(`${filesWithIssues}`)}`,
);

countsSectionProperties.push(
`${INDENT}Ignored issues: ${chalk.white.bold(`${testData.ignoreCount}`)}`,
);

let totalIssuesCount = 0;

const issueCountsBySeverities: { [key in SEVERITY | 'none']: number } = {
critical: 0,
high: 0,
medium: 0,
low: 0,
none: 0,
};

testData.results.forEach((iacTestResponse) => {
totalIssuesCount += iacTestResponse.result.cloudConfigResults.length;
iacTestResponse.result.cloudConfigResults.forEach((iacIssue) => {
issueCountsBySeverities[iacIssue.severity]++;
});
});

countsSectionProperties.push(
`${INDENT}Total issues: ${chalk.white.bold(
`${totalIssuesCount}`,
)} [ ${severityColor.critical(
`${issueCountsBySeverities.critical} critical`,
)}, ${severityColor.high(
`${issueCountsBySeverities.high} high`,
)}, ${severityColor.medium(
`${issueCountsBySeverities.medium} medium`,
)}, ${severityColor.low(`${issueCountsBySeverities.low} low`)} ]`,
);

return countsSectionProperties.join(EOL);
}
6 changes: 6 additions & 0 deletions src/lib/formatters/iac-output/v2/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { IacTestResponse } from '../../../snyk-test/iac-test-result';

export interface IacTestData {
ignoreCount: number;
results: IacTestResponse[];
}
4 changes: 3 additions & 1 deletion src/lib/formatters/test/display-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ export function displayResult(
options: DisplayResultOptions,
foundProjectCount?: number,
) {
const meta = formatTestMeta(res, options);
const meta = options.isNewIacOutputSupported
? ''
: formatTestMeta(res, options);
const dockerAdvice = dockerRemediationForDisplay(res);
const projectType =
(res.packageManager as SupportedProjectTypes) || options.packageManager;
Expand Down

0 comments on commit 5638320

Please sign in to comment.