-
Notifications
You must be signed in to change notification settings - Fork 553
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
IaC local execution experimental release #1 #1601
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
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 { formatResults } from './results-formatter'; | ||
|
||
const readFileContentsAsync = util.promisify(fs.readFile); | ||
const REQUIRED_K8S_FIELDS = ['apiVersion', 'kind', 'metadata']; | ||
|
||
// 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) { | ||
// TODO: add support for proper typing of old TestResult interface. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is a future There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, as it is a typing rabbit-hole, and I prefer not having the old types in the CLI dictate how we build the new experience. |
||
const results = await localProcessing(pathToScan); | ||
const formattedResults = formatResults(results, options); | ||
const singleFileFormattedResult = formattedResults[0]; | ||
|
||
return singleFileFormattedResult as any; | ||
} | ||
|
||
async function localProcessing( | ||
pathToScan: string, | ||
): Promise<IacFileScanResult[]> { | ||
const policyEngine = await buildPolicyEngine(); | ||
rontalx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const filePathsToScan = await getFilePathsToScan(pathToScan); | ||
const fileDataToScan = await parseFileContentsForPolicyEngine( | ||
filePathsToScan, | ||
); | ||
const scanResults = await policyEngine.scanFiles(fileDataToScan); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe worth align this with the evaluate of CCPE (I saw that there is an internal evaluate function there - but maybe there it should be evaluateData or something similar) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will consider that for next release. |
||
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.', | ||
); | ||
} | ||
aron marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return [ | ||
{ filePath: pathToScan, fileType: getFileType(pathToScan) as IacFileTypes }, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we avoid this cast? Or document why it is needed. |
||
]; | ||
} | ||
|
||
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, | ||
}); | ||
} else { | ||
throw new Error('Invalid K8s File!'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we include the missing fields here to help with debugging. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @aviadatsnyk |
||
} | ||
}); | ||
} | ||
|
||
return parsedFileData; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { | ||
OpaWasmInstance, | ||
IacFileData, | ||
IacFileScanResult, | ||
PolicyMetadata, | ||
} from './types'; | ||
import { loadPolicy } from '@open-policy-agent/opa-wasm'; | ||
import * as fs from 'fs'; | ||
import * as path from 'path'; | ||
|
||
const LOCAL_POLICY_ENGINE_DIR = `.iac-data`; | ||
const LOCAL_POLICY_ENGINE_WASM_PATH = `${LOCAL_POLICY_ENGINE_DIR}${path.sep}policy.wasm`; | ||
const LOCAL_POLICY_ENGINE_DATA_PATH = `${LOCAL_POLICY_ENGINE_DIR}${path.sep}data.json`; | ||
|
||
export async function buildPolicyEngine(): Promise<PolicyEngine> { | ||
const policyEngineCoreDataPath = `${process.cwd()}/${LOCAL_POLICY_ENGINE_WASM_PATH}`; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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}`, | ||
); | ||
aron marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
class PolicyEngine { | ||
constructor(private opaWasmInstance: OpaWasmInstance) { | ||
this.opaWasmInstance = opaWasmInstance; | ||
} | ||
|
||
private evaluate(data: Record<string, any>): PolicyMetadata[] { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what if it throws? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
return this.opaWasmInstance.evaluate(data)[0].result; | ||
} | ||
|
||
public async scanFiles( | ||
aron marked this conversation as resolved.
Show resolved
Hide resolved
|
||
filesToScan: IacFileData[], | ||
): Promise<IacFileScanResult[]> { | ||
try { | ||
return filesToScan.map((iacFile: IacFileData) => { | ||
const violatedPolicies = this.evaluate(iacFile.jsonContent); | ||
return { | ||
...iacFile, | ||
violatedPolicies, | ||
}; | ||
}); | ||
} catch (err) { | ||
// TODO: to distinguish between different failure reasons | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we want to implement that? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not for now. |
||
throw new Error(`Failed to run policy engine: ${err}`); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import { IacFileScanResult, PolicyMetadata } from './types'; | ||
import { SEVERITY } from '../../../../lib/snyk-test/common'; | ||
// import { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please clean There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
// issuesToLineNumbers, | ||
// CloudConfigFileTypes, | ||
// } from '@snyk/cloud-config-parser'; | ||
|
||
const SEVERITIES = [SEVERITY.LOW, SEVERITY.MEDIUM, SEVERITY.HIGH]; | ||
|
||
export function formatResults( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should return a |
||
iacLocalExecutionResults: Array<IacFileScanResult>, | ||
options: { severityThreshold?: SEVERITY }, | ||
) { | ||
const iacLocalExecutionGroupedResults = groupMultiDocResults( | ||
iacLocalExecutionResults, | ||
); | ||
return iacLocalExecutionGroupedResults.map((iacScanResult) => | ||
iacLocalFileScanToFormattedResult(iacScanResult, options.severityThreshold), | ||
); | ||
} | ||
|
||
// | ||
// function getFileTypeForLineNumber( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹💨 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. generally I 100% agree. otherwise it will just get re-implemented soon. |
||
// fileType: string, | ||
// ): CloudConfigFileTypes { | ||
// switch (fileType) { | ||
// case 'yaml': | ||
// case 'yml': | ||
// return CloudConfigFileTypes.YAML; | ||
// case 'json': | ||
// return CloudConfigFileTypes.JSON; | ||
// default: | ||
// return CloudConfigFileTypes.YAML; | ||
// } | ||
// } | ||
|
||
function iacLocalFileScanToFormattedResult( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should return a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. absolutely correct, but for now adding a TODO on these as it's a typing rabbit-hole due to lack of accurate typing down the road in the rest of the CLI.. |
||
iacFileScanResult: IacFileScanResult, | ||
severityThreshold?: SEVERITY, | ||
) { | ||
const formattedIssues = iacFileScanResult.violatedPolicies.map((policy) => { | ||
// TODO: make sure we handle this issue with annotations: | ||
// https://github.com/snyk/registry/pull/17277 | ||
const cloudConfigPath = [`[DocId:${iacFileScanResult.docId}]`].concat( | ||
policy.msg.split('.'), | ||
); | ||
const lineNumber = -1; | ||
// TODO: once package becomes public, restore the commented out code for having the issue-to-line-number functionality | ||
// try { | ||
// lineNumber = issuesToLineNumbers( | ||
// iacFileScanResult.fileContent, | ||
// getFileTypeForLineNumber(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( | ||
formattedIssues, | ||
severityThreshold, | ||
), | ||
}, | ||
isPrivate: true, | ||
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.values(groupedData); | ||
} | ||
|
||
function filterPoliciesBySeverity( | ||
violatedPolicies: PolicyMetadata[], | ||
severityThreshold?: SEVERITY, | ||
): PolicyMetadata[] { | ||
if (!severityThreshold || severityThreshold === SEVERITY.LOW) { | ||
return violatedPolicies; | ||
} | ||
|
||
const severitiesToInclude = SEVERITIES.slice( | ||
SEVERITIES.indexOf(severityThreshold), | ||
); | ||
|
||
return violatedPolicies.filter((policy) => | ||
severitiesToInclude.includes(policy.severity), | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { SEVERITY } from '../../../../lib/snyk-test/common'; | ||
import { IacFileInDirectory } from '../../../../lib/types'; | ||
|
||
export type IacFileMetadata = 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: SEVERITY; | ||
msg: string; | ||
policyEngineType: 'opa'; | ||
issue: string; | ||
impact: string; | ||
resolve: string; | ||
references: string[]; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{".circleci":{"jobs":{"run_tests":{"docker":[{"image":"circleci/golang:1.14"}],"steps":["checkout",{"restore_cache":{"keys":["go-mod-v4-{{ checksum \"go.sum\" }}"]}},{"run":"go build"},{"save_cache":{"key":"go-mod-v4-{{ checksum \"go.sum\" }}","paths":["/go/pkg/mod"]}},{"run":{"command":"./cloud-config-opa-policies test .\n","name":"Run Tests"}}]}},"version":2.1,"workflows":{"build_and_push":{"jobs":["run_tests"]},"version":2}},"ecosystems":{"kubernetes":{"SNYK_CC_K8S_1":{"description":"","id":"1","impact":"Compromised container could potentially modify the underlying host’s kernel by loading unauthorized modules (i.e. drivers).","issue":"Container is running in privileged mode","policyEngineType":"opa","publicId":"SNYK-CC-K8S-1","references":["CIS Kubernetes Benchmark 1.6.0 - 5.2.1 Minimize the admission of privileged containers","https://kubernetes.io/docs/concepts/policy/pod-security-policy/#privileged","https://kubernetes.io/blog/2016/08/security-best-practices-kubernetes-deployment/"],"resolve":"Remove `securityContext.privileged` attribute, or set value to `false`","severity":"high","subType":"Deployment","title":"Container is running in privileged mode","type":"k8s"}}}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why are we binding to a git commit and not the
npm
distro?https://www.npmjs.com/package/@open-policy-agent/opa-wasm
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#1601 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
intentionally.
the package is not mature, and the latest tagged release is missing functionality that we are dependent on.
therefore, until they do a new release, we're targeting a specific commit-hash.
see:
open-policy-agent/npm-opa-wasm#31