Skip to content

Commit

Permalink
feat: iac experimental single k8s file
Browse files Browse the repository at this point in the history
  • Loading branch information
rontalx committed Feb 1, 2021
1 parent fda908b commit 913c536
Show file tree
Hide file tree
Showing 7 changed files with 298 additions and 1 deletion.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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/iac-local-execution/ @snyk/cloudconfig
src/lib/cloud-config-projects.ts @snyk/cloudconfig
src/lib/iac/ @snyk/cloudconfig
src/lib/snyk-test/iac-test-result.ts @snyk/cloudconfig
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@
"author": "snyk.io",
"license": "Apache-2.0",
"dependencies": {
"@open-policy-agent/opa-wasm": "1.1.0",
"@snyk/cli-interface": "2.11.0",
"@snyk/cloud-config-parser": "^1.9.2",
"@snyk/dep-graph": "1.21.0",
"@snyk/gemfile": "1.2.0",
"@snyk/snyk-cocoapods-plugin": "2.5.1",
Expand Down
82 changes: 82 additions & 0 deletions src/cli/commands/test/iac-local-execution/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import * as fs from 'fs';
import * as YAML from 'js-yaml';
import { isLocalFolder } from '../../../../lib/detect';
import { getFileType } from '../../../../lib/iac/iac-parser';
import * as util from 'util';
import { IacFileTypes } from '../../../../lib/iac/constants';
import { IacFileScanResult, IacFileMetadata, IacFileData } from './types';
import { buildPolicyEngine } from './policy-engine';
import { transformToLegacyResults } from './legacy-adapter';

const readFileContentsAsync = util.promisify(fs.readFile);

export default async function legacyWrapper(pathToScan: string, options) {
const results = await localProcessing(pathToScan);
const legacyResults = transformToLegacyResults(results, options);
const singleFileLegacyResult = legacyResults[0];

return singleFileLegacyResult as any;
}

async function localProcessing(
pathToScan: string,
): Promise<IacFileScanResult[]> {
const policyEngine = await buildPolicyEngine();
const filePathsToScan = await getFilePathsToScan(pathToScan);
const fileDataToScan = await parseFileContentsForPolicyEngine(
filePathsToScan,
);
const scanResults = await policyEngine.scanFiles(fileDataToScan);
return scanResults;
}

async function getFilePathsToScan(pathToScan): Promise<IacFileMetadata[]> {
if (isLocalFolder(pathToScan)) {
throw new Error(
'IaC Experimental version does not support directory scan yet.',
);
}
if (getFileType(pathToScan) === 'tf') {
throw new Error(
'IaC Experimental version does not support Terraform scan yet.',
);
}

return [
{ filePath: pathToScan, fileType: getFileType(pathToScan) as IacFileTypes },
];
}

const REQUIRED_K8S_FIELDS = ['apiVersion', 'kind', 'metadata'];

async function parseFileContentsForPolicyEngine(
filesMetadata: IacFileMetadata[],
): Promise<IacFileData[]> {
const parsedFileData: Array<IacFileData> = [];
for (const fileMetadata of filesMetadata) {
const fileContent = await readFileContentsAsync(
fileMetadata.filePath,
'utf-8',
);
const yamlDocuments = YAML.safeLoadAll(fileContent);

yamlDocuments.forEach((parsedYamlDocument, docId) => {
if (
REQUIRED_K8S_FIELDS.every((requiredField) =>
parsedYamlDocument.hasOwnProperty(requiredField),
)
) {
parsedFileData.push({
...fileMetadata,
fileContent: fileContent,
jsonContent: parsedYamlDocument,
docId: yamlDocuments.length > 1 ? docId : undefined,
});
} else {
throw new Error('Invalid K8s File!');
}
});
}

return await Promise.all(parsedFileData);
}
115 changes: 115 additions & 0 deletions src/cli/commands/test/iac-local-execution/legacy-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { IacFileScanResult, PolicyMetadata } from './types';
import {
issuesToLineNumbers,
CloudConfigFileTypes,
} from '@snyk/cloud-config-parser';

export function transformToLegacyResults(
iacLocalExecutionResults: Array<IacFileScanResult>,
options: { severityThreshold?: string },
) {
const iacLocalExecutionGroupedResults = groupMultiDocResults(
iacLocalExecutionResults,
);
return iacLocalExecutionGroupedResults.map((iacScanResult) =>
iacLocalFileScanToLegacyResult(iacScanResult, options.severityThreshold),
);
}

function getLegacyFileTypeForLineNumber(
fileType: string,
): CloudConfigFileTypes {
switch (fileType) {
case 'yaml':
case 'yml':
return CloudConfigFileTypes.YAML;
case 'json':
return CloudConfigFileTypes.JSON;
default:
return CloudConfigFileTypes.YAML;
}
}

function iacLocalFileScanToLegacyResult(
iacFileScanResult: IacFileScanResult,
severityThreshold?: string,
) {
const legacyIssues = iacFileScanResult.violatedPolicies.map((policy) => {
const cloudConfigPath = iacFileScanResult.docId
? [`[DocId:${iacFileScanResult.docId}]`].concat(policy.msg.split('.'))
: policy.msg.split('.');
let lineNumber = -1;
try {
lineNumber = issuesToLineNumbers(
iacFileScanResult.fileContent,
getLegacyFileTypeForLineNumber(iacFileScanResult.fileType),
cloudConfigPath,
);
} catch (err) {
//
}

return {
...policy,
id: policy.publicId,
from: [],
name: policy.title,
cloudConfigPath,
isIgnored: false,
iacDescription: {
issue: policy.issue,
impact: policy.impact,
resolve: policy.resolve,
},
severity: policy.severity,
lineNumber: lineNumber,
};
});
return {
result: {
cloudConfigResults: filterPoliciesBySeverity(
legacyIssues,
severityThreshold,
),
},
packageManager: 'k8sconfig',
targetFile: iacFileScanResult.filePath,
};
}

function groupMultiDocResults(
scanResults: Array<IacFileScanResult>,
): Array<IacFileScanResult> {
const groupedData = scanResults.reduce((memo, result) => {
if (memo[result.filePath]) {
memo[result.filePath].violatedPolicies = memo[
result.filePath
].violatedPolicies.concat(result.violatedPolicies);
} else {
memo[result.filePath] = result;
}

return memo;
}, {} as IacFileScanResult);

return Object.keys(groupedData).map((k) => groupedData[k]);
}

const SEVERITIES = ['low', 'medium', 'high'];

function filterPoliciesBySeverity(
violatedPolicies: PolicyMetadata[],
severityThreshold?: string,
): PolicyMetadata[] {
if (!severityThreshold || severityThreshold === SEVERITIES[0]) {
return violatedPolicies;
}

const severitiesToInclude = SEVERITIES.slice(
SEVERITIES.indexOf(severityThreshold),
);

return violatedPolicies.filter(
(policy) => severitiesToInclude.indexOf(policy.severity) > -1,
);
}
58 changes: 58 additions & 0 deletions src/cli/commands/test/iac-local-execution/policy-engine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { OpaWasmInstance, IacFileData, IacFileScanResult } from './types';
import { loadPolicy } from '@open-policy-agent/opa-wasm';
import * as fs from 'fs';

const LOCAL_POLICY_ENGINE_DIR = `.iac-data`;
const LOCAL_POLICY_ENGINE_WASM_PATH = `${LOCAL_POLICY_ENGINE_DIR}/policy.wasm`;
const LOCAL_POLICY_ENGINE_DATA_PATH = `${LOCAL_POLICY_ENGINE_DIR}/data.json`;

export async function buildPolicyEngine(): Promise<PolicyEngine> {
const policyEngineCoreDataPath = `${process.cwd()}/${LOCAL_POLICY_ENGINE_WASM_PATH}`;
const policyEngineMetaDataPath = `${process.cwd()}/${LOCAL_POLICY_ENGINE_DATA_PATH}`;
try {
const wasmFile = fs.readFileSync(policyEngineCoreDataPath);
const policyMetaData = fs.readFileSync(policyEngineMetaDataPath);
const policyMetadataAsJson: Record<string, any> = JSON.parse(
policyMetaData.toString(),
);

const opaWasmInstance: OpaWasmInstance = await loadPolicy(
Buffer.from(wasmFile),
);
opaWasmInstance.setData(policyMetadataAsJson);

return new PolicyEngine(opaWasmInstance);
} catch (err) {
throw new Error(
`Failed to build policy engine from path: ${LOCAL_POLICY_ENGINE_DIR}: \n err: ${err.message}`,
);
}
}

class PolicyEngine {
opaWasmInstance: OpaWasmInstance;

constructor(opaWasmInstance: OpaWasmInstance) {
this.opaWasmInstance = opaWasmInstance;
}

private evaluate(data: Record<string, any>) {
return this.opaWasmInstance.evaluate(data)[0].result;
}

public async scanFiles(
filesToScan: IacFileData[],
): Promise<IacFileScanResult[]> {
try {
return filesToScan.map((iacFile: IacFileData) => {
const violatedPolicies = this.evaluate(iacFile.jsonContent);
return {
...iacFile,
violatedPolicies,
};
});
} catch (err) {
throw new Error(`Failed to run policy engine: ${err}`);
}
}
}
33 changes: 33 additions & 0 deletions src/cli/commands/test/iac-local-execution/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { IacFileInDirectory } from '../../../../lib/types';

// eslint-disable-next-line
export interface IacFileMetadata extends IacFileInDirectory {}
export interface IacFileData extends IacFileMetadata {
jsonContent: Record<string, any>;
fileContent: string;
docId?: number;
}
export interface IacFileScanResult extends IacFileData {
violatedPolicies: PolicyMetadata[];
}

export interface OpaWasmInstance {
evaluate: (data: Record<string, any>) => { results: PolicyMetadata[] };
setData: (data: Record<string, any>) => void;
}

export interface PolicyMetadata {
id: string;
publicId: string;
type: string;
subType: string;
title: string;
description: string;
severity: string;
msg: string;
policyEngineType: 'opa';
issue: string;
impact: string;
resolve: string;
references: string[];
}
8 changes: 7 additions & 1 deletion src/cli/commands/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ import {
getDisplayedOutput,
} from './formatters/format-test-results';

import iacLocalProcessing from './iac-local-execution';

const debug = Debug('snyk-test');
const SEPARATOR = '\n-------------------------------------------------------\n';

Expand Down Expand Up @@ -133,7 +135,11 @@ async function test(...args: MethodArgs): Promise<TestCommandResult> {
let res: (TestResult | TestResult[]) | Error;

try {
res = await snyk.test(path, testOpts);
if (options.iac && options.experimental) {
res = await iacLocalProcessing(path, options);
} else {
res = await snyk.test(path, testOpts);
}
if (testOpts.iacDirFiles) {
options.iacDirFiles = testOpts.iacDirFiles;
}
Expand Down

0 comments on commit 913c536

Please sign in to comment.