Skip to content

Commit

Permalink
refactor: Add iac-test.ts shim
Browse files Browse the repository at this point in the history
Added a new shim to wrap the legacy and current iac flows.
  • Loading branch information
aron committed Mar 23, 2021
1 parent d5e1114 commit 98e4ddb
Show file tree
Hide file tree
Showing 10 changed files with 103 additions and 65 deletions.
3 changes: 2 additions & 1 deletion .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ README.md @snyk/content @snyk/boost
src/cli/commands/test/formatters/format-reachability.ts @snyk/flow
help/ @snyk/hammer
* @snyk/hammer @snyk/boost
src/cli/commands/test/iac-output.ts @snyk/cloudconfig
src/cli/commands/test/iac-local-execution/ @snyk/cloudconfig
src/cli/commands/test/iac-output.ts @snyk/cloudconfig
src/cli/commands/test/iac-test-shim.ts @snyk/cloudconfig
src/lib/cloud-config-projects.ts @snyk/cloudconfig
src/lib/plugins/sast/ @snyk/sast-team
src/lib/iac/ @snyk/cloudconfig
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { CustomError } from '../../../../lib/errors';
import { IaCTestOptions } from '../../../../lib/types';
import { args } from '../../../args';
import { IaCTestFlags } from './types';

const keys: (keyof IaCTestOptions)[] = [
const keys: (keyof IaCTestFlags)[] = [
'debug',
'insecure',
'debug',
'experimental',
'detectionDepth',
'severityThreshold',
Expand All @@ -27,7 +26,7 @@ function camelcaseToDash(key: string) {
}

class FlagError extends CustomError {
constructor(key) {
constructor(key: string) {
const dashes = key.length === 1 ? '-' : '--';
const flag = camelcaseToDash(key);
const msg = `Unsupported flag "${dashes}${flag}" provided. Run snyk iac test --help for supported flags.`;
Expand Down
28 changes: 17 additions & 11 deletions src/cli/commands/test/iac-local-execution/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,42 @@ import { scanFiles } from './file-scanner';
import { formatScanResults } from './results-formatter';
import { isLocalFolder } from '../../../../lib/detect';
import {
IacOptionFlags,
IaCTestFlags,
IacFileParsed,
IacFileParseFailure,
SafeAnalyticsOutput,
} from './types';
import { initLocalCache } from './local-cache';
import { addIacAnalytics } from './analytics';
import { TestResult } from '../../../../lib/snyk-test/legacy';
import { IacFileInDirectory } from '../../../../lib/types';

// this method executes the local processing engine and then formats the results to adapt with the CLI output.
// the current version is dependent on files to be present locally which are not part of the source code.
// without these files this method would fail.
// if you're interested in trying out the experimental local execution model for IaC scanning, please reach-out.
export async function test(pathToScan: string, options: IacOptionFlags) {
export async function test(
pathToScan: string,
options: IaCTestFlags,
): Promise<{
results: TestResult | TestResult[];
files: IacFileInDirectory[] | null;
}> {
await initLocalCache();
const filesToParse = await loadFiles(pathToScan);
const { parsedFiles, failedFiles } = await parseFiles(filesToParse);
const scannedFiles = await scanFiles(parsedFiles);
const formattedResults = formatScanResults(scannedFiles, options);
addIacAnalytics(formattedResults);

if (isLocalFolder(pathToScan)) {
// TODO: This mutation is here merely to support how the old/current directory scan printing works.
// NOTE: No file or parsed file data should leave this function.
options.iacDirFiles = [...parsedFiles, ...failedFiles].map(
removeFileContent,
);
}

// TODO: add support for proper typing of old TestResult interface.
return formattedResults as any;
return {
results: (formattedResults as unknown) as TestResult[],
// NOTE: No file or parsed file data should leave this function.
files: isLocalFolder(pathToScan)
? [...parsedFiles, ...failedFiles].map(removeFileContent)
: null,
};
}

export function removeFileContent({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
EngineType,
FormattedResult,
IacFileScanResult,
IacOptionFlags,
IaCTestFlags,
PolicyMetadata,
} from './types';
import { SEVERITY } from '../../../../lib/snyk-test/common';
Expand All @@ -16,7 +16,7 @@ const SEVERITIES = [SEVERITY.LOW, SEVERITY.MEDIUM, SEVERITY.HIGH];

export function formatScanResults(
scanResults: Array<IacFileScanResult>,
options: IacOptionFlags,
options: IaCTestFlags,
): FormattedResult[] {
// Relevant only for multi-doc yaml files
const scannedResultsGroupedByDocId = groupMultiDocResults(scanResults);
Expand Down
37 changes: 33 additions & 4 deletions src/cli/commands/test/iac-local-execution/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { IacProjectType } from '../../../../lib/iac/constants';
import { SEVERITY } from '../../../../lib/snyk-test/common';
import { IacFileInDirectory } from '../../../../lib/types';
import {
IacFileInDirectory,
Options,
TestOptions,
} from '../../../../lib/types';

export interface IacFileData extends IacFileInDirectory {
fileContent: string;
Expand Down Expand Up @@ -75,10 +79,35 @@ export interface PolicyMetadata {
references: string[];
}

export interface IacOptionFlags {
// Collection of all options supported by `iac test` command.
// TODO: Needs to be fixed at the args module level.
export type IaCTestFlags = Pick<
Options & TestOptions,
| 'insecure'
| 'debug'
| 'experimental'
| 'detectionDepth'
| 'severityThreshold'
| 'json'
| 'sarif'
> & {
// Supported flags not yet covered by Options or TestOptions
'json-file-output'?: string;
'sarif-file-output'?: string;
v?: boolean;
version?: boolean;
h?: boolean;
help?: 'help';
q?: boolean;
quiet?: boolean;
};

// Includes all IaCTestOptions plus additional properties
// that are added at runtime and not part of the parsed
// CLI flags.
export type IaCTestOptions = IaCTestFlags & {
iacDirFiles?: Array<IacFileInDirectory>;
severityThreshold?: SEVERITY;
}
};

export interface TerraformPlanResource {
address: string; // "aws_cloudwatch_log_group.terra_ci",
Expand Down
34 changes: 34 additions & 0 deletions src/cli/commands/test/iac-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { assertIaCOptionsFlags } from './iac-local-execution/assert-iac-options-flag';
import { IaCTestOptions } from './iac-local-execution/types';
import { test as localTest } from './iac-local-execution';
import { test as legacyTest } from '../../../lib';
import { TestResult } from '../../../lib/snyk-test/legacy';
import { IacFileInDirectory } from '../../../lib/types';

/**
* Shim around the new local execution test path and the existing remote
* test flow. We also locally deal with the way the legacy test flow exposes
* the scanned files via the `options.iacDirFiles` object here so that
* in the new flow we do not mutate the options object.
*/
export async function test(
pathToScan: string,
options: IaCTestOptions,
): Promise<{
results: TestResult | TestResult[];
files: IacFileInDirectory[] | null;
}> {
// Ensure that all flags are correct. We do this to ensure that the
// caller doesn't accidentally mistype --experimental and send their
// configuration files to our backend by accident.
assertIaCOptionsFlags(process.argv);

if (options.experimental) {
// this path is an experimental feature feature for IaC which does issue scanning locally without sending files to our Backend servers.
// once ready for GA, it is aimed to deprecate our remote-processing model, so IaC file scanning in the CLI is done locally.
return localTest(pathToScan, options);
}

const results = await legacyTest(pathToScan, options);
return { files: options.iacDirFiles ?? null, results };
}
19 changes: 6 additions & 13 deletions src/cli/commands/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,12 @@ import {
getDisplayedOutput,
} from './formatters/format-test-results';

import * as iacLocalExecution from './iac-local-execution';
import { test as iacTest } from './iac-test';
import { validateCredentials } from './validate-credentials';
import { generateSnykTestError } from './generate-snyk-test-error';
import { validateTestOptions } from './validate-test-options';
import { setDefaultTestOptions } from './set-default-test-options';
import { processCommandArgs } from '../process-command-args';
import { assertIaCOptionsFlags } from './iac-local-execution/assert-iac-options-flag';

const debug = Debug('snyk-test');
const SEPARATOR = '\n-------------------------------------------------------\n';
Expand Down Expand Up @@ -97,22 +96,16 @@ async function test(...args: MethodArgs): Promise<TestCommandResult> {

try {
if (options.iac) {
// Ensure that all flags are correct. We do this to ensure that the
// caller doesn't accidentally mistype --experimental and send their
// configuration files to our backend by accident.
assertIaCOptionsFlags(process.argv);
}

if (options.iac && testOpts.experimental) {
// this path is an experimental feature feature for IaC which does issue scanning locally without sending files to our Backend servers.
// once ready for GA, it is aimed to deprecate our remote-processing model, so IaC file scanning in the CLI is done locally.
res = await iacLocalExecution.test(path, testOpts);
const { results, files } = await iacTest(path, testOpts);
res = results;
if (files) {
options.iacDirFiles = files;
}
} else {
res = await snyk.test(path, testOpts);
}
if (testOpts.iacDirFiles) {
options.iacDirFiles = testOpts.iacDirFiles;
}
} catch (error) {
res = generateSnykTestError(error);
}
Expand Down
23 changes: 0 additions & 23 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,29 +26,6 @@ export interface TestOptions {
iacDirFiles?: IacFileInDirectory[];
}

// Collection of all options supported by `iac test` command.
// TODO: Needs to be fixed at the args module level.
export type IaCTestOptions = Pick<
Options & TestOptions,
| 'insecure'
| 'debug'
| 'experimental'
| 'detectionDepth'
| 'severityThreshold'
| 'json'
| 'sarif'
> & {
// Supported flags not yet covered by Options or TestOptions
'json-file-output'?: string;
'sarif-file-output'?: string;
v?: boolean;
version?: boolean;
h?: boolean;
help?: 'help';
q?: boolean;
quiet?: boolean;
};

export interface ProtectOptions {
interactive?: boolean;
newPolicy: boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { IaCTestOptions } from '../../src/lib/types';
import { assertIaCOptionsFlags } from '../../src/cli/commands/test/iac-local-execution/assert-iac-options-flag';
import { assertIaCOptionsFlags } from '../../../../src/cli/commands/test/iac-local-execution/assert-iac-options-flag';

describe('assertIaCOptionsFlags()', () => {
const command = ['node', 'cli', 'iac', 'test'];
Expand Down
10 changes: 5 additions & 5 deletions test/jest/unit/iac-unit-tests/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,24 @@ jest.mock('../../../../src/lib/detect', () => ({
import { test } from '../../../../src/cli/commands/test/iac-local-execution';
import {
IacFileParsed,
IacOptionFlags,
IaCTestFlags,
} from '../../../../src/cli/commands/test/iac-local-execution/types';
import { IacProjectType } from '../../../../src/lib/iac/constants';

describe('test()', () => {
it('extends the options object with iacDirFiles when a local directory is provided', async () => {
const opts: IacOptionFlags = {};
await test('./storage/', opts);
const opts: IaCTestFlags = {};
const { files } = await test('./storage/', opts);

expect(opts.iacDirFiles).toEqual([
expect(files).toEqual([
{
filePath: './storage/storage.tf',
fileType: 'tf',
failureReason: 'Mock Test',
projectType: IacProjectType.TERRAFORM,
},
]);
expect(opts.iacDirFiles).not.toEqual(
expect(files).not.toEqual(
expect.arrayContaining([
{
fileContent: 'FAKE_FILE_CONTENT',
Expand Down

0 comments on commit 98e4ddb

Please sign in to comment.