Skip to content

Commit

Permalink
feat: iac test terraform support
Browse files Browse the repository at this point in the history
  • Loading branch information
rontalx committed Sep 21, 2020
1 parent a4ce353 commit c32824e
Show file tree
Hide file tree
Showing 19 changed files with 249 additions and 25 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -12,5 +12,6 @@ src/lib/snyk-test/payload-schema.ts @snyk/cloudconfig
src/lib/snyk-test/run-iac-test.ts @snyk/cloudconfig
test/acceptance/cli-test/cli-test.iac-k8s.spec.ts @snyk/cloudconfig
test/acceptance/cli-test/cli-test.iac-k8s.utils.ts @snyk/cloudconfig
test/fixtures/iac-terraform/* @snyk/cloudconfig
src/lib/errors/invalid-iac-file.ts @snyk/cloudconfig
src/lib/errors/unsupported-options-iac-error.ts @snyk/cloudconfig
3 changes: 2 additions & 1 deletion help/iac.txt
Expand Up @@ -2,7 +2,7 @@ Usage:

$ snyk iac [command] [options] <path>

Find security issues in your Infrastructure as Code files (currently we support Kubernetes files only).
Find security issues in your Infrastructure as Code files (currently we support Kubernetes & Terraform files only).

Commands:

Expand All @@ -26,6 +26,7 @@ Options:
Examples:

$ snyk iac test /path/to/Kubernetes.yaml
$ snyk iac test /path/to/terraform_file.tf


For more information see https://support.snyk.io/hc/en-us/categories/360001342678-Infrastructure-as-code
15 changes: 12 additions & 3 deletions src/cli/commands/test/iac-output.ts
Expand Up @@ -150,6 +150,11 @@ function getIssueLevel(severity: SEVERITY): sarif.ReportingConfiguration.level {
return severity === SEVERITY.HIGH ? 'error' : 'warning';
}

const iacTypeToText = {
k8s: 'Kubernetes',
terraform: 'Terraform',
};

export function mapIacTestResponseToSarifTool(
iacTestResponse: IacTestResponse,
): sarif.Tool {
Expand All @@ -172,7 +177,7 @@ export function mapIacTestResponseToSarifTool(
text: `${upperFirst(iacIssue.severity)} - ${iacIssue.title}`,
},
fullDescription: {
text: `Kubernetes ${iacIssue.subType}`,
text: `${iacTypeToText[iacIssue.type]} ${iacIssue.subType}`,
},
help: {
text: '',
Expand All @@ -182,7 +187,7 @@ export function mapIacTestResponseToSarifTool(
level: getIssueLevel(iacIssue.severity),
},
properties: {
tags: ['security', `kubernetes/${iacIssue.subType}`],
tags: ['security', `${iacIssue.type}/${iacIssue.subType}`],
},
});
pushedIds[iacIssue.id] = true;
Expand All @@ -198,7 +203,11 @@ export function mapIacTestResponseToSarifResults(
(iacIssue: AnnotatedIacIssue) => ({
ruleId: iacIssue.id,
message: {
text: `This line contains a potential ${iacIssue.severity} severity misconfiguration affacting the Kubernetes ${iacIssue.subType}`,
text: `This line contains a potential ${
iacIssue.severity
} severity misconfiguration affecting the ${
iacTypeToText[iacIssue.type]
} ${iacIssue.subType}`,
},
locations: [
{
Expand Down
12 changes: 10 additions & 2 deletions src/cli/commands/test/index.ts
Expand Up @@ -53,6 +53,11 @@ import { getEcosystem, testEcosystem } from '../../../lib/ecosystems';
import { TestLimitReachedError } from '../../../lib/errors';
import { isMultiProjectScan } from '../../../lib/is-multi-project-scan';
import { createSarifOutputForContainers } from './sarif-output';
import {
IacProjectType,
IacProjectTypes,
TEST_SUPPORTED_IAC_PROJECTS,
} from '../../../lib/iac/constants';

const debug = Debug('snyk-test');
const SEPARATOR = '\n-------------------------------------------------------\n';
Expand Down Expand Up @@ -448,7 +453,8 @@ function displayResult(
return prefix + res.message;
}
const issuesText =
res.licensesPolicy || projectType === 'k8sconfig'
res.licensesPolicy ||
TEST_SUPPORTED_IAC_PROJECTS.includes(projectType as IacProjectTypes)
? 'issues'
: 'vulnerabilities';
let pathOrDepsText = '';
Expand Down Expand Up @@ -520,7 +526,9 @@ function displayResult(
);
}

if (res.packageManager === 'k8sconfig') {
if (
TEST_SUPPORTED_IAC_PROJECTS.includes(res.packageManager as IacProjectType)
) {
return getIacDisplayedOutput(
(res as any) as IacTestResponse,
testedInfoText,
Expand Down
34 changes: 29 additions & 5 deletions src/lib/detect.ts
Expand Up @@ -2,13 +2,18 @@ import * as fs from 'fs';
import * as pathLib from 'path';
import * as debugLib from 'debug';
import * as _ from 'lodash';
import { NoSupportedManifestsFoundError } from './errors';
import { IllegalIacFileError, NoSupportedManifestsFoundError } from './errors';
import { SupportedPackageManagers } from './package-managers';
import { validateK8sFile } from './iac/iac-parser';
import {
validateK8sFile,
makeValidateTerraformRequest,
} from './iac/iac-parser';
import { projectTypeByFileType, IacProjectType } from './iac/constants';
import {
SupportLocalFileOnlyIacError,
UnsupportedOptionFileIacError,
} from './errors/unsupported-options-iac-error';
import { IllegalTerraformFileError } from './errors/invalid-iac-file';

const debug = debugLib('snyk-detect');

Expand Down Expand Up @@ -145,7 +150,7 @@ export function detectPackageManager(root: string, options) {
return packageManager;
}

export function isIacProject(root: string, options): string {
export async function isIacProject(root: string, options): Promise<string> {
if (options.file) {
debug('Iac - --file specified ' + options.file);
throw UnsupportedOptionFileIacError(options.file);
Expand All @@ -162,8 +167,27 @@ export function isIacProject(root: string, options): string {

const filePath = pathLib.resolve(root, '.');
const fileContent = fs.readFileSync(filePath, 'utf-8');
validateK8sFile(fileContent, filePath, root);
return 'k8sconfig';
const fileType = root.substr(root.lastIndexOf('.') + 1);
const projectType = projectTypeByFileType[fileType];
switch (projectType) {
case IacProjectType.K8S:
validateK8sFile(fileContent, filePath, root);
break;
case IacProjectType.TERRAFORM: {
const {
isValidTerraformFile,
reason,
} = await makeValidateTerraformRequest(fileContent);
if (!isValidTerraformFile) {
throw IllegalTerraformFileError([root], reason);
}
break;
}
default:
throw IllegalIacFileError([root]);
}

return projectType;
}

// User supplied a "local" file, but that file doesn't exist
Expand Down
19 changes: 19 additions & 0 deletions src/lib/errors/invalid-iac-file.ts
Expand Up @@ -34,3 +34,22 @@ export function IllegalIacFileError(atLocations: string[]): CustomError {
error.userMessage = errorMsg;
return error;
}

export function IllegalTerraformFileError(
atLocations: string[],
reason: string,
): CustomError {
const locationsStr = atLocations.join(', ');
const errorMsg =
`Illegal Terraform target file ${locationsStr} \nValidation Error Reason: ${reason}` +
'.\nPlease see our documentation for supported target files (currently we support Kubernetes files only): ' +
chalk.underline(
'https://support.snyk.io/hc/en-us/articles/360006368877-Scan-and-fix-security-issues-in-your-Kubernetes-configuration-files',
) +
' and make sure you are in the right directory.';

const error = new CustomError(errorMsg);
error.code = 422;
error.userMessage = errorMsg;
return error;
}
26 changes: 26 additions & 0 deletions src/lib/iac/constants.ts
@@ -0,0 +1,26 @@
export type IacProjectTypes = 'k8sconfig' | 'terraformconfig';
export type IacFileTypes = 'yaml' | 'yml' | 'json' | 'tf';

export enum IacProjectType {
K8S = 'k8sconfig',
TERRAFORM = 'terraformconfig',
}

export const TEST_SUPPORTED_IAC_PROJECTS: IacProjectTypes[] = [
IacProjectType.K8S,
IacProjectType.TERRAFORM,
];

export const projectTypeByFileType = {
yaml: IacProjectType.K8S,
yml: IacProjectType.K8S,
json: IacProjectType.K8S,
tf: IacProjectType.TERRAFORM,
};

export type IacValidateTerraformResponse = {
body?: {
isValidTerraformFile: boolean;
reason: string;
};
};
29 changes: 28 additions & 1 deletion src/lib/iac/iac-parser.ts
Expand Up @@ -2,6 +2,10 @@
import * as YAML from 'js-yaml';
import * as debugLib from 'debug';
import { IllegalIacFileError, NotSupportedIacFileError } from '../errors';
import request = require('../request');
import { api as getApiToken } from '../api-token';
import * as config from './../config';
import { IacValidateTerraformResponse } from './constants';

const debug = debugLib('snyk-detect');

Expand All @@ -13,7 +17,7 @@ const mandatoryKeysForSupportedK8sKinds = {
networkpolicy: ['apiVersion', 'metadata', 'spec'],
};

function getFileType(filePath: string): string {
export function getFileType(filePath: string): string {
const filePathSplit = filePath.split('.');
return filePathSplit[filePathSplit.length - 1].toLowerCase();
}
Expand Down Expand Up @@ -88,3 +92,26 @@ export function validateK8sFile(

debug(`k8s config found (${filePath})`);
}

export async function makeValidateTerraformRequest(
terraformFileContent: string,
): Promise<{
isValidTerraformFile: boolean;
reason: string;
}> {
const response = (await request({
body: {
contentBase64: Buffer.from(terraformFileContent).toString('base64'),
},
url: `${config.API}/iac-validate/terraform`,
method: 'POST',
json: true,
headers: {
Authorization: `token ${getApiToken()}`,
},
})) as IacValidateTerraformResponse;
return {
isValidTerraformFile: !!response.body?.isValidTerraformFile,
reason: response.body?.reason || '',
};
}
3 changes: 0 additions & 3 deletions src/lib/iac/iac-projects.ts

This file was deleted.

6 changes: 3 additions & 3 deletions src/lib/snyk-test/index.js
Expand Up @@ -4,7 +4,7 @@ const detect = require('../detect');
const { runTest } = require('./run-test');
const chalk = require('chalk');
const pm = require('../package-managers');
const iacProjects = require('../iac/iac-projects');
const iacProjects = require('../iac/constants');
const {
UnsupportedPackageManagerError,
NotSupportedIacFileError,
Expand All @@ -30,11 +30,11 @@ async function test(root, options, callback) {
return promise;
}

function executeTest(root, options) {
async function executeTest(root, options) {
try {
if (!options.allProjects) {
options.packageManager = options.iac
? detect.isIacProject(root, options)
? await detect.isIacProject(root, options)
: detect.detectPackageManager(root, options);
}
return run(root, options).then((results) => {
Expand Down
4 changes: 2 additions & 2 deletions src/lib/snyk-test/payload-schema.ts
@@ -1,5 +1,5 @@
//TODO(orka): future - change this file
import { IacProjectTypes } from '../iac/iac-projects';
import { IacProjectTypes } from '../iac/constants';

interface Scan {
type: string;
Expand All @@ -9,7 +9,7 @@ interface Scan {

export interface IacFile {
fileContent: string;
fileType: 'yaml' | 'yml' | 'json';
fileType: 'yaml' | 'yml' | 'json' | 'tf';
}

export interface IacScan extends Scan {
Expand Down
9 changes: 6 additions & 3 deletions src/lib/snyk-test/run-iac-test.ts
Expand Up @@ -12,6 +12,7 @@ import { Payload } from './types';
import { IacScan } from './payload-schema';
import { SEVERITY } from './legacy';
import * as pathLib from 'path';
import { projectTypeByFileType, IacFileTypes } from '../iac/constants';

export async function parseIacTestResult(
res: IacTestResponse,
Expand Down Expand Up @@ -52,20 +53,22 @@ export async function assembleIacLocalPayloads(
: '';

const fileContent = fs.readFileSync(targetFile, 'utf8');
const fileType = root.substr(root.lastIndexOf('.') + 1);
const projectType = projectTypeByFileType[fileType];

const body: IacScan = {
data: {
fileContent,
fileType: 'yaml',
fileType: fileType as IacFileTypes,
},
targetFile: root,
type: 'k8sconfig',
type: projectType,
//TODO(orka): future - support policy
policy: '',
targetFileRelativePath: `${targetFileRelativePath}`, // Forcing string
originalProjectName: path.basename(path.dirname(targetFile)),
projectNameOverride: options.projectName,
};

const payload: Payload = {
method: 'POST',
url: config.API + (options.vulnEndpoint || '/test-iac'),
Expand Down
2 changes: 1 addition & 1 deletion src/lib/types.ts
@@ -1,5 +1,5 @@
import { SupportedPackageManagers } from './package-managers';
import { IacProjectTypes } from './iac/iac-projects';
import { IacProjectTypes } from './iac/constants';
import { legacyCommon as legacyApi } from '@snyk/cli-interface';
import { SEVERITY } from './snyk-test/legacy';
import { FailOn } from './snyk-test/common';
Expand Down
2 changes: 1 addition & 1 deletion test/acceptance/cli-test/cli-test.iac-k8s.utils.ts
Expand Up @@ -225,7 +225,7 @@ export function iacTestSarifAssertions(
const expectedIssue = expectedResults.infrastructureAsCodeIssues[i];
t.deepEqual(sarifIssue.ruleId, expectedIssue.id, 'issue id is ok');

const messageText = `This line contains a potential ${expectedIssue.severity} severity misconfiguration affacting the Kubernetes ${expectedIssue.subType}`;
const messageText = `This line contains a potential ${expectedIssue.severity} severity misconfiguration affecting the Kubernetes ${expectedIssue.subType}`;
t.deepEqual(sarifIssue.message.text, messageText, 'issue message is ok');
t.deepEqual(
sarifIssue.locations![0].physicalLocation!.region!.startLine,
Expand Down
12 changes: 12 additions & 0 deletions test/fixtures/iac-terraform/sg_open_ssh.tf
@@ -0,0 +1,12 @@
resource "aws_security_group" "allow_ssh" {
name = "allow_ssh"
description = "Allow SSH inbound from anywhere"
vpc_id = "${aws_vpc.main.id}"

ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
12 changes: 12 additions & 0 deletions test/fixtures/iac-terraform/sg_open_ssh_invalid_go_templates.tf
@@ -0,0 +1,12 @@
resource "aws_security_group" "allow_ssh" {
name = "allow_ssh"
description = "Allow SSH inbound from anywhere"
vpc_id = "${aws_vpc.main.id}"
{{ Disallowed usage of Go Templates! }}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
12 changes: 12 additions & 0 deletions test/fixtures/iac-terraform/sg_open_ssh_invalid_hcl2.tf
@@ -0,0 +1,12 @@
resource "aws_security_group" "allow_ssh" {
name = "allow_ssh"
description = "Allow SSH inbound from anywhere"
vpc_id = "${aws_vpc.main.id}"

ingress
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
1 change: 1 addition & 0 deletions test/smoke/alpine/Dockerfile
Expand Up @@ -3,6 +3,7 @@ FROM shellspec/shellspec:latest
COPY ./smoke/ /snyk/smoke/
COPY ./fixtures/basic-npm/ /snyk/fixtures/basic-npm/
COPY ./fixtures/empty/ /snyk/fixtures/empty/
COPY ./fixtures/iac-terraform/ /snyk/fixtures/iac-terraform/

RUN shellspec --version
RUN apk add curl jq libgcc libstdc++
Expand Down

0 comments on commit c32824e

Please sign in to comment.