From 9fe44b2f4a0080d570287aaf0c23951c333e7175 Mon Sep 17 00:00:00 2001 From: orkamara Date: Wed, 27 May 2020 15:00:57 +0300 Subject: [PATCH] feat: add Kubernetes configs detection logic --- src/lib/cloud-config/cloud-config-parser.ts | 93 +++++++++++++++++++++ src/lib/detect.ts | 25 ++++++ src/lib/errors/index.ts | 4 + src/lib/errors/invalid-cloud-config-file.ts | 40 +++++++++ 4 files changed, 162 insertions(+) create mode 100644 src/lib/cloud-config/cloud-config-parser.ts create mode 100644 src/lib/errors/invalid-cloud-config-file.ts diff --git a/src/lib/cloud-config/cloud-config-parser.ts b/src/lib/cloud-config/cloud-config-parser.ts new file mode 100644 index 00000000000..4a1cdac6266 --- /dev/null +++ b/src/lib/cloud-config/cloud-config-parser.ts @@ -0,0 +1,93 @@ +//TODO(orka): take out into a new lib +import * as YAML from 'js-yaml'; +import * as debugLib from 'debug'; +import { + IllegalCloudConfigFileError, + NotSupportedCloudConfigFileError, +} from './../errors'; + +const debug = debugLib('snyk-detect'); + +const mandatoryKeysForSupportedK8sKinds = { + deployment: ['apiVersion', 'metadata', 'spec'], + pod: ['apiVersion', 'metadata', 'spec'], + service: ['apiVersion', 'metadata', 'spec'], + podsecuritypolicy: ['apiVersion', 'metadata', 'spec'], + networkpolicy: ['apiVersion', 'metadata', 'spec'], +}; + +function getFileType(filePath: string): string { + const filePathSplit = filePath.split('.'); + return filePathSplit[filePathSplit.length - 1].toLowerCase(); +} + +function parseYamlOrJson(fileContent: string, filePath: string): any { + const fileType = getFileType(filePath); + switch (fileType) { + case 'yaml': + case 'yml': + try { + return YAML.safeLoadAll(fileContent); + } catch (e) { + debug('Failed to parse cloud config as a YAML'); + } + break; + case 'json': + try { + const objectsArr: any[] = []; + objectsArr.push(JSON.parse(fileContent)); + return objectsArr; + } catch (e) { + debug('Failed to parse cloud config as a JSON'); + } + break; + default: + debug(`Unsupported cloud config file type (${fileType})`); + } + return undefined; +} + +// This function validates that there is at least one valid doc with a k8s object kind. +// A valid k8s object has a kind key (.kind) from the keys of `mandatoryKeysForSupportedK8sKinds` +// and all of the keys from `mandatoryKeysForSupportedK8sKinds[kind]`. +// If there is a doc with a supported kind, but invalid, we should fail +// The function return true if the yaml is a valid k8s one, or false otherwise +export function validateK8sFile( + fileContent: string, + filePath: string, + root: string, +) { + const k8sObjects: any[] = parseYamlOrJson(fileContent, filePath); + if (!k8sObjects) { + throw IllegalCloudConfigFileError([root]); + } + + let numOfSupportedKeyDocs = 0; + for (let i = 0; i < k8sObjects.length; i++) { + const k8sObject = k8sObjects[i]; + if (!k8sObject || !k8sObject.kind) { + continue; + } + + const kind = k8sObject.kind.toLowerCase(); + if (!Object.keys(mandatoryKeysForSupportedK8sKinds).includes(kind)) { + continue; + } + + numOfSupportedKeyDocs++; + + for (let i = 0; i < mandatoryKeysForSupportedK8sKinds[kind].length; i++) { + const key = mandatoryKeysForSupportedK8sKinds[kind][i]; + if (!k8sObject[key]) { + debug(`Missing key (${key}) from supported k8s object kind (${kind})`); + throw IllegalCloudConfigFileError([root]); + } + } + } + + if (numOfSupportedKeyDocs === 0) { + throw NotSupportedCloudConfigFileError([root]); + } + + debug(`k8s config found (${filePath})`); +} diff --git a/src/lib/detect.ts b/src/lib/detect.ts index 643fcba40a0..3ffea485ee1 100644 --- a/src/lib/detect.ts +++ b/src/lib/detect.ts @@ -4,6 +4,7 @@ import * as debugLib from 'debug'; import * as _ from '@snyk/lodash'; import { NoSupportedManifestsFoundError } from './errors'; import { SupportedPackageManagers } from './package-managers'; +import { validateK8sFile } from './cloud-config/cloud-config-parser'; const debug = debugLib('snyk-detect'); @@ -137,6 +138,30 @@ export function detectPackageManager(root: string, options) { return packageManager; } +export function isCloudConfigProject(root: string, options): string { + if (!isLocalFolder(root)) { + debug('Cloud Config - repo case ' + root); + throw "iac option doesn't support lookup as repo"; + } + + if (!options.file) { + debug('Cloud Config - no file specified ' + root); + throw 'iac option works only with specified files'; + } + + if (localFileSuppliedButNotFound(root, options.file)) { + throw new Error( + 'Could not find the specified file: ' + + options.file + + '\nPlease check that it exists and try again.', + ); + } + const filePath = pathLib.resolve(root, options.file); + const fileContent = fs.readFileSync(filePath, 'utf-8'); + validateK8sFile(fileContent, filePath, root); + return 'k8sconfig'; +} + // User supplied a "local" file, but that file doesn't exist function localFileSuppliedButNotFound(root, file) { return ( diff --git a/src/lib/errors/index.ts b/src/lib/errors/index.ts index adfa47de0a8..2bc2a7457dd 100644 --- a/src/lib/errors/index.ts +++ b/src/lib/errors/index.ts @@ -19,3 +19,7 @@ export { OptionMissingErrorError } from './option-missing-error'; export { ExcludeFlagBadInputError } from './exclude-flag-bad-input'; export { UnsupportedOptionCombinationError } from './unsupported-option-combination-error'; export { FeatureNotSupportedByPackageManagerError } from './feature-not-supported-by-package-manager-error'; +export { + NotSupportedCloudConfigFileError, + IllegalCloudConfigFileError, +} from './invalid-cloud-config-file'; diff --git a/src/lib/errors/invalid-cloud-config-file.ts b/src/lib/errors/invalid-cloud-config-file.ts new file mode 100644 index 00000000000..d8497bfba80 --- /dev/null +++ b/src/lib/errors/invalid-cloud-config-file.ts @@ -0,0 +1,40 @@ +import chalk from 'chalk'; +import { CustomError } from './custom-error'; + +export function NotSupportedCloudConfigFileError(atLocations: string[]) { + const locationsStr = atLocations.join(', '); + const errorMsg = + 'Not supported Cloud Config target files in ' + + locationsStr + + '.\nPlease see our documentation for supported languages and ' + + 'target files: ' + + chalk.underline( + 'https://support.snyk.io/hc/en-us/articles/360000911957-Language-support', + ) + + ' and make sure you are in the right directory.'; + + const error = new CustomError(errorMsg); + error.code = 422; + error.userMessage = errorMsg; + return error; +} + +export function IllegalCloudConfigFileError( + atLocations: string[], +): CustomError { + const locationsStr = atLocations.join(', '); + const errorMsg = + 'Illegal Cloud Config target file ' + + locationsStr + + '.\nPlease see our documentation for supported languages and ' + + 'target files: ' + + chalk.underline( + 'https://support.snyk.io/hc/en-us/articles/360000911957-Language-support', + ) + + ' and make sure you are in the right directory.'; + + const error = new CustomError(errorMsg); + error.code = 422; + error.userMessage = errorMsg; + return error; +}