Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added summary section to IaC test output [CFG-1571] #3090 #3093

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
1 change: 1 addition & 0 deletions test/acceptance/fake-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const featureFlagDefaults = (): Map<string, boolean> => {
return new Map([
['cliFailFast', false],
['iacTerraformVarSupport', false],
['iacCliOutput', false],
]);
};

Expand Down
Loading