From 626ff6d14d932ef20d63a8dd045727d89a54b38a Mon Sep 17 00:00:00 2001 From: ghe Date: Fri, 5 Mar 2021 16:09:01 +0000 Subject: [PATCH 1/9] feat: introduce `snyk fix` CLI command `snyk fix` uses @snyk/fix package to auto fix vulnerabilities after `snyk test` is run behind the scenes --- packages/snyk-fix/src/index.ts | 1 + .../test/unit/__snapshots__/fix.spec.ts.snap | 42 +-- .../fix/convert-legacy-test-result-to-new.ts | 15 + ...nvert-legacy-test-result-to-scan-result.ts | 25 ++ ...rt-legacy-tests-results-to-fix-entities.ts | 27 ++ src/cli/commands/fix/index.ts | 63 ++++ .../fix/validate-fix-command-is-supported.ts | 36 ++ src/cli/commands/index.js | 1 + ...nyk-test-error.ts => format-test-error.ts} | 2 +- src/cli/commands/test/index.ts | 14 +- src/cli/modes.ts | 2 +- src/lib/ecosystems/types.ts | 2 + src/lib/errors/command-not-supported.ts | 17 + src/lib/errors/not-supported-by-ecosystem.ts | 18 + src/lib/types.ts | 1 + test/__snapshots__/cli-fix.spec.ts.snap | 75 ++++ .../test-graph-results.json | 327 ++++++++++++++++++ test/cli-fix.spec.ts | 283 +++++++++++++++ ...ert-legacy-test-result-to-new.spec.ts.snap | 177 ++++++++++ ...cy-test-result-to-scan-result.spec.ts.snap | 52 +++ ...tests-results-to-fix-entities.spec.ts.snap | 243 +++++++++++++ .../convert-legacy-test-result-to-new.spec.ts | 51 +++ ...-legacy-test-result-to-scan-result.spec.ts | 50 +++ ...gacy-tests-results-to-fix-entities.spec.ts | 60 ++++ .../validate-fix-command-is-supported.spec.ts | 60 ++++ 25 files changed, 1614 insertions(+), 30 deletions(-) create mode 100644 src/cli/commands/fix/convert-legacy-test-result-to-new.ts create mode 100644 src/cli/commands/fix/convert-legacy-test-result-to-scan-result.ts create mode 100644 src/cli/commands/fix/convert-legacy-tests-results-to-fix-entities.ts create mode 100644 src/cli/commands/fix/index.ts create mode 100644 src/cli/commands/fix/validate-fix-command-is-supported.ts rename src/cli/commands/test/{generate-snyk-test-error.ts => format-test-error.ts} (93%) create mode 100644 src/lib/errors/command-not-supported.ts create mode 100644 src/lib/errors/not-supported-by-ecosystem.ts create mode 100644 test/__snapshots__/cli-fix.spec.ts.snap create mode 100644 test/acceptance/fixtures/pip-app-with-remediation/test-graph-results.json create mode 100644 test/cli-fix.spec.ts create mode 100644 test/lib/__snapshots__/convert-legacy-test-result-to-new.spec.ts.snap create mode 100644 test/lib/__snapshots__/convert-legacy-test-result-to-scan-result.spec.ts.snap create mode 100644 test/lib/__snapshots__/convert-legacy-tests-results-to-fix-entities.spec.ts.snap create mode 100644 test/lib/convert-legacy-test-result-to-new.spec.ts create mode 100644 test/lib/convert-legacy-test-result-to-scan-result.spec.ts create mode 100644 test/lib/convert-legacy-tests-results-to-fix-entities.spec.ts create mode 100644 test/validate-fix-command-is-supported.spec.ts diff --git a/packages/snyk-fix/src/index.ts b/packages/snyk-fix/src/index.ts index 629a36468b..36f0ebd166 100644 --- a/packages/snyk-fix/src/index.ts +++ b/packages/snyk-fix/src/index.ts @@ -7,6 +7,7 @@ import stripAnsi = require('strip-ansi'); import * as outputFormatter from './lib/output-formatters/show-results-summary'; import { loadPlugin } from './plugins/load-plugin'; import { FixHandlerResultByPlugin } from './plugins/types'; +export { EntityToFix } from './types'; import { EntityToFix, ErrorsByEcoSystem, FixedMeta, FixOptions } from './types'; import { convertErrorToUserMessage } from './lib/errors/error-to-user-message'; diff --git a/packages/snyk-fix/test/unit/__snapshots__/fix.spec.ts.snap b/packages/snyk-fix/test/unit/__snapshots__/fix.spec.ts.snap index 35c12d2eec..c9aed4a536 100644 --- a/packages/snyk-fix/test/unit/__snapshots__/fix.spec.ts.snap +++ b/packages/snyk-fix/test/unit/__snapshots__/fix.spec.ts.snap @@ -2,7 +2,7 @@ exports[`Error handling Snyk fix returns error when called with unsupported type 1`] = ` Object { - "exceptions": Object { + "exceptionsByScanType": Object { "npm": Object { "originals": Array [ Object { @@ -57,22 +57,18 @@ Object { "userMessage": "npm is not supported.", }, }, - "fixSummary": "βœ– No successful fixes + "fixSummary": "βœ– No successful fixes -Unresolved items: +Unresolved items: package.json - βœ– npm is not supported. + βœ– npm is not supported. -Summary: +Summary: - 1 items were not fixed - 0 items were successfully fixed", - "meta": Object { - "failed": 1, - "fixed": 0, - }, - "results": Object {}, + 1 items were not fixed + 0 items were successfully fixed", + "resultsByPlugin": Object {}, } `; @@ -161,26 +157,22 @@ Summary: exports[`Snyk fix Snyk fix returns results for supported & unsupported type 1`] = ` Object { - "exceptions": Object {}, - "fixSummary": "Successful fixes: + "exceptionsByScanType": Object {}, + "fixSummary": "Successful fixes: requirements.txt - βœ” Upgraded django from 1.6.1 to 2.0.1 + βœ” Pinned django from 1.6.1 to 2.0.1 -Unresolved items: +Unresolved items: Pipfile - βœ– Pipfile is not supported + βœ– Pipfile is not supported -Summary: +Summary: - 1 items were not fixed - 1 items were successfully fixed", - "meta": Object { - "failed": 1, - "fixed": 1, - }, - "results": Object { + 1 items were not fixed + 1 items were successfully fixed", + "resultsByPlugin": Object { "python": Object { "failed": Array [], "skipped": Array [ diff --git a/src/cli/commands/fix/convert-legacy-test-result-to-new.ts b/src/cli/commands/fix/convert-legacy-test-result-to-new.ts new file mode 100644 index 0000000000..c0d2d8fe24 --- /dev/null +++ b/src/cli/commands/fix/convert-legacy-test-result-to-new.ts @@ -0,0 +1,15 @@ +import { DepGraphData } from '@snyk/dep-graph'; +import { TestResult } from '../../../lib/ecosystems/types'; +import { TestResult as LegacyTestResult } from '../../../lib/snyk-test/legacy'; + +export function convertLegacyTestResultToNew( + testResult: LegacyTestResult, +): TestResult { + return { + issuesData: {} as any, // TODO: add converter + issues: [], // TODO: add converter + remediation: testResult.remediation, + // TODO: grab this once Ecosystems flow starts sending back ScanResult + depGraphData: {} as DepGraphData, + }; +} diff --git a/src/cli/commands/fix/convert-legacy-test-result-to-scan-result.ts b/src/cli/commands/fix/convert-legacy-test-result-to-scan-result.ts new file mode 100644 index 0000000000..088bc107ac --- /dev/null +++ b/src/cli/commands/fix/convert-legacy-test-result-to-scan-result.ts @@ -0,0 +1,25 @@ +import { ScanResult } from '../../../lib/ecosystems/types'; +import { TestResult } from '../../../lib/snyk-test/legacy'; + +export function convertLegacyTestResultToScanResult( + testResult: TestResult, +): ScanResult { + if (!testResult.packageManager) { + throw new Error( + 'Only results with packageManagers are supported for conversion', + ); + } + return { + identity: { + type: testResult.packageManager, + // this is because not all plugins send it back today, but we should always have it + targetFile: testResult.targetFile || testResult.displayTargetFile, + }, + name: testResult.projectName, + // TODO: grab this once Ecosystems flow starts sending back ScanResult + facts: [], + policy: testResult.policy, + // TODO: grab this once Ecosystems flow starts sending back ScanResult + target: {} as any, + }; +} diff --git a/src/cli/commands/fix/convert-legacy-tests-results-to-fix-entities.ts b/src/cli/commands/fix/convert-legacy-tests-results-to-fix-entities.ts new file mode 100644 index 0000000000..f9e7287606 --- /dev/null +++ b/src/cli/commands/fix/convert-legacy-tests-results-to-fix-entities.ts @@ -0,0 +1,27 @@ +import * as fs from 'fs'; +import * as pathLib from 'path'; +import { convertLegacyTestResultToNew } from './convert-legacy-test-result-to-new'; +import { convertLegacyTestResultToScanResult } from './convert-legacy-test-result-to-scan-result'; +import { TestResult } from '../../../lib/snyk-test/legacy'; + +export function convertLegacyTestResultToFixEntities( + testResults: (TestResult | TestResult[]) | Error, + root: string, +): any { + if (testResults instanceof Error) { + return []; + } + const oldResults = Array.isArray(testResults) ? testResults : [testResults]; + return oldResults.map((res) => ({ + workspace: { + readFile: async (path: string) => { + return fs.readFileSync(pathLib.resolve(root, path), 'utf8'); + }, + writeFile: async (path: string, content: string) => { + return fs.writeFileSync(pathLib.resolve(root, path), content, 'utf8'); + }, + }, + scanResult: convertLegacyTestResultToScanResult(res), + testResult: convertLegacyTestResultToNew(res), + })); +} diff --git a/src/cli/commands/fix/index.ts b/src/cli/commands/fix/index.ts new file mode 100644 index 0000000000..11042d9008 --- /dev/null +++ b/src/cli/commands/fix/index.ts @@ -0,0 +1,63 @@ +export = fix; + +import * as Debug from 'debug'; +import * as snykFix from '@snyk/fix'; + +import { MethodArgs } from '../../args'; +import * as snyk from '../../../lib'; +import { TestResult } from '../../../lib/snyk-test/legacy'; + +import { convertLegacyTestResultToFixEntities } from './convert-legacy-tests-results-to-fix-entities'; +import { formatTestError } from '../test/format-test-error'; +import { processCommandArgs } from '../process-command-args'; +import { validateCredentials } from '../test/validate-credentials'; +import { validateTestOptions } from '../test/validate-test-options'; +import { setDefaultTestOptions } from '../test/set-default-test-options'; +import { validateFixCommandIsSupported } from './validate-fix-command-is-supported'; + +const debug = Debug('snyk-fix'); +const snykFixFeatureFlag = 'cliSnykFix'; + +async function fix(...args: MethodArgs): Promise { + const { options: rawOptions, paths } = await processCommandArgs(...args); + const options = setDefaultTestOptions(rawOptions); + await validateFixCommandIsSupported(options); + validateTestOptions(options); + validateCredentials(options); + + const results: snykFix.EntityToFix[] = []; + + // TODO: once project envelope is default all code below will be deleted + for (const path of paths) { + // Create a copy of the options so a specific test can + // modify them i.e. add `options.file` etc. We'll need + // these options later. + const snykTestOptions = { + ...options, + path, + projectName: options['project-name'], + }; + + let testResults: TestResult | TestResult[]; + + try { + testResults = await snyk.test(path, snykTestOptions); + } catch (error) { + const testError = formatTestError(error); + // TODO: what should be done here? + console.error(testError); + throw new Error(error); + } + const resArray = Array.isArray(testResults) ? testResults : [testResults]; + const newRes = convertLegacyTestResultToFixEntities(resArray, path); + results.push(...newRes); + } + + // fix + debug( + `Organization has ${snykFixFeatureFlag} feature flag enabled for experimental Snyk fix functionality`, + ); + await snykFix.fix(results); + // TODO: what is being returned if anything? + return ''; +} diff --git a/src/cli/commands/fix/validate-fix-command-is-supported.ts b/src/cli/commands/fix/validate-fix-command-is-supported.ts new file mode 100644 index 0000000000..9a3180ec84 --- /dev/null +++ b/src/cli/commands/fix/validate-fix-command-is-supported.ts @@ -0,0 +1,36 @@ +import * as Debug from 'debug'; + +import { getEcosystemForTest } from '../../../lib/ecosystems'; + +import { isFeatureFlagSupportedForOrg } from '../../../lib/feature-flags'; +import { CommandNotSupportedError } from '../../../lib/errors/command-not-supported'; +import { FeatureNotSupportedByEcosystemError } from '../../../lib/errors/not-supported-by-ecosystem'; +import { Options, TestOptions } from '../../../lib/types'; + +const debug = Debug('snyk-fix'); +const snykFixFeatureFlag = 'cliSnykFix'; + +export async function validateFixCommandIsSupported( + options: Options & TestOptions, +): Promise { + if (options.docker) { + throw new FeatureNotSupportedByEcosystemError('snyk fix', 'docker'); + } + + const ecosystem = getEcosystemForTest(options); + if (ecosystem) { + throw new FeatureNotSupportedByEcosystemError('snyk fix', ecosystem); + } + + const snykFixSupported = await isFeatureFlagSupportedForOrg( + snykFixFeatureFlag, + options.org, + ); + + if (!snykFixSupported.ok) { + debug(snykFixSupported.userMessage); + throw new CommandNotSupportedError('snyk fix', options.org || undefined); + } + + return true; +} diff --git a/src/cli/commands/index.js b/src/cli/commands/index.js index 2362387bae..d69fe0d25f 100644 --- a/src/cli/commands/index.js +++ b/src/cli/commands/index.js @@ -12,6 +12,7 @@ const commands = { ignore: hotload('./ignore'), modules: hotload('./modules'), monitor: hotload('./monitor'), + fix: hotload('./fix'), policy: hotload('./policy'), protect: hotload('./protect'), test: hotload('./test'), diff --git a/src/cli/commands/test/generate-snyk-test-error.ts b/src/cli/commands/test/format-test-error.ts similarity index 93% rename from src/cli/commands/test/generate-snyk-test-error.ts rename to src/cli/commands/test/format-test-error.ts index b1b7fb25b9..2bc853e440 100644 --- a/src/cli/commands/test/generate-snyk-test-error.ts +++ b/src/cli/commands/test/format-test-error.ts @@ -1,4 +1,4 @@ -export function generateSnykTestError(error) { +export function formatTestError(error) { // Possible error cases: // - the test found some vulns. `error.message` is a // JSON-stringified diff --git a/src/cli/commands/test/index.ts b/src/cli/commands/test/index.ts index b4f589f5a7..b27848e1e8 100644 --- a/src/cli/commands/test/index.ts +++ b/src/cli/commands/test/index.ts @@ -1,12 +1,13 @@ export = test; +import * as Debug from 'debug'; +import * as pathLib from 'path'; const cloneDeep = require('lodash.clonedeep'); const assign = require('lodash.assign'); import chalk from 'chalk'; + import * as snyk from '../../../lib'; import { isCI } from '../../../lib/is-ci'; -import * as Debug from 'debug'; -import * as pathLib from 'path'; import { IacFileInDirectory, Options, @@ -51,10 +52,15 @@ import { import { test as iacTest } from './iac-test-shim'; import { validateCredentials } from './validate-credentials'; +<<<<<<< HEAD import { generateSnykTestError } from './generate-snyk-test-error'; import { validateTestOptions } from './validate-test-options'; import { setDefaultTestOptions } from './set-default-test-options'; +======= +>>>>>>> feat: introduce `snyk fix` CLI command import { processCommandArgs } from '../process-command-args'; +import { formatTestError } from './format-test-error'; +import { validateTestOptions } from './validate-test-options'; const debug = Debug('snyk-test'); const SEPARATOR = '\n-------------------------------------------------------\n'; @@ -108,7 +114,9 @@ async function test(...args: MethodArgs): Promise { res = await snyk.test(path, testOpts); } } catch (error) { - res = generateSnykTestError(error); + // not throwing here but instead returning error response + // for legacy flow reasons. + res = formatTestError(error); } // Not all test results are arrays in order to be backwards compatible diff --git a/src/cli/modes.ts b/src/cli/modes.ts index e769ff3ba9..8e99579929 100644 --- a/src/cli/modes.ts +++ b/src/cli/modes.ts @@ -8,7 +8,7 @@ interface ModeData { const modes: Record = { source: { - allowedCommands: ['test', 'monitor'], + allowedCommands: ['test', 'monitor', 'fix'], config: (args): [] => { args['source'] = true; return args; diff --git a/src/lib/ecosystems/types.ts b/src/lib/ecosystems/types.ts index 1492e665de..3dea08e4f7 100644 --- a/src/lib/ecosystems/types.ts +++ b/src/lib/ecosystems/types.ts @@ -1,4 +1,5 @@ import { DepGraphData } from '@snyk/dep-graph'; +import { RemediationChanges } from '../snyk-test/legacy'; import { Options } from '../types'; export type Ecosystem = 'cpp' | 'docker' | 'code'; @@ -71,6 +72,7 @@ export interface TestResult { issues: Issue[]; issuesData: IssuesData; depGraphData: DepGraphData; + remediation?: RemediationChanges; } export interface EcosystemPlugin { diff --git a/src/lib/errors/command-not-supported.ts b/src/lib/errors/command-not-supported.ts new file mode 100644 index 0000000000..f4ec9c5451 --- /dev/null +++ b/src/lib/errors/command-not-supported.ts @@ -0,0 +1,17 @@ +import { CustomError } from './custom-error'; + +export class CommandNotSupportedError extends CustomError { + public readonly command: string; + public readonly org?: string; + + constructor(command: string, org?: string) { + super(`${command} is not supported for org ${org}.`); + this.code = 422; + this.command = command; + this.org = org; + + this.userMessage = `\`${command}\`' is not supported ${ + org ? `for org '${org}'` : '' + }`; + } +} diff --git a/src/lib/errors/not-supported-by-ecosystem.ts b/src/lib/errors/not-supported-by-ecosystem.ts new file mode 100644 index 0000000000..e260b12dc4 --- /dev/null +++ b/src/lib/errors/not-supported-by-ecosystem.ts @@ -0,0 +1,18 @@ +import { CustomError } from './custom-error'; +import { SupportedPackageManagers } from '../package-managers'; +import { Ecosystem } from '../ecosystems/types'; + +export class FeatureNotSupportedByEcosystemError extends CustomError { + public readonly feature: string; + + constructor( + feature: string, + ecosystem: SupportedPackageManagers | Ecosystem, + ) { + super(`Unsupported ecosystem ${ecosystem} for ${feature}.`); + this.code = 422; + this.feature = feature; + + this.userMessage = `'${feature}' is not supported for ecosystem '${ecosystem}'`; + } +} diff --git a/src/lib/types.ts b/src/lib/types.ts index 0f9829efba..f48bfa276f 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -195,6 +195,7 @@ export enum SupportedCliCommands { // auth = 'auth', // TODO: auth does not support argv._ at the moment test = 'test', monitor = 'monitor', + fix = 'fix', protect = 'protect', policy = 'policy', ignore = 'ignore', diff --git a/test/__snapshots__/cli-fix.spec.ts.snap b/test/__snapshots__/cli-fix.spec.ts.snap new file mode 100644 index 0000000000..0bc491704c --- /dev/null +++ b/test/__snapshots__/cli-fix.spec.ts.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snyk fix (system tests) \`shows expected response when Python project was skipped because of missing remediation data --all-projects\` 1`] = ` +"βœ” Done +βœ– No successful fixes + +Unresolved items: + + package.json + βœ– npm is not supported. + +Summary: + + 1 items were not fixed + 0 items were successfully fixed + +" +`; + +exports[`snyk fix (system tests) \`shows expected response when Python project was skipped because of missing remediation data --file and custom name\` 1`] = ` +"- Looking for supported Python items +βœ” Looking for supported Python items +β ‹ Processing 1 requirements.txt items.βœ” Processing 1 requirements.txt items. +βœ” Done +βœ– No successful fixes + +Unresolved items: + + /Users/lili/www/snyk/snyk/test/acceptance/workspaces/pip-app-custom/base.txt + βœ– No remediation data available + +Summary: + + 1 items were not fixed + 0 items were successfully fixed + +" +`; + +exports[`snyk fix (system tests) \`shows expected response when Python project was skipped because of missing remediation data --file\` 1`] = ` +"- Looking for supported Python items +βœ” Looking for supported Python items +β ‹ Processing 1 requirements.txt items.βœ” Processing 1 requirements.txt items. +βœ” Done +βœ– No successful fixes + +Unresolved items: + + /Users/lili/www/snyk/snyk/test/acceptance/workspaces/pip-app/requirements.txt + βœ– No remediation data available + +Summary: + + 1 items were not fixed + 0 items were successfully fixed + +" +`; + +exports[`snyk fix (system tests) \`shows expected response when nothing could be fixed + returns exit code 2\` 1`] = ` +"βœ” Done +βœ– No successful fixes + +Unresolved items: + + package.json + βœ– npm is not supported. + +Summary: + + 1 items were not fixed + 0 items were successfully fixed + +" +`; diff --git a/test/acceptance/fixtures/pip-app-with-remediation/test-graph-results.json b/test/acceptance/fixtures/pip-app-with-remediation/test-graph-results.json new file mode 100644 index 0000000000..036ccd5799 --- /dev/null +++ b/test/acceptance/fixtures/pip-app-with-remediation/test-graph-results.json @@ -0,0 +1,327 @@ +{ + "vulnerabilities": [ + { + "CVSSv3": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N", + "alternativeIds": [], + "creationTime": "2021-02-01T13:11:56.558734Z", + "credit": [ + "Wang Baohua" + ], + "cvssScore": 3.1, + "description": "## Overview\n[django](https://pypi.org/project/Django/) is a high-level Python Web framework that encourages rapid development and clean, pragmatic design.\n\nAffected versions of this package are vulnerable to Directory Traversal via the `django.utils.archive.extract()` function, which is used by `startapp --template` and `startproject --template`. This can happen via an archive with absolute paths or relative paths with dot segments.\n\n## Details\n\nA Directory Traversal attack (also known as path traversal) aims to access files and directories that are stored outside the intended folder. By manipulating files with \"dot-dot-slash (../)\" sequences and its variations, or by using absolute file paths, it may be possible to access arbitrary files and directories stored on file system, including application source code, configuration, and other critical system files.\n\nDirectory Traversal vulnerabilities can be generally divided into two types:\n\n- **Information Disclosure**: Allows the attacker to gain information about the folder structure or read the contents of sensitive files on the system.\n\n`st` is a module for serving static files on web pages, and contains a [vulnerability of this type](https://snyk.io/vuln/npm:st:20140206). In our example, we will serve files from the `public` route.\n\nIf an attacker requests the following URL from our server, it will in turn leak the sensitive private key of the root user.\n\n```\ncurl http://localhost:8080/public/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/root/.ssh/id_rsa\n```\n**Note** `%2e` is the URL encoded version of `.` (dot).\n\n- **Writing arbitrary files**: Allows the attacker to create or replace existing files. This type of vulnerability is also known as `Zip-Slip`. \n\nOne way to achieve this is by using a malicious `zip` archive that holds path traversal filenames. When each filename in the zip archive gets concatenated to the target extraction folder, without validation, the final path ends up outside of the target folder. If an executable or a configuration file is overwritten with a file containing malicious code, the problem can turn into an arbitrary code execution issue quite easily.\n\nThe following is an example of a `zip` archive with one benign file and one malicious file. Extracting the malicious file will result in traversing out of the target folder, ending up in `/root/.ssh/` overwriting the `authorized_keys` file:\n\n```\n2018-04-15 22:04:29 ..... 19 19 good.txt\n2018-04-15 22:04:42 ..... 20 20 ../../../../../../root/.ssh/authorized_keys\n```\n\n## Remediation\nUpgrade `django` to version 2.2.18, 3.0.12, 3.1.6 or higher.\n## References\n- [Django Advisory](https://www.djangoproject.com/weblog/2021/feb/01/security-releases/)\n- [GitHub Commit](https://github.com/django/django/commit/05413afa8c18cdb978fcdf470e09f7a12b234a23)\n", + "disclosureTime": "2021-02-01T12:56:31Z", + "exploit": "Not Defined", + "fixedIn": [ + "2.2.18", + "3.0.12", + "3.1.6" + ], + "functions": [], + "functions_new": [], + "id": "SNYK-PYTHON-DJANGO-1066259", + "identifiers": { + "CVE": [ + "CVE-2021-3281" + ], + "CWE": [ + "CWE-22" + ] + }, + "language": "python", + "modificationTime": "2021-02-01T15:11:08.053324Z", + "moduleName": "django", + "packageManager": "pip", + "packageName": "django", + "patches": [], + "proprietary": false, + "publicationTime": "2021-02-01T15:11:08.261009Z", + "references": [ + { + "title": "Django Advisory", + "url": "https://www.djangoproject.com/weblog/2021/feb/01/security-releases/" + }, + { + "title": "GitHub Commit", + "url": "https://github.com/django/django/commit/05413afa8c18cdb978fcdf470e09f7a12b234a23" + } + ], + "semver": { + "vulnerable": [ + "[1.4,2.2.18)", + "[3.0a1,3.0.12)", + "[3.1a1,3.1.6)" + ] + }, + "severity": "low", + "severityWithCritical": "low", + "title": "Directory Traversal", + "from": [ + "pip-app@0.0.0", + "django@1.6.1" + ], + "upgradePath": [], + "isUpgradable": false, + "isPatchable": false, + "name": "django", + "version": "1.6.1" + }, + { + "CVSSv3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N", + "alternativeIds": [], + "creationTime": "2019-01-08T15:45:12.317736Z", + "credit": [ + "Jerbi Nessim" + ], + "cvssScore": 4.3, + "description": "## Overview\n[django](https://pypi.org/project/Django/) is a high-level Python Web framework that encourages rapid development and clean, pragmatic design.\n\nAffected versions of this package are vulnerable to Content Spoofing. The default 404 page did not properly handle user-supplied data, an attacker could supply content to the web application, typically via a parameter value, that is reflected back to the user. This presented the user with a modified page under the context of the trusted domain.\n## Remediation\nUpgrade `django` to version 1.11.18, 2.0.10, 2.1.5 or higher.\n## References\n- [Django Project Security Blog](https://www.djangoproject.com/weblog/2019/jan/04/security-releases/)\n- [GitHub Commit](https://github.com/django/django/commit/1ecc0a395)\n- [RedHat Bugzilla Bug](https://bugzilla.redhat.com/show_bug.cgi?id=1663722)\n", + "disclosureTime": "2019-01-04T22:34:17Z", + "exploit": "Not Defined", + "fixedIn": [ + "1.11.18", + "2.0.10", + "2.1.5" + ], + "functions": [], + "functions_new": [], + "id": "SNYK-PYTHON-DJANGO-72888", + "identifiers": { + "CVE": [ + "CVE-2019-3498" + ], + "CWE": [ + "CWE-148" + ] + }, + "language": "python", + "modificationTime": "2020-06-12T14:36:55.736404Z", + "moduleName": "django", + "packageManager": "pip", + "packageName": "django", + "patches": [], + "proprietary": false, + "publicationTime": "2019-01-08T16:10:39.792267Z", + "references": [ + { + "title": "Django Project Security Blog", + "url": "https://www.djangoproject.com/weblog/2019/jan/04/security-releases/" + }, + { + "title": "GitHub Commit", + "url": "https://github.com/django/django/commit/1ecc0a395" + }, + { + "title": "RedHat Bugzilla Bug", + "url": "https://bugzilla.redhat.com/show_bug.cgi?id=1663722" + } + ], + "semver": { + "vulnerable": [ + "[,1.11.18)", + "[2.0.0, 2.0.10)", + "[2.1.0, 2.1.5)" + ] + }, + "severity": "medium", + "severityWithCritical": "medium", + "title": "Content Spoofing", + "from": [ + "pip-app@0.0.0", + "django@1.6.1" + ], + "upgradePath": [], + "isUpgradable": false, + "isPatchable": false, + "name": "django", + "version": "1.6.1" + } + ], + "ok": false, + "dependencyCount": 2, + "org": "lili", + "policy": "# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.\nversion: v1.19.0\nignore: {}\npatch: {}\n", + "isPrivate": true, + "licensesPolicy": { + "severities": {}, + "orgLicenseRules": { + "AGPL-1.0": { + "licenseType": "AGPL-1.0", + "severity": "high", + "instructions": "" + }, + "AGPL-3.0": { + "licenseType": "AGPL-3.0", + "severity": "high", + "instructions": "" + }, + "Artistic-1.0": { + "licenseType": "Artistic-1.0", + "severity": "medium", + "instructions": "" + }, + "Artistic-2.0": { + "licenseType": "Artistic-2.0", + "severity": "medium", + "instructions": "" + }, + "CDDL-1.0": { + "licenseType": "CDDL-1.0", + "severity": "medium", + "instructions": "" + }, + "CPOL-1.02": { + "licenseType": "CPOL-1.02", + "severity": "high", + "instructions": "" + }, + "EPL-1.0": { + "licenseType": "EPL-1.0", + "severity": "medium", + "instructions": "" + }, + "GPL-2.0": { + "licenseType": "GPL-2.0", + "severity": "high", + "instructions": "" + }, + "GPL-3.0": { + "licenseType": "GPL-3.0", + "severity": "high", + "instructions": "" + }, + "LGPL-2.0": { + "licenseType": "LGPL-2.0", + "severity": "medium", + "instructions": "" + }, + "LGPL-2.1": { + "licenseType": "LGPL-2.1", + "severity": "medium", + "instructions": "" + }, + "LGPL-3.0": { + "licenseType": "LGPL-3.0", + "severity": "medium", + "instructions": "" + }, + "MPL-1.1": { + "licenseType": "MPL-1.1", + "severity": "medium", + "instructions": "" + }, + "MPL-2.0": { + "licenseType": "MPL-2.0", + "severity": "medium", + "instructions": "" + }, + "MS-RL": { + "licenseType": "MS-RL", + "severity": "medium", + "instructions": "" + }, + "SimPL-2.0": { + "licenseType": "SimPL-2.0", + "severity": "high", + "instructions": "" + }, + "MIT": { + "licenseType": "MIT", + "severity": "high", + "instructions": "Not suitable to use, please find a different package." + } + } + }, + "packageManager": "pip", + "ignoreSettings": null, + "summary": "32 vulnerable dependency paths", + "remediation": { + "unresolved": [ + { + "CVSSv3": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N", + "alternativeIds": [], + "creationTime": "2021-02-01T13:11:56.558734Z", + "credit": [ + "Wang Baohua" + ], + "cvssScore": 3.1, + "description": "## Overview\n[django](https://pypi.org/project/Django/) is a high-level Python Web framework that encourages rapid development and clean, pragmatic design.\n\nAffected versions of this package are vulnerable to Directory Traversal via the `django.utils.archive.extract()` function, which is used by `startapp --template` and `startproject --template`. This can happen via an archive with absolute paths or relative paths with dot segments.\n\n## Details\n\nA Directory Traversal attack (also known as path traversal) aims to access files and directories that are stored outside the intended folder. By manipulating files with \"dot-dot-slash (../)\" sequences and its variations, or by using absolute file paths, it may be possible to access arbitrary files and directories stored on file system, including application source code, configuration, and other critical system files.\n\nDirectory Traversal vulnerabilities can be generally divided into two types:\n\n- **Information Disclosure**: Allows the attacker to gain information about the folder structure or read the contents of sensitive files on the system.\n\n`st` is a module for serving static files on web pages, and contains a [vulnerability of this type](https://snyk.io/vuln/npm:st:20140206). In our example, we will serve files from the `public` route.\n\nIf an attacker requests the following URL from our server, it will in turn leak the sensitive private key of the root user.\n\n```\ncurl http://localhost:8080/public/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/root/.ssh/id_rsa\n```\n**Note** `%2e` is the URL encoded version of `.` (dot).\n\n- **Writing arbitrary files**: Allows the attacker to create or replace existing files. This type of vulnerability is also known as `Zip-Slip`. \n\nOne way to achieve this is by using a malicious `zip` archive that holds path traversal filenames. When each filename in the zip archive gets concatenated to the target extraction folder, without validation, the final path ends up outside of the target folder. If an executable or a configuration file is overwritten with a file containing malicious code, the problem can turn into an arbitrary code execution issue quite easily.\n\nThe following is an example of a `zip` archive with one benign file and one malicious file. Extracting the malicious file will result in traversing out of the target folder, ending up in `/root/.ssh/` overwriting the `authorized_keys` file:\n\n```\n2018-04-15 22:04:29 ..... 19 19 good.txt\n2018-04-15 22:04:42 ..... 20 20 ../../../../../../root/.ssh/authorized_keys\n```\n\n## Remediation\nUpgrade `django` to version 2.2.18, 3.0.12, 3.1.6 or higher.\n## References\n- [Django Advisory](https://www.djangoproject.com/weblog/2021/feb/01/security-releases/)\n- [GitHub Commit](https://github.com/django/django/commit/05413afa8c18cdb978fcdf470e09f7a12b234a23)\n", + "disclosureTime": "2021-02-01T12:56:31Z", + "exploit": "Not Defined", + "fixedIn": [ + "2.2.18", + "3.0.12", + "3.1.6" + ], + "functions": [], + "functions_new": [], + "id": "SNYK-PYTHON-DJANGO-1066259", + "identifiers": { + "CVE": [ + "CVE-2021-3281" + ], + "CWE": [ + "CWE-22" + ] + }, + "language": "python", + "modificationTime": "2021-02-01T15:11:08.053324Z", + "moduleName": "django", + "packageManager": "pip", + "packageName": "django", + "patches": [], + "proprietary": false, + "publicationTime": "2021-02-01T15:11:08.261009Z", + "references": [ + { + "title": "Django Advisory", + "url": "https://www.djangoproject.com/weblog/2021/feb/01/security-releases/" + }, + { + "title": "GitHub Commit", + "url": "https://github.com/django/django/commit/05413afa8c18cdb978fcdf470e09f7a12b234a23" + } + ], + "semver": { + "vulnerable": [ + "[1.4,2.2.18)", + "[3.0a1,3.0.12)", + "[3.1a1,3.1.6)" + ] + }, + "severity": "low", + "severityWithCritical": "low", + "title": "Directory Traversal", + "from": [ + "pip-app@0.0.0", + "django@1.6.1" + ], + "upgradePath": [], + "isUpgradable": false, + "isPatchable": false, + "isPinnable": true, + "name": "django", + "version": "1.6.1" + } + ], + "upgrade": {}, + "patch": {}, + "ignore": {}, + "pin": { + "django@1.6.1": { + "upgradeTo": "django@2.2.18", + "vulns": [ + "SNYK-PYTHON-DJANGO-72888" + ], + "isTransitive": false + } + } + }, + "filesystemPolicy": false, + "filtered": { + "ignore": [], + "patch": [] + }, + "uniqueCount": 32, + "projectName": "pip-app", + "foundProjectCount": 2, + "displayTargetFile": "requirements.txt" +} diff --git a/test/cli-fix.spec.ts b/test/cli-fix.spec.ts new file mode 100644 index 0000000000..0d9055e9d3 --- /dev/null +++ b/test/cli-fix.spec.ts @@ -0,0 +1,283 @@ +import { exec } from 'child_process'; +import * as pathLib from 'path'; + +import { fakeServer } from './acceptance/fake-server'; +import cli = require('../src/cli/commands'); + +const main = './dist/cli/index.js'.replace(/\//g, pathLib.sep); +const testTimeout = 50000; +describe('snyk fix (system tests)', () => { + let oldkey; + let oldendpoint; + const apiKey = '123456789'; + const port = process.env.PORT || process.env.SNYK_PORT || '12345'; + + const BASE_API = '/api/v1'; + const SNYK_API = 'http://localhost:' + port + BASE_API; + const SNYK_HOST = 'http://localhost:' + port; + + const server = fakeServer(BASE_API, apiKey); + const noVulnsProjectPath = pathLib.join( + __dirname, + '/acceptance', + 'workspaces', + 'no-vulns', + ); + + const pipRequirementsTxt = pathLib.join( + __dirname, + '/acceptance', + 'workspaces', + 'pip-app', + ); + + const pipCustomRequirementsTxt = pathLib.join( + __dirname, + '/acceptance', + 'workspaces', + 'pip-app-custom', + 'base.txt', + ); + + beforeAll(async () => { + let key = await cli.config('get', 'api'); + oldkey = key; + + key = await cli.config('get', 'endpoint'); + oldendpoint = key; + + await new Promise((resolve) => { + server.listen(port, resolve); + }); + }); + + afterAll(async () => { + delete process.env.SNYK_API; + delete process.env.SNYK_HOST; + delete process.env.SNYK_PORT; + + await server.close(); + let key = 'set'; + let value = 'api=' + oldkey; + if (!oldkey) { + key = 'unset'; + value = 'api'; + } + await cli.config(key, value); + if (oldendpoint) { + await cli.config('endpoint', oldendpoint); + } + }); + it( + '`errors when FF is not enabled`', + (done) => { + exec( + `node ${main} fix --org=no-flag`, + { + env: { + PATH: process.env.PATH, + SNYK_TOKEN: apiKey, + SNYK_API, + SNYK_HOST, + }, + }, + (err, stdout) => { + expect(stdout).toMatch( + "`snyk fix` is not supported for org 'no-flag'", + ); + done(); + }, + ); + }, + testTimeout, + ); + it( + '`shows error when called with --source`', + (done) => { + exec( + `node ${main} fix --source`, + { + env: { + PATH: process.env.PATH, + SNYK_TOKEN: apiKey, + SNYK_API, + SNYK_HOST, + }, + }, + (err, stdout) => { + expect(stdout).toMatch( + "`snyk fix` is not supported for ecosystem 'cpp'", + ); + done(); + }, + ); + }, + testTimeout, + ); + + it( + '`shows error when called with --docker (deprecated)`', + (done) => { + exec( + `node ${main} fix --docker`, + { + env: { + PATH: process.env.PATH, + SNYK_TOKEN: apiKey, + SNYK_API, + SNYK_HOST, + }, + }, + (err, stdout) => { + expect(stdout).toMatch( + "`snyk fix` is not supported for ecosystem 'docker'", + ); + done(); + }, + ); + }, + testTimeout, + ); + + // TODO: this is only showing help when fails? + it.skip( + '`shows error when called with container (deprecated)`', + (done) => { + exec( + `node ${main} container fix`, + { + env: { + PATH: process.env.PATH, + SNYK_TOKEN: apiKey, + SNYK_API, + SNYK_HOST, + }, + }, + (err, stdout) => { + expect(stdout).toMatch( + "`snyk fix` is not supported for ecosystem 'docker'", + ); + done(); + }, + ); + }, + testTimeout, + ); + it( + '`shows error when called with --code`', + (done) => { + exec( + `node ${main} fix --code`, + { + env: { + PATH: process.env.PATH, + SNYK_TOKEN: apiKey, + SNYK_API, + SNYK_HOST, + }, + }, + (err, stdout) => { + expect(stdout).toMatch( + "`snyk fix` is not supported for ecosystem 'code'", + ); + done(); + }, + ); + }, + testTimeout, + ); + + it( + '`shows expected response when nothing could be fixed + returns exit code 2`', + (done) => { + exec( + `node ${main} fix ${noVulnsProjectPath}`, + { + env: { + PATH: process.env.PATH, + SNYK_TOKEN: apiKey, + SNYK_API, + SNYK_HOST, + }, + }, + (err, stdout, stderr) => { + if (!err) { + throw new Error('Test expected to return an error'); + } + expect(stderr).toBe(''); + expect(stdout).toMatchSnapshot(); + expect(err.message).toMatch('Command failed'); + expect(err.code).toBe(2); + done(); + }, + ); + }, + testTimeout, + ); + it( + '`shows expected response when Python project was skipped because of missing remediation data --file`', + (done) => { + exec( + `node ${main} fix --file=${pathLib.join( + pipRequirementsTxt, + 'requirements.txt', + )}`, + { + env: { + PATH: process.env.PATH, + SNYK_TOKEN: apiKey, + SNYK_API, + SNYK_HOST, + }, + }, + (err, stdout) => { + expect(stdout).toMatchSnapshot(); + done(); + }, + ); + }, + testTimeout, + ); + it( + '`shows expected response when Python project was skipped because of missing remediation data --file and custom name`', + (done) => { + exec( + `node ${main} fix --file=${pipCustomRequirementsTxt} --package-manager=pip`, + { + env: { + PATH: process.env.PATH, + SNYK_TOKEN: apiKey, + SNYK_API, + SNYK_HOST, + }, + }, + (err, stdout) => { + expect(stdout).toMatchSnapshot(); + done(); + }, + ); + }, + testTimeout, + ); + it( + '`shows expected response when Python project was skipped because of missing remediation data --all-projects`', + (done) => { + exec( + `node ${main} fix ${noVulnsProjectPath}`, + { + env: { + PATH: process.env.PATH, + SNYK_TOKEN: apiKey, + SNYK_API, + SNYK_HOST, + }, + }, + (err, stdout) => { + expect(stdout).toMatchSnapshot(); + done(); + }, + ); + }, + testTimeout, + ); +}); diff --git a/test/lib/__snapshots__/convert-legacy-test-result-to-new.spec.ts.snap b/test/lib/__snapshots__/convert-legacy-test-result-to-new.spec.ts.snap new file mode 100644 index 0000000000..f1382adf9a --- /dev/null +++ b/test/lib/__snapshots__/convert-legacy-test-result-to-new.spec.ts.snap @@ -0,0 +1,177 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Convert legacy TestResult to ScanResult can convert npm test result with no remediation 1`] = ` +Object { + "depGraphData": Object {}, + "issues": Array [], + "issuesData": Object {}, + "remediation": undefined, +} +`; + +exports[`Convert legacy TestResult to ScanResult can convert npm test result with remediation 1`] = ` +Object { + "depGraphData": Object {}, + "issues": Array [], + "issuesData": Object {}, + "remediation": Object { + "ignore": Object {}, + "patch": Object { + "npm:node-uuid:20160328": Object { + "paths": Array [ + Object { + "ms": Object { + "patched": "2019-11-29T15:08:55.159Z", + }, + }, + ], + }, + }, + "pin": Object {}, + "unresolved": Array [], + "upgrade": Object { + "qs@0.0.6": Object { + "upgradeTo": "qs@6.0.4", + "upgrades": Array [ + "qs@0.0.6", + "qs@0.0.6", + "qs@0.0.6", + ], + "vulns": Array [ + "npm:qs:20170213", + "npm:qs:20140806", + "npm:qs:20140806-1", + ], + }, + }, + }, +} +`; + +exports[`Convert legacy TestResult to ScanResult can convert pip test result with remediation (pins) 1`] = ` +Object { + "depGraphData": Object {}, + "issues": Array [], + "issuesData": Object {}, + "remediation": Object { + "ignore": Object {}, + "patch": Object {}, + "pin": Object { + "django@1.6.1": Object { + "isTransitive": false, + "upgradeTo": "django@2.2.18", + "vulns": Array [ + "SNYK-PYTHON-DJANGO-72888", + ], + }, + }, + "unresolved": Array [ + Object { + "CVSSv3": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N", + "alternativeIds": Array [], + "creationTime": "2021-02-01T13:11:56.558734Z", + "credit": Array [ + "Wang Baohua", + ], + "cvssScore": 3.1, + "description": "## Overview +[django](https://pypi.org/project/Django/) is a high-level Python Web framework that encourages rapid development and clean, pragmatic design. + +Affected versions of this package are vulnerable to Directory Traversal via the \`django.utils.archive.extract()\` function, which is used by \`startapp --template\` and \`startproject --template\`. This can happen via an archive with absolute paths or relative paths with dot segments. + +## Details + +A Directory Traversal attack (also known as path traversal) aims to access files and directories that are stored outside the intended folder. By manipulating files with \\"dot-dot-slash (../)\\" sequences and its variations, or by using absolute file paths, it may be possible to access arbitrary files and directories stored on file system, including application source code, configuration, and other critical system files. + +Directory Traversal vulnerabilities can be generally divided into two types: + +- **Information Disclosure**: Allows the attacker to gain information about the folder structure or read the contents of sensitive files on the system. + +\`st\` is a module for serving static files on web pages, and contains a [vulnerability of this type](https://snyk.io/vuln/npm:st:20140206). In our example, we will serve files from the \`public\` route. + +If an attacker requests the following URL from our server, it will in turn leak the sensitive private key of the root user. + +\`\`\` +curl http://localhost:8080/public/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/root/.ssh/id_rsa +\`\`\` +**Note** \`%2e\` is the URL encoded version of \`.\` (dot). + +- **Writing arbitrary files**: Allows the attacker to create or replace existing files. This type of vulnerability is also known as \`Zip-Slip\`. + +One way to achieve this is by using a malicious \`zip\` archive that holds path traversal filenames. When each filename in the zip archive gets concatenated to the target extraction folder, without validation, the final path ends up outside of the target folder. If an executable or a configuration file is overwritten with a file containing malicious code, the problem can turn into an arbitrary code execution issue quite easily. + +The following is an example of a \`zip\` archive with one benign file and one malicious file. Extracting the malicious file will result in traversing out of the target folder, ending up in \`/root/.ssh/\` overwriting the \`authorized_keys\` file: + +\`\`\` +2018-04-15 22:04:29 ..... 19 19 good.txt +2018-04-15 22:04:42 ..... 20 20 ../../../../../../root/.ssh/authorized_keys +\`\`\` + +## Remediation +Upgrade \`django\` to version 2.2.18, 3.0.12, 3.1.6 or higher. +## References +- [Django Advisory](https://www.djangoproject.com/weblog/2021/feb/01/security-releases/) +- [GitHub Commit](https://github.com/django/django/commit/05413afa8c18cdb978fcdf470e09f7a12b234a23) +", + "disclosureTime": "2021-02-01T12:56:31Z", + "exploit": "Not Defined", + "fixedIn": Array [ + "2.2.18", + "3.0.12", + "3.1.6", + ], + "from": Array [ + "pip-app@0.0.0", + "django@1.6.1", + ], + "functions": Array [], + "functions_new": Array [], + "id": "SNYK-PYTHON-DJANGO-1066259", + "identifiers": Object { + "CVE": Array [ + "CVE-2021-3281", + ], + "CWE": Array [ + "CWE-22", + ], + }, + "isPatchable": false, + "isPinnable": true, + "isUpgradable": false, + "language": "python", + "modificationTime": "2021-02-01T15:11:08.053324Z", + "moduleName": "django", + "name": "django", + "packageManager": "pip", + "packageName": "django", + "patches": Array [], + "proprietary": false, + "publicationTime": "2021-02-01T15:11:08.261009Z", + "references": Array [ + Object { + "title": "Django Advisory", + "url": "https://www.djangoproject.com/weblog/2021/feb/01/security-releases/", + }, + Object { + "title": "GitHub Commit", + "url": "https://github.com/django/django/commit/05413afa8c18cdb978fcdf470e09f7a12b234a23", + }, + ], + "semver": Object { + "vulnerable": Array [ + "[1.4,2.2.18)", + "[3.0a1,3.0.12)", + "[3.1a1,3.1.6)", + ], + }, + "severity": "low", + "severityWithCritical": "low", + "title": "Directory Traversal", + "upgradePath": Array [], + "version": "1.6.1", + }, + ], + "upgrade": Object {}, + }, +} +`; diff --git a/test/lib/__snapshots__/convert-legacy-test-result-to-scan-result.spec.ts.snap b/test/lib/__snapshots__/convert-legacy-test-result-to-scan-result.spec.ts.snap new file mode 100644 index 0000000000..788e258a11 --- /dev/null +++ b/test/lib/__snapshots__/convert-legacy-test-result-to-scan-result.spec.ts.snap @@ -0,0 +1,52 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Convert legacy TestResult to ScanResult can convert npm test result with no remediation 1`] = ` +Object { + "facts": Array [], + "identity": Object { + "targetFile": "package-lock.json", + "type": "npm", + }, + "name": "shallow-goof", + "policy": "# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.19.0 +ignore: {} +patch: {} +", + "target": Object {}, +} +`; + +exports[`Convert legacy TestResult to ScanResult can convert npm test result with remediation 1`] = ` +Object { + "facts": Array [], + "identity": Object { + "targetFile": "package-lock.json", + "type": "npm", + }, + "name": "shallow-goof", + "policy": "# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.19.0 +ignore: {} +patch: {} +", + "target": Object {}, +} +`; + +exports[`Convert legacy TestResult to ScanResult can convert pip test result with remediation (pins) 1`] = ` +Object { + "facts": Array [], + "identity": Object { + "targetFile": "requirements.txt", + "type": "pip", + }, + "name": "pip-app", + "policy": "# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.19.0 +ignore: {} +patch: {} +", + "target": Object {}, +} +`; diff --git a/test/lib/__snapshots__/convert-legacy-tests-results-to-fix-entities.spec.ts.snap b/test/lib/__snapshots__/convert-legacy-tests-results-to-fix-entities.spec.ts.snap new file mode 100644 index 0000000000..0a24824e29 --- /dev/null +++ b/test/lib/__snapshots__/convert-legacy-tests-results-to-fix-entities.spec.ts.snap @@ -0,0 +1,243 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Convert legacy TestResult to ScanResult can convert npm test result with no remediation 1`] = ` +Array [ + Object { + "scanResult": Object { + "facts": Array [], + "identity": Object { + "targetFile": "package-lock.json", + "type": "npm", + }, + "name": "shallow-goof", + "policy": "# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.19.0 +ignore: {} +patch: {} +", + "target": Object {}, + }, + "testResult": Object { + "depGraphData": Object {}, + "issues": Array [], + "issuesData": Object {}, + "remediation": undefined, + }, + "workspace": Object { + "readFile": [Function], + "writeFile": [Function], + }, + }, +] +`; + +exports[`Convert legacy TestResult to ScanResult can convert npm test result with remediation 1`] = ` +Array [ + Object { + "scanResult": Object { + "facts": Array [], + "identity": Object { + "targetFile": "package-lock.json", + "type": "npm", + }, + "name": "shallow-goof", + "policy": "# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.19.0 +ignore: {} +patch: {} +", + "target": Object {}, + }, + "testResult": Object { + "depGraphData": Object {}, + "issues": Array [], + "issuesData": Object {}, + "remediation": Object { + "ignore": Object {}, + "patch": Object { + "npm:node-uuid:20160328": Object { + "paths": Array [ + Object { + "ms": Object { + "patched": "2019-11-29T15:08:55.159Z", + }, + }, + ], + }, + }, + "pin": Object {}, + "unresolved": Array [], + "upgrade": Object { + "qs@0.0.6": Object { + "upgradeTo": "qs@6.0.4", + "upgrades": Array [ + "qs@0.0.6", + "qs@0.0.6", + "qs@0.0.6", + ], + "vulns": Array [ + "npm:qs:20170213", + "npm:qs:20140806", + "npm:qs:20140806-1", + ], + }, + }, + }, + }, + "workspace": Object { + "readFile": [Function], + "writeFile": [Function], + }, + }, +] +`; + +exports[`Convert legacy TestResult to ScanResult can convert pip test result with remediation (pins) 1`] = ` +Array [ + Object { + "scanResult": Object { + "facts": Array [], + "identity": Object { + "targetFile": "requirements.txt", + "type": "pip", + }, + "name": "pip-app", + "policy": "# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.19.0 +ignore: {} +patch: {} +", + "target": Object {}, + }, + "testResult": Object { + "depGraphData": Object {}, + "issues": Array [], + "issuesData": Object {}, + "remediation": Object { + "ignore": Object {}, + "patch": Object {}, + "pin": Object { + "django@1.6.1": Object { + "isTransitive": false, + "upgradeTo": "django@2.2.18", + "vulns": Array [ + "SNYK-PYTHON-DJANGO-72888", + ], + }, + }, + "unresolved": Array [ + Object { + "CVSSv3": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N", + "alternativeIds": Array [], + "creationTime": "2021-02-01T13:11:56.558734Z", + "credit": Array [ + "Wang Baohua", + ], + "cvssScore": 3.1, + "description": "## Overview +[django](https://pypi.org/project/Django/) is a high-level Python Web framework that encourages rapid development and clean, pragmatic design. + +Affected versions of this package are vulnerable to Directory Traversal via the \`django.utils.archive.extract()\` function, which is used by \`startapp --template\` and \`startproject --template\`. This can happen via an archive with absolute paths or relative paths with dot segments. + +## Details + +A Directory Traversal attack (also known as path traversal) aims to access files and directories that are stored outside the intended folder. By manipulating files with \\"dot-dot-slash (../)\\" sequences and its variations, or by using absolute file paths, it may be possible to access arbitrary files and directories stored on file system, including application source code, configuration, and other critical system files. + +Directory Traversal vulnerabilities can be generally divided into two types: + +- **Information Disclosure**: Allows the attacker to gain information about the folder structure or read the contents of sensitive files on the system. + +\`st\` is a module for serving static files on web pages, and contains a [vulnerability of this type](https://snyk.io/vuln/npm:st:20140206). In our example, we will serve files from the \`public\` route. + +If an attacker requests the following URL from our server, it will in turn leak the sensitive private key of the root user. + +\`\`\` +curl http://localhost:8080/public/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/root/.ssh/id_rsa +\`\`\` +**Note** \`%2e\` is the URL encoded version of \`.\` (dot). + +- **Writing arbitrary files**: Allows the attacker to create or replace existing files. This type of vulnerability is also known as \`Zip-Slip\`. + +One way to achieve this is by using a malicious \`zip\` archive that holds path traversal filenames. When each filename in the zip archive gets concatenated to the target extraction folder, without validation, the final path ends up outside of the target folder. If an executable or a configuration file is overwritten with a file containing malicious code, the problem can turn into an arbitrary code execution issue quite easily. + +The following is an example of a \`zip\` archive with one benign file and one malicious file. Extracting the malicious file will result in traversing out of the target folder, ending up in \`/root/.ssh/\` overwriting the \`authorized_keys\` file: + +\`\`\` +2018-04-15 22:04:29 ..... 19 19 good.txt +2018-04-15 22:04:42 ..... 20 20 ../../../../../../root/.ssh/authorized_keys +\`\`\` + +## Remediation +Upgrade \`django\` to version 2.2.18, 3.0.12, 3.1.6 or higher. +## References +- [Django Advisory](https://www.djangoproject.com/weblog/2021/feb/01/security-releases/) +- [GitHub Commit](https://github.com/django/django/commit/05413afa8c18cdb978fcdf470e09f7a12b234a23) +", + "disclosureTime": "2021-02-01T12:56:31Z", + "exploit": "Not Defined", + "fixedIn": Array [ + "2.2.18", + "3.0.12", + "3.1.6", + ], + "from": Array [ + "pip-app@0.0.0", + "django@1.6.1", + ], + "functions": Array [], + "functions_new": Array [], + "id": "SNYK-PYTHON-DJANGO-1066259", + "identifiers": Object { + "CVE": Array [ + "CVE-2021-3281", + ], + "CWE": Array [ + "CWE-22", + ], + }, + "isPatchable": false, + "isPinnable": true, + "isUpgradable": false, + "language": "python", + "modificationTime": "2021-02-01T15:11:08.053324Z", + "moduleName": "django", + "name": "django", + "packageManager": "pip", + "packageName": "django", + "patches": Array [], + "proprietary": false, + "publicationTime": "2021-02-01T15:11:08.261009Z", + "references": Array [ + Object { + "title": "Django Advisory", + "url": "https://www.djangoproject.com/weblog/2021/feb/01/security-releases/", + }, + Object { + "title": "GitHub Commit", + "url": "https://github.com/django/django/commit/05413afa8c18cdb978fcdf470e09f7a12b234a23", + }, + ], + "semver": Object { + "vulnerable": Array [ + "[1.4,2.2.18)", + "[3.0a1,3.0.12)", + "[3.1a1,3.1.6)", + ], + }, + "severity": "low", + "severityWithCritical": "low", + "title": "Directory Traversal", + "upgradePath": Array [], + "version": "1.6.1", + }, + ], + "upgrade": Object {}, + }, + }, + "workspace": Object { + "readFile": [Function], + "writeFile": [Function], + }, + }, +] +`; diff --git a/test/lib/convert-legacy-test-result-to-new.spec.ts b/test/lib/convert-legacy-test-result-to-new.spec.ts new file mode 100644 index 0000000000..9cc06950c0 --- /dev/null +++ b/test/lib/convert-legacy-test-result-to-new.spec.ts @@ -0,0 +1,51 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +import { convertLegacyTestResultToNew } from '../../src/cli/commands/fix/convert-legacy-test-result-to-new'; + +describe('Convert legacy TestResult to ScanResult', () => { + it('can convert npm test result with no remediation', () => { + const noRemediationRes = JSON.parse( + fs.readFileSync( + path.resolve( + __dirname, + '..', + 'acceptance/fixtures/npm-package-with-severity-override/test-graph-result-no-remediation.json', + ), + 'utf8', + ), + ); + const res = convertLegacyTestResultToNew(noRemediationRes); + expect(res).toMatchSnapshot(); + }); + + it('can convert npm test result with remediation', () => { + const withRemediation = JSON.parse( + fs.readFileSync( + path.resolve( + __dirname, + '..', + 'acceptance/fixtures/npm-package-with-severity-override/test-graph-result-patches.json', + ), + 'utf8', + ), + ); + const res = convertLegacyTestResultToNew(withRemediation); + expect(res).toMatchSnapshot(); + }); + + it('can convert pip test result with remediation (pins)', () => { + const withRemediation = JSON.parse( + fs.readFileSync( + path.resolve( + __dirname, + '..', + 'acceptance/fixtures/pip-app-with-remediation/test-graph-results.json', + ), + 'utf8', + ), + ); + const res = convertLegacyTestResultToNew(withRemediation); + expect(res).toMatchSnapshot(); + }); +}); diff --git a/test/lib/convert-legacy-test-result-to-scan-result.spec.ts b/test/lib/convert-legacy-test-result-to-scan-result.spec.ts new file mode 100644 index 0000000000..b3529d08dd --- /dev/null +++ b/test/lib/convert-legacy-test-result-to-scan-result.spec.ts @@ -0,0 +1,50 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +import { convertLegacyTestResultToScanResult } from '../../src/cli/commands/fix/convert-legacy-test-result-to-scan-result'; + +describe('Convert legacy TestResult to ScanResult', () => { + it('can convert npm test result with no remediation', () => { + const noRemediationRes = JSON.parse( + fs.readFileSync( + path.resolve( + __dirname, + '..', + 'acceptance/fixtures/npm-package-with-severity-override/test-graph-result-no-remediation.json', + ), + 'utf8', + ), + ); + const res = convertLegacyTestResultToScanResult(noRemediationRes); + expect(res).toMatchSnapshot(); + }); + + it('can convert npm test result with remediation', () => { + const withRemediation = JSON.parse( + fs.readFileSync( + path.resolve( + __dirname, + '..', + 'acceptance/fixtures/npm-package-with-severity-override/test-graph-result-patches.json', + ), + 'utf8', + ), + ); + const res = convertLegacyTestResultToScanResult(withRemediation); + expect(res).toMatchSnapshot(); + }); + it('can convert pip test result with remediation (pins)', () => { + const withRemediation = JSON.parse( + fs.readFileSync( + path.resolve( + __dirname, + '..', + 'acceptance/fixtures/pip-app-with-remediation/test-graph-results.json', + ), + 'utf8', + ), + ); + const res = convertLegacyTestResultToScanResult(withRemediation); + expect(res).toMatchSnapshot(); + }); +}); diff --git a/test/lib/convert-legacy-tests-results-to-fix-entities.spec.ts b/test/lib/convert-legacy-tests-results-to-fix-entities.spec.ts new file mode 100644 index 0000000000..b02ff41d86 --- /dev/null +++ b/test/lib/convert-legacy-tests-results-to-fix-entities.spec.ts @@ -0,0 +1,60 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +import { convertLegacyTestResultToFixEntities } from '../../src/cli/commands/fix/convert-legacy-tests-results-to-fix-entities'; + +describe('Convert legacy TestResult to ScanResult', () => { + it('can convert npm test result with no remediation', () => { + const noRemediationRes = JSON.parse( + fs.readFileSync( + path.resolve( + __dirname, + '..', + 'acceptance/fixtures/npm-package-with-severity-override/test-graph-result-no-remediation.json', + ), + 'utf8', + ), + ); + const res = convertLegacyTestResultToFixEntities( + noRemediationRes, + __dirname, + ); + expect(res).toMatchSnapshot(); + }); + + it('can convert npm test result with remediation', () => { + const withRemediation = JSON.parse( + fs.readFileSync( + path.resolve( + __dirname, + '..', + 'acceptance/fixtures/npm-package-with-severity-override/test-graph-result-patches.json', + ), + 'utf8', + ), + ); + const res = convertLegacyTestResultToFixEntities( + withRemediation, + path.resolve( + __dirname, + '..', + 'acceptance/fixtures/npm-package-with-severity-override', + ), + ); + expect(res).toMatchSnapshot(); + }); + it('can convert pip test result with remediation (pins)', () => { + const withRemediation = JSON.parse( + fs.readFileSync( + path.resolve( + __dirname, + '..', + 'acceptance/fixtures/pip-app-with-remediation/test-graph-results.json', + ), + 'utf8', + ), + ); + const res = convertLegacyTestResultToFixEntities(withRemediation, '.'); + expect(res).toMatchSnapshot(); + }); +}); diff --git a/test/validate-fix-command-is-supported.spec.ts b/test/validate-fix-command-is-supported.spec.ts new file mode 100644 index 0000000000..fe2760a098 --- /dev/null +++ b/test/validate-fix-command-is-supported.spec.ts @@ -0,0 +1,60 @@ +import { validateFixCommandIsSupported } from '../src/cli/commands/fix/validate-fix-command-is-supported'; +import * as featureFlags from '../src/lib/feature-flags'; +import { ShowVulnPaths } from '../src/lib/types'; +describe('setDefaultTestOptions', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + it('fix is supported for OS projects + enabled FF', () => { + jest + .spyOn(featureFlags, 'isFeatureFlagSupportedForOrg') + .mockResolvedValue({ ok: true }); + const options = { path: '/', showVulnPaths: 'all' as ShowVulnPaths }; + const supported = validateFixCommandIsSupported(options); + expect(supported).toBeTruthy(); + }); + + it('fix is NOT supported for OS projects + disabled FF', () => { + jest + .spyOn(featureFlags, 'isFeatureFlagSupportedForOrg') + .mockResolvedValue({ ok: false }); + const options = { path: '/', showVulnPaths: 'all' as ShowVulnPaths }; + expect(validateFixCommandIsSupported(options)).rejects.toThrowError(''); + }); + + it('fix is NOT supported for --source + enabled FF', () => { + jest + .spyOn(featureFlags, 'isFeatureFlagSupportedForOrg') + .mockResolvedValue({ ok: true }); + const options = { + path: '/', + showVulnPaths: 'all' as ShowVulnPaths, + source: true, + }; + expect(validateFixCommandIsSupported(options)).rejects.toThrowError(''); + }); + + it('fix is NOT supported for --docker + enabled FF', () => { + jest + .spyOn(featureFlags, 'isFeatureFlagSupportedForOrg') + .mockResolvedValue({ ok: true }); + const options = { + path: '/', + showVulnPaths: 'all' as ShowVulnPaths, + docker: true, + }; + expect(validateFixCommandIsSupported(options)).rejects.toThrowError(''); + }); + + it('fix is NOT supported for --code + enabled FF', () => { + jest + .spyOn(featureFlags, 'isFeatureFlagSupportedForOrg') + .mockResolvedValue({ ok: true }); + const options = { + path: '/', + showVulnPaths: 'all' as ShowVulnPaths, + code: true, + }; + expect(validateFixCommandIsSupported(options)).rejects.toThrowError(''); + }); +}); From cbeeeafd187bbfdeaed917a586b6e5c9303d0035 Mon Sep 17 00:00:00 2001 From: ghe Date: Fri, 12 Mar 2021 15:39:05 +0000 Subject: [PATCH 2/9] refactor: call legacy snyk test in a function call --- src/cli/commands/fix/index.ts | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/cli/commands/fix/index.ts b/src/cli/commands/fix/index.ts index 11042d9008..4068927701 100644 --- a/src/cli/commands/fix/index.ts +++ b/src/cli/commands/fix/index.ts @@ -26,8 +26,26 @@ async function fix(...args: MethodArgs): Promise { validateCredentials(options); const results: snykFix.EntityToFix[] = []; + results.push(...(await runSnykTestLegacy(options, paths))); - // TODO: once project envelope is default all code below will be deleted + // fix + debug( + `Organization has ${snykFixFeatureFlag} feature flag enabled for experimental Snyk fix functionality`, + ); + await snykFix.fix(results); + // TODO: what is being returned if anything? + return ''; +} + +/* @deprecated + * TODO: once project envelope is default all code below will be deleted + * we should be calling test via new Ecosystems instead + */ +async function runSnykTestLegacy( + options, + paths, +): Promise { + const results: snykFix.EntityToFix[] = []; for (const path of paths) { // Create a copy of the options so a specific test can // modify them i.e. add `options.file` etc. We'll need @@ -44,20 +62,11 @@ async function fix(...args: MethodArgs): Promise { testResults = await snyk.test(path, snykTestOptions); } catch (error) { const testError = formatTestError(error); - // TODO: what should be done here? - console.error(testError); - throw new Error(error); + throw testError; } const resArray = Array.isArray(testResults) ? testResults : [testResults]; const newRes = convertLegacyTestResultToFixEntities(resArray, path); results.push(...newRes); } - - // fix - debug( - `Organization has ${snykFixFeatureFlag} feature flag enabled for experimental Snyk fix functionality`, - ); - await snykFix.fix(results); - // TODO: what is being returned if anything? - return ''; + return results; } From 34df01d123f8b8f7846918c09fdf9e525079912f Mon Sep 17 00:00:00 2001 From: ghe Date: Fri, 12 Mar 2021 15:55:53 +0000 Subject: [PATCH 3/9] feat: output the fixSummary provided by @snyk/fix --- .../test/unit/__snapshots__/fix.spec.ts.snap | 42 ++++++++------ src/cli/commands/fix/index.ts | 9 ++- src/cli/commands/test/index.ts | 5 -- src/lib/errors/command-not-supported.ts | 2 +- src/lib/errors/not-supported-by-ecosystem.ts | 2 +- test/__snapshots__/cli-fix.spec.ts.snap | 8 +-- .../workspaces/pip-app-custom/base.txt | 1 + test/cli-fix.spec.ts | 55 ++++++++++++++----- 8 files changed, 79 insertions(+), 45 deletions(-) create mode 100644 test/acceptance/workspaces/pip-app-custom/base.txt diff --git a/packages/snyk-fix/test/unit/__snapshots__/fix.spec.ts.snap b/packages/snyk-fix/test/unit/__snapshots__/fix.spec.ts.snap index c9aed4a536..3523d535b0 100644 --- a/packages/snyk-fix/test/unit/__snapshots__/fix.spec.ts.snap +++ b/packages/snyk-fix/test/unit/__snapshots__/fix.spec.ts.snap @@ -2,7 +2,7 @@ exports[`Error handling Snyk fix returns error when called with unsupported type 1`] = ` Object { - "exceptionsByScanType": Object { + "exceptions": Object { "npm": Object { "originals": Array [ Object { @@ -57,18 +57,22 @@ Object { "userMessage": "npm is not supported.", }, }, - "fixSummary": "βœ– No successful fixes + "fixSummary": "βœ– No successful fixes -Unresolved items: +Unresolved items: package.json - βœ– npm is not supported. + βœ– npm is not supported. -Summary: +Summary: - 1 items were not fixed - 0 items were successfully fixed", - "resultsByPlugin": Object {}, + 1 items were not fixed + 0 items were successfully fixed", + "meta": Object { + "failed": 1, + "fixed": 0, + }, + "results": Object {}, } `; @@ -157,22 +161,26 @@ Summary: exports[`Snyk fix Snyk fix returns results for supported & unsupported type 1`] = ` Object { - "exceptionsByScanType": Object {}, - "fixSummary": "Successful fixes: + "exceptions": Object {}, + "fixSummary": "Successful fixes: requirements.txt - βœ” Pinned django from 1.6.1 to 2.0.1 + βœ” Pinned django from 1.6.1 to 2.0.1 -Unresolved items: +Unresolved items: Pipfile - βœ– Pipfile is not supported + βœ– Pipfile is not supported -Summary: +Summary: - 1 items were not fixed - 1 items were successfully fixed", - "resultsByPlugin": Object { + 1 items were not fixed + 1 items were successfully fixed", + "meta": Object { + "failed": 1, + "fixed": 1, + }, + "results": Object { "python": Object { "failed": Array [], "skipped": Array [ diff --git a/src/cli/commands/fix/index.ts b/src/cli/commands/fix/index.ts index 4068927701..6e58bcce2d 100644 --- a/src/cli/commands/fix/index.ts +++ b/src/cli/commands/fix/index.ts @@ -32,9 +32,12 @@ async function fix(...args: MethodArgs): Promise { debug( `Organization has ${snykFixFeatureFlag} feature flag enabled for experimental Snyk fix functionality`, ); - await snykFix.fix(results); - // TODO: what is being returned if anything? - return ''; + const { fixSummary, meta } = await snykFix.fix(results); + + if (meta.fixed === 0) { + throw new Error(fixSummary); + } + return fixSummary; } /* @deprecated diff --git a/src/cli/commands/test/index.ts b/src/cli/commands/test/index.ts index b27848e1e8..ab6a540cdc 100644 --- a/src/cli/commands/test/index.ts +++ b/src/cli/commands/test/index.ts @@ -52,15 +52,10 @@ import { import { test as iacTest } from './iac-test-shim'; import { validateCredentials } from './validate-credentials'; -<<<<<<< HEAD -import { generateSnykTestError } from './generate-snyk-test-error'; import { validateTestOptions } from './validate-test-options'; import { setDefaultTestOptions } from './set-default-test-options'; -======= ->>>>>>> feat: introduce `snyk fix` CLI command import { processCommandArgs } from '../process-command-args'; import { formatTestError } from './format-test-error'; -import { validateTestOptions } from './validate-test-options'; const debug = Debug('snyk-test'); const SEPARATOR = '\n-------------------------------------------------------\n'; diff --git a/src/lib/errors/command-not-supported.ts b/src/lib/errors/command-not-supported.ts index f4ec9c5451..09a75a1647 100644 --- a/src/lib/errors/command-not-supported.ts +++ b/src/lib/errors/command-not-supported.ts @@ -10,7 +10,7 @@ export class CommandNotSupportedError extends CustomError { this.command = command; this.org = org; - this.userMessage = `\`${command}\`' is not supported ${ + this.userMessage = `\`${command}\` is not supported ${ org ? `for org '${org}'` : '' }`; } diff --git a/src/lib/errors/not-supported-by-ecosystem.ts b/src/lib/errors/not-supported-by-ecosystem.ts index e260b12dc4..5ac9e2afa5 100644 --- a/src/lib/errors/not-supported-by-ecosystem.ts +++ b/src/lib/errors/not-supported-by-ecosystem.ts @@ -13,6 +13,6 @@ export class FeatureNotSupportedByEcosystemError extends CustomError { this.code = 422; this.feature = feature; - this.userMessage = `'${feature}' is not supported for ecosystem '${ecosystem}'`; + this.userMessage = `\`${feature}\` is not supported for ecosystem '${ecosystem}'`; } } diff --git a/test/__snapshots__/cli-fix.spec.ts.snap b/test/__snapshots__/cli-fix.spec.ts.snap index 0bc491704c..aaac1bf228 100644 --- a/test/__snapshots__/cli-fix.spec.ts.snap +++ b/test/__snapshots__/cli-fix.spec.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`snyk fix (system tests) \`shows expected response when Python project was skipped because of missing remediation data --all-projects\` 1`] = ` -"βœ” Done +"βœ– Done βœ– No successful fixes Unresolved items: @@ -21,7 +21,7 @@ exports[`snyk fix (system tests) \`shows expected response when Python project w "- Looking for supported Python items βœ” Looking for supported Python items β ‹ Processing 1 requirements.txt items.βœ” Processing 1 requirements.txt items. -βœ” Done +βœ– Done βœ– No successful fixes Unresolved items: @@ -41,7 +41,7 @@ exports[`snyk fix (system tests) \`shows expected response when Python project w "- Looking for supported Python items βœ” Looking for supported Python items β ‹ Processing 1 requirements.txt items.βœ” Processing 1 requirements.txt items. -βœ” Done +βœ– Done βœ– No successful fixes Unresolved items: @@ -58,7 +58,7 @@ Summary: `; exports[`snyk fix (system tests) \`shows expected response when nothing could be fixed + returns exit code 2\` 1`] = ` -"βœ” Done +"βœ– Done βœ– No successful fixes Unresolved items: diff --git a/test/acceptance/workspaces/pip-app-custom/base.txt b/test/acceptance/workspaces/pip-app-custom/base.txt new file mode 100644 index 0000000000..c3eeef4fb7 --- /dev/null +++ b/test/acceptance/workspaces/pip-app-custom/base.txt @@ -0,0 +1 @@ +Jinja2==2.7.2 diff --git a/test/cli-fix.spec.ts b/test/cli-fix.spec.ts index 0d9055e9d3..5ad348e498 100644 --- a/test/cli-fix.spec.ts +++ b/test/cli-fix.spec.ts @@ -1,5 +1,6 @@ import { exec } from 'child_process'; import * as pathLib from 'path'; +import stripAnsi from 'strip-ansi'; import { fakeServer } from './acceptance/fake-server'; import cli = require('../src/cli/commands'); @@ -81,7 +82,13 @@ describe('snyk fix (system tests)', () => { SNYK_HOST, }, }, - (err, stdout) => { + (err, stdout, stderr) => { + if (!err) { + throw new Error('Test expected to return an error'); + } + expect(stderr).toBe(''); + expect(err.message).toMatch('Command failed'); + expect(err.code).toEqual(2); expect(stdout).toMatch( "`snyk fix` is not supported for org 'no-flag'", ); @@ -104,7 +111,13 @@ describe('snyk fix (system tests)', () => { SNYK_HOST, }, }, - (err, stdout) => { + (err, stdout, stderr) => { + if (!err) { + throw new Error('Test expected to return an error'); + } + expect(stderr).toBe(''); + expect(err.message).toMatch('Command failed'); + expect(err.code).toEqual(2); expect(stdout).toMatch( "`snyk fix` is not supported for ecosystem 'cpp'", ); @@ -128,7 +141,13 @@ describe('snyk fix (system tests)', () => { SNYK_HOST, }, }, - (err, stdout) => { + (err, stdout, stderr) => { + if (!err) { + throw new Error('Test expected to return an error'); + } + expect(stderr).toBe(''); + expect(err.message).toMatch('Command failed'); + expect(err.code).toEqual(2); expect(stdout).toMatch( "`snyk fix` is not supported for ecosystem 'docker'", ); @@ -139,8 +158,10 @@ describe('snyk fix (system tests)', () => { testTimeout, ); - // TODO: this is only showing help when fails? - it.skip( + /* this command is different + * it shows help text not an error when command is not supported + */ + it( '`shows error when called with container (deprecated)`', (done) => { exec( @@ -153,10 +174,10 @@ describe('snyk fix (system tests)', () => { SNYK_HOST, }, }, - (err, stdout) => { - expect(stdout).toMatch( - "`snyk fix` is not supported for ecosystem 'docker'", - ); + (err, stdout, stderr) => { + expect(stderr).toBe(''); + expect(stdout).toMatch('COMMANDS'); + expect(err).toBe(null); done(); }, ); @@ -176,7 +197,13 @@ describe('snyk fix (system tests)', () => { SNYK_HOST, }, }, - (err, stdout) => { + (err, stdout, stderr) => { + if (!err) { + throw new Error('Test expected to return an error'); + } + expect(stderr).toBe(''); + expect(err.message).toMatch('Command failed'); + expect(err.code).toEqual(2); expect(stdout).toMatch( "`snyk fix` is not supported for ecosystem 'code'", ); @@ -205,7 +232,7 @@ describe('snyk fix (system tests)', () => { throw new Error('Test expected to return an error'); } expect(stderr).toBe(''); - expect(stdout).toMatchSnapshot(); + expect(stripAnsi(stdout)).toMatchSnapshot(); expect(err.message).toMatch('Command failed'); expect(err.code).toBe(2); done(); @@ -231,7 +258,7 @@ describe('snyk fix (system tests)', () => { }, }, (err, stdout) => { - expect(stdout).toMatchSnapshot(); + expect(stripAnsi(stdout)).toMatchSnapshot(); done(); }, ); @@ -252,7 +279,7 @@ describe('snyk fix (system tests)', () => { }, }, (err, stdout) => { - expect(stdout).toMatchSnapshot(); + expect(stripAnsi(stdout)).toMatchSnapshot(); done(); }, ); @@ -273,7 +300,7 @@ describe('snyk fix (system tests)', () => { }, }, (err, stdout) => { - expect(stdout).toMatchSnapshot(); + expect(stripAnsi(stdout)).toMatchSnapshot(); done(); }, ); From 4ef96f50f594bd1e9ebe080d07151483c93bdb8a Mon Sep 17 00:00:00 2001 From: ghe Date: Mon, 15 Mar 2021 18:57:33 +0000 Subject: [PATCH 4/9] feat: skip failed snyk test paths when fixing --- package.json | 1 + src/cli/commands/fix/index.ts | 79 ++- .../commands/test/set-default-test-options.ts | 4 +- test/__snapshots__/cli-fix.spec.ts.snap | 75 --- .../__snapshots__/cli-fix.system.spec.ts.snap | 37 ++ test/cli-fix.functional.spec.ts | 184 ++++++ ...cli-fix.spec.ts => cli-fix.system.spec.ts} | 64 +-- .../test-result-pip-with-remediation.json | 535 ++++++++++++++++++ 8 files changed, 819 insertions(+), 160 deletions(-) delete mode 100644 test/__snapshots__/cli-fix.spec.ts.snap create mode 100644 test/__snapshots__/cli-fix.system.spec.ts.snap create mode 100644 test/cli-fix.functional.spec.ts rename test/{cli-fix.spec.ts => cli-fix.system.spec.ts} (80%) create mode 100644 test/fixtures/snyk-fix/test-result-pip-with-remediation.json diff --git a/package.json b/package.json index 9fb02e1ea0..6fc22bbc2d 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "micromatch": "4.0.2", "needle": "2.6.0", "open": "^7.0.3", + "ora": "5.3.0", "os-name": "^3.0.0", "promise-queue": "^2.2.5", "proxy-agent": "^3.1.1", diff --git a/src/cli/commands/fix/index.ts b/src/cli/commands/fix/index.ts index 6e58bcce2d..06d6de5c80 100644 --- a/src/cli/commands/fix/index.ts +++ b/src/cli/commands/fix/index.ts @@ -2,6 +2,8 @@ export = fix; import * as Debug from 'debug'; import * as snykFix from '@snyk/fix'; +import * as pathLib from 'path'; +import * as ora from 'ora'; import { MethodArgs } from '../../args'; import * as snyk from '../../../lib'; @@ -14,17 +16,24 @@ import { validateCredentials } from '../test/validate-credentials'; import { validateTestOptions } from '../test/validate-test-options'; import { setDefaultTestOptions } from '../test/set-default-test-options'; import { validateFixCommandIsSupported } from './validate-fix-command-is-supported'; +import { Options, TestOptions } from '../../../lib/types'; const debug = Debug('snyk-fix'); const snykFixFeatureFlag = 'cliSnykFix'; +interface FixOptions { + dryRun?: boolean; + quiet?: boolean; +} async function fix(...args: MethodArgs): Promise { - const { options: rawOptions, paths } = await processCommandArgs(...args); - const options = setDefaultTestOptions(rawOptions); + const { options: rawOptions, paths } = await processCommandArgs( + ...args, + ); + const options = setDefaultTestOptions(rawOptions); + debug(options); await validateFixCommandIsSupported(options); validateTestOptions(options); validateCredentials(options); - const results: snykFix.EntityToFix[] = []; results.push(...(await runSnykTestLegacy(options, paths))); @@ -32,8 +41,8 @@ async function fix(...args: MethodArgs): Promise { debug( `Organization has ${snykFixFeatureFlag} feature flag enabled for experimental Snyk fix functionality`, ); - const { fixSummary, meta } = await snykFix.fix(results); - + const { dryRun, quiet } = options; + const { fixSummary, meta } = await snykFix.fix(results, { dryRun, quiet }); if (meta.fixed === 0) { throw new Error(fixSummary); } @@ -45,31 +54,57 @@ async function fix(...args: MethodArgs): Promise { * we should be calling test via new Ecosystems instead */ async function runSnykTestLegacy( - options, - paths, + options: Options & TestOptions & FixOptions, + paths: string[], ): Promise { const results: snykFix.EntityToFix[] = []; + const stdOutSpinner = ora({ + isSilent: options.quiet, + stream: process.stdout, + }); + const stdErrSpinner = ora({ + isSilent: options.quiet, + stream: process.stdout, + }); + stdErrSpinner.start(); + stdOutSpinner.start(); + for (const path of paths) { - // Create a copy of the options so a specific test can - // modify them i.e. add `options.file` etc. We'll need - // these options later. - const snykTestOptions = { - ...options, - path, - projectName: options['project-name'], - }; + let relativePath = path; + try { + const { dir } = pathLib.parse(path); + relativePath = pathLib.relative(process.cwd(), dir); + stdOutSpinner.info(`Running \`snyk test\` for ${relativePath}`); + // Create a copy of the options so a specific test can + // modify them i.e. add `options.file` etc. We'll need + // these options later. + const snykTestOptions = { + ...options, + path, + projectName: options['project-name'], + }; - let testResults: TestResult | TestResult[]; + const testResults: TestResult[] = []; - try { - testResults = await snyk.test(path, snykTestOptions); + const testResultForPath: TestResult | TestResult[] = await snyk.test( + path, + { ...snykTestOptions, quiet: true }, + ); + testResults.push( + ...(Array.isArray(testResultForPath) + ? testResultForPath + : [testResultForPath]), + ); + const newRes = convertLegacyTestResultToFixEntities(testResults, path); + results.push(...newRes); } catch (error) { const testError = formatTestError(error); - throw testError; + const userMessage = `Test for ${relativePath} failed with error: ${testError.message}.\nRun \`snyk test ${relativePath} -d\` for more information.`; + stdErrSpinner.fail(userMessage); + debug(userMessage); } - const resArray = Array.isArray(testResults) ? testResults : [testResults]; - const newRes = convertLegacyTestResultToFixEntities(resArray, path); - results.push(...newRes); } + stdOutSpinner.stop(); + stdErrSpinner.stop(); return results; } diff --git a/src/cli/commands/test/set-default-test-options.ts b/src/cli/commands/test/set-default-test-options.ts index a6106fe7fd..e8396980dd 100644 --- a/src/cli/commands/test/set-default-test-options.ts +++ b/src/cli/commands/test/set-default-test-options.ts @@ -1,7 +1,9 @@ import * as config from '../../../lib/config'; import { Options, ShowVulnPaths, TestOptions } from '../../../lib/types'; -export function setDefaultTestOptions(options: Options): Options & TestOptions { +export function setDefaultTestOptions( + options: Options & CommandOptions, +): Options & TestOptions & CommandOptions { const svpSupplied = (options['show-vulnerable-paths'] || '') .toString() .toLowerCase(); diff --git a/test/__snapshots__/cli-fix.spec.ts.snap b/test/__snapshots__/cli-fix.spec.ts.snap deleted file mode 100644 index aaac1bf228..0000000000 --- a/test/__snapshots__/cli-fix.spec.ts.snap +++ /dev/null @@ -1,75 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`snyk fix (system tests) \`shows expected response when Python project was skipped because of missing remediation data --all-projects\` 1`] = ` -"βœ– Done -βœ– No successful fixes - -Unresolved items: - - package.json - βœ– npm is not supported. - -Summary: - - 1 items were not fixed - 0 items were successfully fixed - -" -`; - -exports[`snyk fix (system tests) \`shows expected response when Python project was skipped because of missing remediation data --file and custom name\` 1`] = ` -"- Looking for supported Python items -βœ” Looking for supported Python items -β ‹ Processing 1 requirements.txt items.βœ” Processing 1 requirements.txt items. -βœ– Done -βœ– No successful fixes - -Unresolved items: - - /Users/lili/www/snyk/snyk/test/acceptance/workspaces/pip-app-custom/base.txt - βœ– No remediation data available - -Summary: - - 1 items were not fixed - 0 items were successfully fixed - -" -`; - -exports[`snyk fix (system tests) \`shows expected response when Python project was skipped because of missing remediation data --file\` 1`] = ` -"- Looking for supported Python items -βœ” Looking for supported Python items -β ‹ Processing 1 requirements.txt items.βœ” Processing 1 requirements.txt items. -βœ– Done -βœ– No successful fixes - -Unresolved items: - - /Users/lili/www/snyk/snyk/test/acceptance/workspaces/pip-app/requirements.txt - βœ– No remediation data available - -Summary: - - 1 items were not fixed - 0 items were successfully fixed - -" -`; - -exports[`snyk fix (system tests) \`shows expected response when nothing could be fixed + returns exit code 2\` 1`] = ` -"βœ– Done -βœ– No successful fixes - -Unresolved items: - - package.json - βœ– npm is not supported. - -Summary: - - 1 items were not fixed - 0 items were successfully fixed - -" -`; diff --git a/test/__snapshots__/cli-fix.system.spec.ts.snap b/test/__snapshots__/cli-fix.system.spec.ts.snap new file mode 100644 index 0000000000..48bd27c0ee --- /dev/null +++ b/test/__snapshots__/cli-fix.system.spec.ts.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`snyk fix (system tests) \`shows expected response when Python project was skipped because of missing remediation data --all-projects\` 1`] = ` +"β„Ή Running \`snyk test\` for test/acceptance/workspaces +βœ– Done +βœ– No successful fixes + +Unresolved items: + + package.json + βœ– npm is not supported. + +Summary: + + 1 items were not fixed + 0 items were successfully fixed + +" +`; + +exports[`snyk fix (system tests) \`shows expected response when nothing could be fixed + returns exit code 2\` 1`] = ` +"β„Ή Running \`snyk test\` for test/acceptance/workspaces +βœ– Done +βœ– No successful fixes + +Unresolved items: + + package.json + βœ– npm is not supported. + +Summary: + + 1 items were not fixed + 0 items were successfully fixed + +" +`; diff --git a/test/cli-fix.functional.spec.ts b/test/cli-fix.functional.spec.ts new file mode 100644 index 0000000000..3b3f4bf9ab --- /dev/null +++ b/test/cli-fix.functional.spec.ts @@ -0,0 +1,184 @@ +import * as pathLib from 'path'; +import * as fs from 'fs'; + +import cli = require('../src/cli/commands'); +import * as snyk from '../src/lib'; + +import * as validateFixSupported from '../src/cli/commands/fix/validate-fix-command-is-supported'; + +import stripAnsi from 'strip-ansi'; + +const testTimeout = 100000; + +const pipAppWorkspace = pathLib.join( + __dirname, + '/acceptance', + 'workspaces', + 'pip-app', +); + +const npmWorkspace = pathLib.join( + __dirname, + 'acceptance', + 'workspaces', + 'no-vulns', +); + +const pipRequirementsTxt = pathLib.join(pipAppWorkspace, 'requirements.txt'); + +const pipRequirementsCustomTxt = pathLib.join( + __dirname, + '/acceptance', + 'workspaces', + 'pip-app-custom', + 'base.txt', +); + +const pipWithRemediation = JSON.parse( + fs.readFileSync( + pathLib.resolve( + __dirname, + 'fixtures', + 'snyk-fix', + 'test-result-pip-with-remediation.json', + ), + 'utf8', + ), +); + +describe('snyk fix (functional tests)', () => { + beforeAll(async () => { + jest + .spyOn(validateFixSupported, 'validateFixCommandIsSupported') + .mockResolvedValue(true); + }); + + afterAll(async () => { + jest.clearAllMocks(); + }); + it( + 'shows successful fixes Python requirements.txt project was fixed via --file', + async () => { + // read data from console.log + let stdoutMessages = ''; + let stderrMessages = ''; + jest + .spyOn(console, 'log') + .mockImplementation((msg: string) => (stdoutMessages += msg)); + jest + .spyOn(console, 'error') + .mockImplementation((msg: string) => (stderrMessages += msg)); + + jest.spyOn(snyk, 'test').mockResolvedValue({ + ...pipWithRemediation, + // pip plugin does not return targetFile, instead fix will fallback to displayTargetFile + displayTargetFile: pipRequirementsTxt, + }); + const res = await cli.fix('.', { + file: pipRequirementsTxt, + dryRun: true, // prevents write to disc + quiet: true, + }); + expect(stripAnsi(res)).toMatch('βœ” Upgraded Jinja2 from 2.7.2 to 2.11.3'); + expect(stdoutMessages).toEqual(''); + expect(stderrMessages).toEqual(''); + }, + testTimeout, + ); + it( + 'shows successful fixes Python custom name base.txt project was fixed via --file', + async () => { + // read data from console.log + let stdoutMessages = ''; + let stderrMessages = ''; + jest + .spyOn(console, 'log') + .mockImplementation((msg: string) => (stdoutMessages += msg)); + jest + .spyOn(console, 'error') + .mockImplementation((msg: string) => (stderrMessages += msg)); + + jest.spyOn(snyk, 'test').mockResolvedValue({ + ...pipWithRemediation, + // pip plugin does not return targetFile, instead fix will fallback to displayTargetFile + displayTargetFile: pipRequirementsCustomTxt, + }); + const res = await cli.fix('.', { + file: pipRequirementsCustomTxt, + packageManager: 'pip', + dryRun: true, // prevents write to disc + quiet: true, + }); + expect(stripAnsi(res)).toMatch('βœ” Upgraded Jinja2 from 2.7.2 to 2.11.3'); + expect(stdoutMessages).toEqual(''); + expect(stderrMessages).toEqual(''); + }, + testTimeout, + ); + + it( + 'snyk fix continues to fix when 1 path fails to test with `snyk fix path1 path2`', + async () => { + // read data from console.log + let stdoutMessages = ''; + let stderrMessages = ''; + jest + .spyOn(console, 'log') + .mockImplementation((msg: string) => (stdoutMessages += msg)); + jest + .spyOn(console, 'error') + .mockImplementation((msg: string) => (stderrMessages += msg)); + + jest + .spyOn(snyk, 'test') + .mockRejectedValueOnce(new Error('Failed to get npm dependencies')); + jest.spyOn(snyk, 'test').mockResolvedValue({ + ...pipWithRemediation, + // pip plugin does not return targetFile, instead fix will fallback to displayTargetFile + displayTargetFile: pipRequirementsTxt, + }); + const res = await cli.fix(npmWorkspace, pipAppWorkspace, { + dryRun: true, // prevents write to disc + quiet: true, + }); + expect(stripAnsi(res)).toMatch('βœ” Upgraded Jinja2 from 2.7.2 to 2.11.3'); + // only use ora to output + expect(stdoutMessages).toEqual(''); + expect(stderrMessages).toEqual(''); + }, + testTimeout, + ); + + it( + 'snyk fails to fix when all path fails to test with `snyk fix path1 path2`', + async () => { + // read data from console.log + let stdoutMessages = ''; + let stderrMessages = ''; + jest + .spyOn(console, 'log') + .mockImplementation((msg: string) => (stdoutMessages += msg)); + jest + .spyOn(console, 'error') + .mockImplementation((msg: string) => (stderrMessages += msg)); + + jest + .spyOn(snyk, 'test') + .mockRejectedValue(new Error('Failed to get dependencies')); + + let res; + try { + await cli.fix(npmWorkspace, pipAppWorkspace, { + dryRun: true, // prevents write to disc + quiet: true, + }); + } catch (error) { + res = error; + } + expect(stripAnsi(res.message)).toMatch('No successful fixes'); + expect(stdoutMessages).toEqual(''); + expect(stderrMessages).toEqual(''); + }, + testTimeout, + ); +}); diff --git a/test/cli-fix.spec.ts b/test/cli-fix.system.spec.ts similarity index 80% rename from test/cli-fix.spec.ts rename to test/cli-fix.system.spec.ts index 5ad348e498..63c2fc8678 100644 --- a/test/cli-fix.spec.ts +++ b/test/cli-fix.system.spec.ts @@ -25,21 +25,6 @@ describe('snyk fix (system tests)', () => { 'no-vulns', ); - const pipRequirementsTxt = pathLib.join( - __dirname, - '/acceptance', - 'workspaces', - 'pip-app', - ); - - const pipCustomRequirementsTxt = pathLib.join( - __dirname, - '/acceptance', - 'workspaces', - 'pip-app-custom', - 'base.txt', - ); - beforeAll(async () => { let key = await cli.config('get', 'api'); oldkey = key; @@ -161,8 +146,8 @@ describe('snyk fix (system tests)', () => { /* this command is different * it shows help text not an error when command is not supported */ - it( - '`shows error when called with container (deprecated)`', + it.skip( + '`shows error when called with container`', (done) => { exec( `node ${main} container fix`, @@ -241,51 +226,6 @@ describe('snyk fix (system tests)', () => { }, testTimeout, ); - it( - '`shows expected response when Python project was skipped because of missing remediation data --file`', - (done) => { - exec( - `node ${main} fix --file=${pathLib.join( - pipRequirementsTxt, - 'requirements.txt', - )}`, - { - env: { - PATH: process.env.PATH, - SNYK_TOKEN: apiKey, - SNYK_API, - SNYK_HOST, - }, - }, - (err, stdout) => { - expect(stripAnsi(stdout)).toMatchSnapshot(); - done(); - }, - ); - }, - testTimeout, - ); - it( - '`shows expected response when Python project was skipped because of missing remediation data --file and custom name`', - (done) => { - exec( - `node ${main} fix --file=${pipCustomRequirementsTxt} --package-manager=pip`, - { - env: { - PATH: process.env.PATH, - SNYK_TOKEN: apiKey, - SNYK_API, - SNYK_HOST, - }, - }, - (err, stdout) => { - expect(stripAnsi(stdout)).toMatchSnapshot(); - done(); - }, - ); - }, - testTimeout, - ); it( '`shows expected response when Python project was skipped because of missing remediation data --all-projects`', (done) => { diff --git a/test/fixtures/snyk-fix/test-result-pip-with-remediation.json b/test/fixtures/snyk-fix/test-result-pip-with-remediation.json new file mode 100644 index 0000000000..c158424887 --- /dev/null +++ b/test/fixtures/snyk-fix/test-result-pip-with-remediation.json @@ -0,0 +1,535 @@ +{ + "vulnerabilities": [ + { + "CVSSv3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L/E:P", + "alternativeIds": [], + "creationTime": "2020-09-25T17:30:26.286074Z", + "credit": [ + "Yeting Li" + ], + "cvssScore": 5.3, + "description": "## Overview\n[jinja2](https://pypi.org/project/Jinja2/) is a template engine written in pure Python. It provides a Django inspired non-XML syntax but supports inline expressions and an optional sandboxed environment.\n\nAffected versions of this package are vulnerable to Regular Expression Denial of Service (ReDoS). The ReDoS vulnerability is mainly due to the `_punctuation_re regex` operator and its use of multiple wildcards. The last wildcard is the most exploitable as it searches for trailing punctuation.\r\n\r\nThis issue can be mitigated by using Markdown to format user content instead of the urlize filter, or by implementing request timeouts or limiting process memory.\r\n\r\n### PoC by Yeting Li\r\n```\r\nfrom jinja2.utils import urlize\r\nfrom time import perf_counter\r\n\r\nfor i in range(3):\r\n text = \"abc@\" + \".\" * (i+1)*5000 + \"!\"\r\n LEN = len(text)\r\n BEGIN = perf_counter()\r\n urlize(text)\r\n DURATION = perf_counter() - BEGIN\r\n print(f\"{LEN}: took {DURATION} seconds!\")\r\n```\n\n## Details\n\nDenial of Service (DoS) describes a family of attacks, all aimed at making a system inaccessible to its original and legitimate users. There are many types of DoS attacks, ranging from trying to clog the network pipes to the system by generating a large volume of traffic from many machines (a Distributed Denial of Service - DDoS - attack) to sending crafted requests that cause a system to crash or take a disproportional amount of time to process.\n\nThe Regular expression Denial of Service (ReDoS) is a type of Denial of Service attack. Regular expressions are incredibly powerful, but they aren't very intuitive and can ultimately end up making it easy for attackers to take your site down.\n\nLet’s take the following regular expression as an example:\n```js\nregex = /A(B|C+)+D/\n```\n\nThis regular expression accomplishes the following:\n- `A` The string must start with the letter 'A'\n- `(B|C+)+` The string must then follow the letter A with either the letter 'B' or some number of occurrences of the letter 'C' (the `+` matches one or more times). The `+` at the end of this section states that we can look for one or more matches of this section.\n- `D` Finally, we ensure this section of the string ends with a 'D'\n\nThe expression would match inputs such as `ABBD`, `ABCCCCD`, `ABCBCCCD` and `ACCCCCD`\n\nIt most cases, it doesn't take very long for a regex engine to find a match:\n\n```bash\n$ time node -e '/A(B|C+)+D/.test(\"ACCCCCCCCCCCCCCCCCCCCCCCCCCCCD\")'\n0.04s user 0.01s system 95% cpu 0.052 total\n\n$ time node -e '/A(B|C+)+D/.test(\"ACCCCCCCCCCCCCCCCCCCCCCCCCCCCX\")'\n1.79s user 0.02s system 99% cpu 1.812 total\n```\n\nThe entire process of testing it against a 30 characters long string takes around ~52ms. But when given an invalid string, it takes nearly two seconds to complete the test, over ten times as long as it took to test a valid string. The dramatic difference is due to the way regular expressions get evaluated.\n\nMost Regex engines will work very similarly (with minor differences). The engine will match the first possible way to accept the current character and proceed to the next one. If it then fails to match the next one, it will backtrack and see if there was another way to digest the previous character. If it goes too far down the rabbit hole only to find out the string doesn’t match in the end, and if many characters have multiple valid regex paths, the number of backtracking steps can become very large, resulting in what is known as _catastrophic backtracking_.\n\nLet's look at how our expression runs into this problem, using a shorter string: \"ACCCX\". While it seems fairly straightforward, there are still four different ways that the engine could match those three C's:\n1. CCC\n2. CC+C\n3. C+CC\n4. C+C+C.\n\nThe engine has to try each of those combinations to see if any of them potentially match against the expression. When you combine that with the other steps the engine must take, we can use [RegEx 101 debugger](https://regex101.com/debugger) to see the engine has to take a total of 38 steps before it can determine the string doesn't match.\n\nFrom there, the number of steps the engine must use to validate a string just continues to grow.\n\n| String | Number of C's | Number of steps |\n| -------|-------------:| -----:|\n| ACCCX | 3 | 38\n| ACCCCX | 4 | 71\n| ACCCCCX | 5 | 136\n| ACCCCCCCCCCCCCCX | 14 | 65,553\n\n\nBy the time the string includes 14 C's, the engine has to take over 65,000 steps just to see if the string is valid. These extreme situations can cause them to work very slowly (exponentially related to input size, as shown above), allowing an attacker to exploit this and can cause the service to excessively consume CPU, resulting in a Denial of Service.\n\n## Remediation\nUpgrade `jinja2` to version 2.11.3 or higher.\n## References\n- [GitHub Additional Information](https://github.com/pallets/jinja/blob/ab81fd9c277900c85da0c322a2ff9d68a235b2e6/src/jinja2/utils.py#L20)\n- [GitHub PR](https://github.com/pallets/jinja/pull/1343)\n", + "disclosureTime": "2020-09-25T17:29:19Z", + "exploit": "Proof of Concept", + "fixedIn": [ + "2.11.3" + ], + "functions": [], + "functions_new": [], + "id": "SNYK-PYTHON-JINJA2-1012994", + "identifiers": { + "CVE": [ + "CVE-2020-28493" + ], + "CWE": [ + "CWE-400" + ] + }, + "language": "python", + "modificationTime": "2021-02-01T19:52:16.877030Z", + "moduleName": "jinja2", + "packageManager": "pip", + "packageName": "jinja2", + "patches": [], + "proprietary": true, + "publicationTime": "2021-02-01T19:52:17Z", + "references": [ + { + "title": "GitHub Additional Information", + "url": "https://github.com/pallets/jinja/blob/ab81fd9c277900c85da0c322a2ff9d68a235b2e6/src/jinja2/utils.py%23L20" + }, + { + "title": "GitHub PR", + "url": "https://github.com/pallets/jinja/pull/1343" + } + ], + "semver": { + "vulnerable": [ + "[,2.11.3)" + ] + }, + "severity": "medium", + "severityWithCritical": "medium", + "title": "Regular Expression Denial of Service (ReDoS)", + "from": [ + "pip-app@0.0.0", + "jinja2@2.7.2" + ], + "upgradePath": [], + "isUpgradable": false, + "isPatchable": false, + "name": "jinja2", + "version": "2.7.2" + }, + { + "CVSSv3": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:L/I:L/A:L/RL:O", + "alternativeIds": [], + "creationTime": "2019-04-07T10:24:16.310959Z", + "credit": [ + "Unknown" + ], + "cvssScore": 6, + "description": "## Overview\n[jinja2](https://pypi.org/project/Jinja2/) is a template engine written in pure Python. It provides a Django inspired non-XML syntax but supports inline expressions and an optional sandboxed environment.\n\nAffected versions of this package are vulnerable to Sandbox Escape via the `str.format_map`.\n## Remediation\nUpgrade `jinja2` to version 2.10.1 or higher.\n## References\n- [Release Notes](https://palletsprojects.com/blog/jinja-2-10-1-released)\n", + "disclosureTime": "2019-04-07T00:42:43Z", + "exploit": "Not Defined", + "fixedIn": [ + "2.10.1" + ], + "functions": [], + "functions_new": [], + "id": "SNYK-PYTHON-JINJA2-174126", + "identifiers": { + "CVE": [ + "CVE-2019-10906" + ], + "CWE": [ + "CWE-265" + ] + }, + "language": "python", + "modificationTime": "2020-06-12T14:36:55.661596Z", + "moduleName": "jinja2", + "packageManager": "pip", + "packageName": "jinja2", + "patches": [], + "proprietary": false, + "publicationTime": "2019-04-07T00:42:43Z", + "references": [ + { + "title": "Release Notes", + "url": "https://palletsprojects.com/blog/jinja-2-10-1-released" + } + ], + "semver": { + "vulnerable": [ + "[,2.10.1)" + ] + }, + "severity": "medium", + "severityWithCritical": "medium", + "title": "Sandbox Escape", + "from": [ + "pip-app@0.0.0", + "jinja2@2.7.2" + ], + "upgradePath": [], + "isUpgradable": false, + "isPatchable": false, + "name": "jinja2", + "version": "2.7.2" + }, + { + "CVSSv3": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:L", + "alternativeIds": [], + "creationTime": "2017-05-28T08:29:50.295000Z", + "credit": [ + "Arun Babu Neelicattu" + ], + "cvssScore": 5.3, + "description": "## Overview\r\n[`jinja2`](https://pypi.python.org/pypi/jinja2) is a small but fast and easy to use stand-alone template engine written in pure python.\r\nFileSystemBytecodeCache in Jinja2 2.7.2 does not properly create temporary directories, which allows local users to gain privileges by pre-creating a temporary directory with a user's uid.\r\n\r\n**NOTE:** this vulnerability exists because of an incomplete fix for [CVE-2014-1402](https://snyk.io/vulnSNYK-PYTHON-JINJA2-40028).\r\n\r\n## References\r\n- [NVD](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2014-0012)\r\n- [Bugzilla redhat](https://bugzilla.redhat.com/show_bug.cgi?id=1051421)\r\n- [GitHub PR #1](https://github.com/mitsuhiko/jinja2/pull/292)\r\n- [GitHub PR #2](https://github.com/mitsuhiko/jinja2/pull/296)\r\n- [GitHub Commit](https://github.com/mitsuhiko/jinja2/commit/acb672b6a179567632e032f547582f30fa2f4aa7)\r\n", + "disclosureTime": "2014-01-18T05:33:40.101000Z", + "exploit": "Not Defined", + "fixedIn": [], + "functions": [], + "functions_new": [], + "id": "SNYK-PYTHON-JINJA2-40250", + "identifiers": { + "CVE": [ + "CVE-2014-0012" + ], + "CWE": [ + "CWE-264" + ] + }, + "language": "python", + "modificationTime": "2019-02-17T08:46:41.648104Z", + "moduleName": "jinja2", + "packageManager": "pip", + "packageName": "jinja2", + "patches": [], + "proprietary": false, + "publicationTime": "2014-01-18T05:33:40.101000Z", + "references": [ + { + "title": "GitHub Commit", + "url": "https://github.com/mitsuhiko/jinja2/commit/acb672b6a179567632e032f547582f30fa2f4aa7" + }, + { + "title": "GitHub PR", + "url": "https://github.com/mitsuhiko/jinja2/pull/292" + }, + { + "title": "GitHub PR", + "url": "https://github.com/mitsuhiko/jinja2/pull/296" + }, + { + "title": "NVD", + "url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2014-0012" + }, + { + "title": "RedHat Bugzilla Bug", + "url": "https://bugzilla.redhat.com/show_bug.cgi?id=1051421" + } + ], + "semver": { + "vulnerable": [ + "[2.7.2]" + ] + }, + "severity": "medium", + "severityWithCritical": "medium", + "title": "Privilege Escalation", + "from": [ + "pip-app@0.0.0", + "jinja2@2.7.2" + ], + "upgradePath": [], + "isUpgradable": false, + "isPatchable": false, + "name": "jinja2", + "version": "2.7.2" + }, + { + "CVSSv3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N", + "alternativeIds": [], + "creationTime": "2019-07-29T13:28:48.288799Z", + "credit": [ + "Unknown" + ], + "cvssScore": 8.6, + "description": "## Overview\n[jinja2](https://pypi.org/project/Jinja2/) is a template engine written in pure Python. It provides a Django inspired non-XML syntax but supports inline expressions and an optional sandboxed environment.\n\nAffected versions of this package are vulnerable to Sandbox Bypass. Users were allowed to insert `str.format` through web templates, leading to an escape from sandbox.\n## Remediation\nUpgrade `jinja2` to version 2.8.1 or higher.\n## References\n- [GitHub Commit](https://github.com/pallets/jinja/commit/9b53045c34e61013dc8f09b7e52a555fa16bed16)\n", + "disclosureTime": "2016-12-29T13:27:18Z", + "exploit": "Not Defined", + "fixedIn": [ + "2.8.1" + ], + "functions": [], + "functions_new": [], + "id": "SNYK-PYTHON-JINJA2-455616", + "identifiers": { + "CVE": [ + "CVE-2016-10745" + ], + "CWE": [ + "CWE-234" + ] + }, + "language": "python", + "modificationTime": "2020-06-12T14:36:58.461729Z", + "moduleName": "jinja2", + "packageManager": "pip", + "packageName": "jinja2", + "patches": [], + "proprietary": false, + "publicationTime": "2019-07-30T13:11:16Z", + "references": [ + { + "title": "GitHub Commit", + "url": "https://github.com/pallets/jinja/commit/9b53045c34e61013dc8f09b7e52a555fa16bed16" + } + ], + "semver": { + "vulnerable": [ + "[2.5, 2.8.1)" + ] + }, + "severity": "high", + "severityWithCritical": "high", + "title": "Sandbox Bypass", + "from": [ + "pip-app@0.0.0", + "jinja2@2.7.2" + ], + "upgradePath": [], + "isUpgradable": false, + "isPatchable": false, + "name": "jinja2", + "version": "2.7.2" + } + ], + "ok": false, + "dependencyCount": 2, + "org": "bananaq", + "policy": "# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.\nversion: v1.19.0\nignore: {}\npatch: {}\n", + "isPrivate": true, + "licensesPolicy": {}, + "packageManager": "pip", + "ignoreSettings": null, + "summary": "4 vulnerable dependency paths", + "remediation": { + "unresolved": [ + { + "CVSSv3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L/E:P", + "alternativeIds": [], + "creationTime": "2020-09-25T17:30:26.286074Z", + "credit": [ + "Yeting Li" + ], + "cvssScore": 5.3, + "description": "## Overview\n[jinja2](https://pypi.org/project/Jinja2/) is a template engine written in pure Python. It provides a Django inspired non-XML syntax but supports inline expressions and an optional sandboxed environment.\n\nAffected versions of this package are vulnerable to Regular Expression Denial of Service (ReDoS). The ReDoS vulnerability is mainly due to the `_punctuation_re regex` operator and its use of multiple wildcards. The last wildcard is the most exploitable as it searches for trailing punctuation.\r\n\r\nThis issue can be mitigated by using Markdown to format user content instead of the urlize filter, or by implementing request timeouts or limiting process memory.\r\n\r\n### PoC by Yeting Li\r\n```\r\nfrom jinja2.utils import urlize\r\nfrom time import perf_counter\r\n\r\nfor i in range(3):\r\n text = \"abc@\" + \".\" * (i+1)*5000 + \"!\"\r\n LEN = len(text)\r\n BEGIN = perf_counter()\r\n urlize(text)\r\n DURATION = perf_counter() - BEGIN\r\n print(f\"{LEN}: took {DURATION} seconds!\")\r\n```\n\n## Details\n\nDenial of Service (DoS) describes a family of attacks, all aimed at making a system inaccessible to its original and legitimate users. There are many types of DoS attacks, ranging from trying to clog the network pipes to the system by generating a large volume of traffic from many machines (a Distributed Denial of Service - DDoS - attack) to sending crafted requests that cause a system to crash or take a disproportional amount of time to process.\n\nThe Regular expression Denial of Service (ReDoS) is a type of Denial of Service attack. Regular expressions are incredibly powerful, but they aren't very intuitive and can ultimately end up making it easy for attackers to take your site down.\n\nLet’s take the following regular expression as an example:\n```js\nregex = /A(B|C+)+D/\n```\n\nThis regular expression accomplishes the following:\n- `A` The string must start with the letter 'A'\n- `(B|C+)+` The string must then follow the letter A with either the letter 'B' or some number of occurrences of the letter 'C' (the `+` matches one or more times). The `+` at the end of this section states that we can look for one or more matches of this section.\n- `D` Finally, we ensure this section of the string ends with a 'D'\n\nThe expression would match inputs such as `ABBD`, `ABCCCCD`, `ABCBCCCD` and `ACCCCCD`\n\nIt most cases, it doesn't take very long for a regex engine to find a match:\n\n```bash\n$ time node -e '/A(B|C+)+D/.test(\"ACCCCCCCCCCCCCCCCCCCCCCCCCCCCD\")'\n0.04s user 0.01s system 95% cpu 0.052 total\n\n$ time node -e '/A(B|C+)+D/.test(\"ACCCCCCCCCCCCCCCCCCCCCCCCCCCCX\")'\n1.79s user 0.02s system 99% cpu 1.812 total\n```\n\nThe entire process of testing it against a 30 characters long string takes around ~52ms. But when given an invalid string, it takes nearly two seconds to complete the test, over ten times as long as it took to test a valid string. The dramatic difference is due to the way regular expressions get evaluated.\n\nMost Regex engines will work very similarly (with minor differences). The engine will match the first possible way to accept the current character and proceed to the next one. If it then fails to match the next one, it will backtrack and see if there was another way to digest the previous character. If it goes too far down the rabbit hole only to find out the string doesn’t match in the end, and if many characters have multiple valid regex paths, the number of backtracking steps can become very large, resulting in what is known as _catastrophic backtracking_.\n\nLet's look at how our expression runs into this problem, using a shorter string: \"ACCCX\". While it seems fairly straightforward, there are still four different ways that the engine could match those three C's:\n1. CCC\n2. CC+C\n3. C+CC\n4. C+C+C.\n\nThe engine has to try each of those combinations to see if any of them potentially match against the expression. When you combine that with the other steps the engine must take, we can use [RegEx 101 debugger](https://regex101.com/debugger) to see the engine has to take a total of 38 steps before it can determine the string doesn't match.\n\nFrom there, the number of steps the engine must use to validate a string just continues to grow.\n\n| String | Number of C's | Number of steps |\n| -------|-------------:| -----:|\n| ACCCX | 3 | 38\n| ACCCCX | 4 | 71\n| ACCCCCX | 5 | 136\n| ACCCCCCCCCCCCCCX | 14 | 65,553\n\n\nBy the time the string includes 14 C's, the engine has to take over 65,000 steps just to see if the string is valid. These extreme situations can cause them to work very slowly (exponentially related to input size, as shown above), allowing an attacker to exploit this and can cause the service to excessively consume CPU, resulting in a Denial of Service.\n\n## Remediation\nUpgrade `jinja2` to version 2.11.3 or higher.\n## References\n- [GitHub Additional Information](https://github.com/pallets/jinja/blob/ab81fd9c277900c85da0c322a2ff9d68a235b2e6/src/jinja2/utils.py#L20)\n- [GitHub PR](https://github.com/pallets/jinja/pull/1343)\n", + "disclosureTime": "2020-09-25T17:29:19Z", + "exploit": "Proof of Concept", + "fixedIn": [ + "2.11.3" + ], + "functions": [], + "functions_new": [], + "id": "SNYK-PYTHON-JINJA2-1012994", + "identifiers": { + "CVE": [ + "CVE-2020-28493" + ], + "CWE": [ + "CWE-400" + ] + }, + "language": "python", + "modificationTime": "2021-02-01T19:52:16.877030Z", + "moduleName": "jinja2", + "packageManager": "pip", + "packageName": "jinja2", + "patches": [], + "proprietary": true, + "publicationTime": "2021-02-01T19:52:17Z", + "references": [ + { + "title": "GitHub Additional Information", + "url": "https://github.com/pallets/jinja/blob/ab81fd9c277900c85da0c322a2ff9d68a235b2e6/src/jinja2/utils.py%23L20" + }, + { + "title": "GitHub PR", + "url": "https://github.com/pallets/jinja/pull/1343" + } + ], + "semver": { + "vulnerable": [ + "[,2.11.3)" + ] + }, + "severity": "medium", + "severityWithCritical": "medium", + "title": "Regular Expression Denial of Service (ReDoS)", + "from": [ + "pip-app@0.0.0", + "jinja2@2.7.2" + ], + "upgradePath": [], + "isUpgradable": false, + "isPatchable": false, + "isPinnable": true, + "name": "jinja2", + "version": "2.7.2" + }, + { + "CVSSv3": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:L/I:L/A:L/RL:O", + "alternativeIds": [], + "creationTime": "2019-04-07T10:24:16.310959Z", + "credit": [ + "Unknown" + ], + "cvssScore": 6, + "description": "## Overview\n[jinja2](https://pypi.org/project/Jinja2/) is a template engine written in pure Python. It provides a Django inspired non-XML syntax but supports inline expressions and an optional sandboxed environment.\n\nAffected versions of this package are vulnerable to Sandbox Escape via the `str.format_map`.\n## Remediation\nUpgrade `jinja2` to version 2.10.1 or higher.\n## References\n- [Release Notes](https://palletsprojects.com/blog/jinja-2-10-1-released)\n", + "disclosureTime": "2019-04-07T00:42:43Z", + "exploit": "Not Defined", + "fixedIn": [ + "2.10.1" + ], + "functions": [], + "functions_new": [], + "id": "SNYK-PYTHON-JINJA2-174126", + "identifiers": { + "CVE": [ + "CVE-2019-10906" + ], + "CWE": [ + "CWE-265" + ] + }, + "language": "python", + "modificationTime": "2020-06-12T14:36:55.661596Z", + "moduleName": "jinja2", + "packageManager": "pip", + "packageName": "jinja2", + "patches": [], + "proprietary": false, + "publicationTime": "2019-04-07T00:42:43Z", + "references": [ + { + "title": "Release Notes", + "url": "https://palletsprojects.com/blog/jinja-2-10-1-released" + } + ], + "semver": { + "vulnerable": [ + "[,2.10.1)" + ] + }, + "severity": "medium", + "severityWithCritical": "medium", + "title": "Sandbox Escape", + "from": [ + "pip-app@0.0.0", + "jinja2@2.7.2" + ], + "upgradePath": [], + "isUpgradable": false, + "isPatchable": false, + "isPinnable": true, + "name": "jinja2", + "version": "2.7.2" + }, + { + "CVSSv3": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:L", + "alternativeIds": [], + "creationTime": "2017-05-28T08:29:50.295000Z", + "credit": [ + "Arun Babu Neelicattu" + ], + "cvssScore": 5.3, + "description": "## Overview\r\n[`jinja2`](https://pypi.python.org/pypi/jinja2) is a small but fast and easy to use stand-alone template engine written in pure python.\r\nFileSystemBytecodeCache in Jinja2 2.7.2 does not properly create temporary directories, which allows local users to gain privileges by pre-creating a temporary directory with a user's uid.\r\n\r\n**NOTE:** this vulnerability exists because of an incomplete fix for [CVE-2014-1402](https://snyk.io/vulnSNYK-PYTHON-JINJA2-40028).\r\n\r\n## References\r\n- [NVD](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2014-0012)\r\n- [Bugzilla redhat](https://bugzilla.redhat.com/show_bug.cgi?id=1051421)\r\n- [GitHub PR #1](https://github.com/mitsuhiko/jinja2/pull/292)\r\n- [GitHub PR #2](https://github.com/mitsuhiko/jinja2/pull/296)\r\n- [GitHub Commit](https://github.com/mitsuhiko/jinja2/commit/acb672b6a179567632e032f547582f30fa2f4aa7)\r\n", + "disclosureTime": "2014-01-18T05:33:40.101000Z", + "exploit": "Not Defined", + "fixedIn": [], + "functions": [], + "functions_new": [], + "id": "SNYK-PYTHON-JINJA2-40250", + "identifiers": { + "CVE": [ + "CVE-2014-0012" + ], + "CWE": [ + "CWE-264" + ] + }, + "language": "python", + "modificationTime": "2019-02-17T08:46:41.648104Z", + "moduleName": "jinja2", + "packageManager": "pip", + "packageName": "jinja2", + "patches": [], + "proprietary": false, + "publicationTime": "2014-01-18T05:33:40.101000Z", + "references": [ + { + "title": "GitHub Commit", + "url": "https://github.com/mitsuhiko/jinja2/commit/acb672b6a179567632e032f547582f30fa2f4aa7" + }, + { + "title": "GitHub PR", + "url": "https://github.com/mitsuhiko/jinja2/pull/292" + }, + { + "title": "GitHub PR", + "url": "https://github.com/mitsuhiko/jinja2/pull/296" + }, + { + "title": "NVD", + "url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2014-0012" + }, + { + "title": "RedHat Bugzilla Bug", + "url": "https://bugzilla.redhat.com/show_bug.cgi?id=1051421" + } + ], + "semver": { + "vulnerable": [ + "[2.7.2]" + ] + }, + "severity": "medium", + "severityWithCritical": "medium", + "title": "Privilege Escalation", + "from": [ + "pip-app@0.0.0", + "jinja2@2.7.2" + ], + "upgradePath": [], + "isUpgradable": false, + "isPatchable": false, + "isPinnable": false, + "name": "jinja2", + "version": "2.7.2" + }, + { + "CVSSv3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N", + "alternativeIds": [], + "creationTime": "2019-07-29T13:28:48.288799Z", + "credit": [ + "Unknown" + ], + "cvssScore": 8.6, + "description": "## Overview\n[jinja2](https://pypi.org/project/Jinja2/) is a template engine written in pure Python. It provides a Django inspired non-XML syntax but supports inline expressions and an optional sandboxed environment.\n\nAffected versions of this package are vulnerable to Sandbox Bypass. Users were allowed to insert `str.format` through web templates, leading to an escape from sandbox.\n## Remediation\nUpgrade `jinja2` to version 2.8.1 or higher.\n## References\n- [GitHub Commit](https://github.com/pallets/jinja/commit/9b53045c34e61013dc8f09b7e52a555fa16bed16)\n", + "disclosureTime": "2016-12-29T13:27:18Z", + "exploit": "Not Defined", + "fixedIn": [ + "2.8.1" + ], + "functions": [], + "functions_new": [], + "id": "SNYK-PYTHON-JINJA2-455616", + "identifiers": { + "CVE": [ + "CVE-2016-10745" + ], + "CWE": [ + "CWE-234" + ] + }, + "language": "python", + "modificationTime": "2020-06-12T14:36:58.461729Z", + "moduleName": "jinja2", + "packageManager": "pip", + "packageName": "jinja2", + "patches": [], + "proprietary": false, + "publicationTime": "2019-07-30T13:11:16Z", + "references": [ + { + "title": "GitHub Commit", + "url": "https://github.com/pallets/jinja/commit/9b53045c34e61013dc8f09b7e52a555fa16bed16" + } + ], + "semver": { + "vulnerable": [ + "[2.5, 2.8.1)" + ] + }, + "severity": "high", + "severityWithCritical": "high", + "title": "Sandbox Bypass", + "from": [ + "pip-app@0.0.0", + "jinja2@2.7.2" + ], + "upgradePath": [], + "isUpgradable": false, + "isPatchable": false, + "isPinnable": true, + "name": "jinja2", + "version": "2.7.2" + } + ], + "upgrade": {}, + "patch": {}, + "ignore": {}, + "pin": { + "jinja2@2.7.2": { + "upgradeTo": "jinja2@2.11.3", + "vulns": [ + "SNYK-PYTHON-JINJA2-1012994", + "SNYK-PYTHON-JINJA2-174126", + "SNYK-PYTHON-JINJA2-455616" + ], + "isTransitive": false + } + } + }, + "filesystemPolicy": false, + "filtered": { + "ignore": [], + "patch": [] + }, + "uniqueCount": 4, + "projectName": "pip-app", + "foundProjectCount": 23 +} From 23bd1007542155f762871d99ed8f7f0a85153662 Mon Sep 17 00:00:00 2001 From: ghe Date: Tue, 16 Mar 2021 15:02:52 +0000 Subject: [PATCH 5/9] feat: improve snyk fix spinner output --- packages/snyk-fix/src/index.ts | 2 - .../test/unit/__snapshots__/fix.spec.ts.snap | 2 +- src/cli/commands/fix/get-display-path.ts | 13 +++++++ src/cli/commands/fix/index.ts | 11 +++--- src/lib/plugins/get-deps-from-plugin.ts | 2 +- src/lib/snyk-test/assemble-payloads.ts | 4 +- src/lib/snyk-test/run-test.ts | 14 +++++-- src/lib/types.ts | 1 + .../__snapshots__/cli-fix.system.spec.ts.snap | 37 ------------------- test/cli-fix.system.spec.ts | 23 +----------- test/get-display-path.functional.spec.ts | 23 ++++++++++++ 11 files changed, 58 insertions(+), 74 deletions(-) create mode 100644 src/cli/commands/fix/get-display-path.ts delete mode 100644 test/__snapshots__/cli-fix.system.spec.ts.snap create mode 100644 test/get-display-path.functional.spec.ts diff --git a/packages/snyk-fix/src/index.ts b/packages/snyk-fix/src/index.ts index 36f0ebd166..1e323c8184 100644 --- a/packages/snyk-fix/src/index.ts +++ b/packages/snyk-fix/src/index.ts @@ -7,7 +7,6 @@ import stripAnsi = require('strip-ansi'); import * as outputFormatter from './lib/output-formatters/show-results-summary'; import { loadPlugin } from './plugins/load-plugin'; import { FixHandlerResultByPlugin } from './plugins/types'; -export { EntityToFix } from './types'; import { EntityToFix, ErrorsByEcoSystem, FixedMeta, FixOptions } from './types'; import { convertErrorToUserMessage } from './lib/errors/error-to-user-message'; @@ -62,7 +61,6 @@ export async function fix( text: 'Done', symbol: meta.fixed === 0 ? chalk.red('βœ–') : chalk.green('βœ”'), }); - return { results: resultsByPlugin, exceptions: exceptionsByScanType, diff --git a/packages/snyk-fix/test/unit/__snapshots__/fix.spec.ts.snap b/packages/snyk-fix/test/unit/__snapshots__/fix.spec.ts.snap index 3523d535b0..35c12d2eec 100644 --- a/packages/snyk-fix/test/unit/__snapshots__/fix.spec.ts.snap +++ b/packages/snyk-fix/test/unit/__snapshots__/fix.spec.ts.snap @@ -165,7 +165,7 @@ Object { "fixSummary": "Successful fixes: requirements.txt - βœ” Pinned django from 1.6.1 to 2.0.1 + βœ” Upgraded django from 1.6.1 to 2.0.1 Unresolved items: diff --git a/src/cli/commands/fix/get-display-path.ts b/src/cli/commands/fix/get-display-path.ts new file mode 100644 index 0000000000..cd9c08b6a0 --- /dev/null +++ b/src/cli/commands/fix/get-display-path.ts @@ -0,0 +1,13 @@ +import * as pathLib from 'path'; + +import { isLocalFolder } from '../../../lib/detect'; + +export function getDisplayPath(path: string): string { + if (!isLocalFolder(path)) { + return path; + } + if (path === process.cwd()) { + return '.'; + } + return pathLib.relative(process.cwd(), path); +} diff --git a/src/cli/commands/fix/index.ts b/src/cli/commands/fix/index.ts index 06d6de5c80..4d7fa6bb9e 100644 --- a/src/cli/commands/fix/index.ts +++ b/src/cli/commands/fix/index.ts @@ -2,7 +2,6 @@ export = fix; import * as Debug from 'debug'; import * as snykFix from '@snyk/fix'; -import * as pathLib from 'path'; import * as ora from 'ora'; import { MethodArgs } from '../../args'; @@ -17,6 +16,7 @@ import { validateTestOptions } from '../test/validate-test-options'; import { setDefaultTestOptions } from '../test/set-default-test-options'; import { validateFixCommandIsSupported } from './validate-fix-command-is-supported'; import { Options, TestOptions } from '../../../lib/types'; +import { getDisplayPath } from './get-display-path'; const debug = Debug('snyk-fix'); const snykFixFeatureFlag = 'cliSnykFix'; @@ -70,11 +70,10 @@ async function runSnykTestLegacy( stdOutSpinner.start(); for (const path of paths) { - let relativePath = path; + let displayPath = path; try { - const { dir } = pathLib.parse(path); - relativePath = pathLib.relative(process.cwd(), dir); - stdOutSpinner.info(`Running \`snyk test\` for ${relativePath}`); + displayPath = getDisplayPath(path); + stdOutSpinner.info(`Running \`snyk test\` for ${displayPath}`); // Create a copy of the options so a specific test can // modify them i.e. add `options.file` etc. We'll need // these options later. @@ -99,7 +98,7 @@ async function runSnykTestLegacy( results.push(...newRes); } catch (error) { const testError = formatTestError(error); - const userMessage = `Test for ${relativePath} failed with error: ${testError.message}.\nRun \`snyk test ${relativePath} -d\` for more information.`; + const userMessage = `Test for ${displayPath} failed with error: ${testError.message}.\nRun \`snyk test ${displayPath} -d\` for more information.`; stdErrSpinner.fail(userMessage); debug(userMessage); } diff --git a/src/lib/plugins/get-deps-from-plugin.ts b/src/lib/plugins/get-deps-from-plugin.ts index 245a22d2b3..94166d2d3a 100644 --- a/src/lib/plugins/get-deps-from-plugin.ts +++ b/src/lib/plugins/get-deps-from-plugin.ts @@ -85,7 +85,7 @@ export async function getDepsFromPlugin( root, ); - if (!options.json && userWarningMessage) { + if (!options.json && !options.quiet && userWarningMessage) { console.warn(chalk.bold.red(userWarningMessage)); } return inspectRes; diff --git a/src/lib/snyk-test/assemble-payloads.ts b/src/lib/snyk-test/assemble-payloads.ts index 7d15f82918..d527ce0768 100644 --- a/src/lib/snyk-test/assemble-payloads.ts +++ b/src/lib/snyk-test/assemble-payloads.ts @@ -31,7 +31,9 @@ export async function assembleEcosystemPayloads( path.relative('..', '.') + ' project dir'); spinner.clear(spinnerLbl)(); - await spinner(spinnerLbl); + if (!options.quiet) { + await spinner(spinnerLbl); + } try { const plugin = getPlugin(ecosystem); diff --git a/src/lib/snyk-test/run-test.ts b/src/lib/snyk-test/run-test.ts index 893465f857..5e94c3a572 100644 --- a/src/lib/snyk-test/run-test.ts +++ b/src/lib/snyk-test/run-test.ts @@ -240,7 +240,9 @@ async function sendAndParseResults( const iacResults: Promise[] = []; await spinner.clear(spinnerLbl)(); - await spinner(spinnerLbl); + if (!options.quiet) { + await spinner(spinnerLbl); + } for (const payload of payloads) { iacResults.push( queue.add(async () => { @@ -266,7 +268,9 @@ async function sendAndParseResults( const results: TestResult[] = []; for (const payload of payloads) { await spinner.clear(spinnerLbl)(); - await spinner(spinnerLbl); + if (!options.quiet) { + await spinner(spinnerLbl); + } /** sendTestPayload() deletes the request.body from the payload once completed. */ const payloadCopy = Object.assign({}, payload); const res = await sendTestPayload(payload); @@ -556,7 +560,9 @@ async function assembleLocalPayloads( try { const payloads: Payload[] = []; await spinner.clear(spinnerLbl)(); - await spinner(spinnerLbl); + if (!options.quiet) { + await spinner(spinnerLbl); + } if (options.iac) { return assembleIacLocalPayloads(root, options); } @@ -564,7 +570,7 @@ async function assembleLocalPayloads( const failedResults = (deps as MultiProjectResultCustom).failedResults; if (failedResults?.length) { await spinner.clear(spinnerLbl)(); - if (!options.json) { + if (!options.json && !options.quiet) { console.warn( chalk.bold.red( `βœ— ${failedResults.length}/${failedResults.length + diff --git a/src/lib/types.ts b/src/lib/types.ts index f48bfa276f..f6ce5d2338 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -80,6 +80,7 @@ export interface Options { debug?: boolean; sarif?: boolean; 'group-issues'?: boolean; + quiet?: boolean; } // TODO(kyegupov): catch accessing ['undefined-properties'] via noImplicitAny diff --git a/test/__snapshots__/cli-fix.system.spec.ts.snap b/test/__snapshots__/cli-fix.system.spec.ts.snap deleted file mode 100644 index 48bd27c0ee..0000000000 --- a/test/__snapshots__/cli-fix.system.spec.ts.snap +++ /dev/null @@ -1,37 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`snyk fix (system tests) \`shows expected response when Python project was skipped because of missing remediation data --all-projects\` 1`] = ` -"β„Ή Running \`snyk test\` for test/acceptance/workspaces -βœ– Done -βœ– No successful fixes - -Unresolved items: - - package.json - βœ– npm is not supported. - -Summary: - - 1 items were not fixed - 0 items were successfully fixed - -" -`; - -exports[`snyk fix (system tests) \`shows expected response when nothing could be fixed + returns exit code 2\` 1`] = ` -"β„Ή Running \`snyk test\` for test/acceptance/workspaces -βœ– Done -βœ– No successful fixes - -Unresolved items: - - package.json - βœ– npm is not supported. - -Summary: - - 1 items were not fixed - 0 items were successfully fixed - -" -`; diff --git a/test/cli-fix.system.spec.ts b/test/cli-fix.system.spec.ts index 63c2fc8678..2c1de030f3 100644 --- a/test/cli-fix.system.spec.ts +++ b/test/cli-fix.system.spec.ts @@ -217,7 +217,7 @@ describe('snyk fix (system tests)', () => { throw new Error('Test expected to return an error'); } expect(stderr).toBe(''); - expect(stripAnsi(stdout)).toMatchSnapshot(); + expect(stripAnsi(stdout)).toMatch('No successful fixes'); expect(err.message).toMatch('Command failed'); expect(err.code).toBe(2); done(); @@ -226,25 +226,4 @@ describe('snyk fix (system tests)', () => { }, testTimeout, ); - it( - '`shows expected response when Python project was skipped because of missing remediation data --all-projects`', - (done) => { - exec( - `node ${main} fix ${noVulnsProjectPath}`, - { - env: { - PATH: process.env.PATH, - SNYK_TOKEN: apiKey, - SNYK_API, - SNYK_HOST, - }, - }, - (err, stdout) => { - expect(stripAnsi(stdout)).toMatchSnapshot(); - done(); - }, - ); - }, - testTimeout, - ); }); diff --git a/test/get-display-path.functional.spec.ts b/test/get-display-path.functional.spec.ts new file mode 100644 index 0000000000..1c201845e6 --- /dev/null +++ b/test/get-display-path.functional.spec.ts @@ -0,0 +1,23 @@ +import * as pathLib from 'path'; +import { getDisplayPath } from '../src/cli/commands/fix/get-display-path'; + +describe('getDisplayPath', () => { + it('paths that do not exist on disk returned as is', () => { + const displayPath = getDisplayPath('semver@2.3.1'); + expect(displayPath).toEqual('semver@2.3.1'); + }); + it('current path is displayed as .', () => { + const displayPath = getDisplayPath(process.cwd()); + expect(displayPath).toEqual('.'); + }); + it('a local path is returned as relative path to current dir', () => { + const displayPath = getDisplayPath(`test${pathLib.sep}fixtures`); + expect(displayPath).toEqual(`test${pathLib.sep}fixtures`); + }); + it('a local full path is returned as relative path to current dir', () => { + const displayPath = getDisplayPath( + pathLib.resolve(process.cwd(), 'test', 'fixtures'), + ); + expect(displayPath).toEqual(`test${pathLib.sep}fixtures`); + }); +}); From c68c7dabe21fbc57e53162b5758243f116151063 Mon Sep 17 00:00:00 2001 From: ghe Date: Fri, 19 Mar 2021 16:02:33 +0000 Subject: [PATCH 6/9] feat: add @snyk/fix as a dep --- package.json | 1 - test/smoke/spec/snyk_fix_spec.sh | 13 +++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 test/smoke/spec/snyk_fix_spec.sh diff --git a/package.json b/package.json index 6fc22bbc2d..9fb02e1ea0 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,6 @@ "micromatch": "4.0.2", "needle": "2.6.0", "open": "^7.0.3", - "ora": "5.3.0", "os-name": "^3.0.0", "promise-queue": "^2.2.5", "proxy-agent": "^3.1.1", diff --git a/test/smoke/spec/snyk_fix_spec.sh b/test/smoke/spec/snyk_fix_spec.sh new file mode 100644 index 0000000000..0b35a189d4 --- /dev/null +++ b/test/smoke/spec/snyk_fix_spec.sh @@ -0,0 +1,13 @@ +#shellcheck shell=sh + +Describe "Snyk fix command" + Describe "supported only with FF" + + It "by default snyk fix is not supported" + When run snyk fix + The status should be failure + The output should include "is not supported" + The stderr should equal "" + End + End +End From ca508acc13e6cdbd9d34afa4afbff09225ddf94e Mon Sep 17 00:00:00 2001 From: ghe Date: Fri, 19 Mar 2021 17:50:42 +0000 Subject: [PATCH 7/9] test: smoke test for `snyk fix` Show this is not supported by default --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index 9fb02e1ea0..d2a202b557 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "@snyk/cli-interface": "2.11.0", "@snyk/code-client": "3.1.5", "@snyk/dep-graph": "^1.27.1", + "@snyk/fix": "1.501.0", "@snyk/gemfile": "1.2.0", "@snyk/graphlib": "^2.1.9-patch.3", "@snyk/inquirer": "^7.3.3-patch", @@ -106,6 +107,7 @@ "micromatch": "4.0.2", "needle": "2.6.0", "open": "^7.0.3", + "ora": "5.3.0", "os-name": "^3.0.0", "promise-queue": "^2.2.5", "proxy-agent": "^3.1.1", From 77e6665c0eca6b612c238125baee23418f90677c Mon Sep 17 00:00:00 2001 From: Avishag Israeli Date: Tue, 23 Mar 2021 11:58:55 +0200 Subject: [PATCH 8/9] chore: lerna release with exact version This will ensure that cross-package dependencies will be all using the same version --- .circleci/config.yml | 2 +- packages/snyk-fix/src/index.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a94215840c..7bb52ef9c8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -441,7 +441,7 @@ jobs: - run: name: Lerna Publish command: | - lerna publish minor --yes --no-push --no-git-tag-version + lerna publish minor --yes --no-push --no-git-tag-version --exact - run: name: Install osslsigncode command: sudo apt-get install -y osslsigncode diff --git a/packages/snyk-fix/src/index.ts b/packages/snyk-fix/src/index.ts index 1e323c8184..629a36468b 100644 --- a/packages/snyk-fix/src/index.ts +++ b/packages/snyk-fix/src/index.ts @@ -61,6 +61,7 @@ export async function fix( text: 'Done', symbol: meta.fixed === 0 ? chalk.red('βœ–') : chalk.green('βœ”'), }); + return { results: resultsByPlugin, exceptions: exceptionsByScanType, From 3d872fb10f46f20cd0e1bfa80c067b3ad6a0e438 Mon Sep 17 00:00:00 2001 From: ghe Date: Mon, 29 Mar 2021 16:47:26 +0100 Subject: [PATCH 9/9] test: assert exact errors for unsupported --- src/cli/modes.ts | 2 +- test/cli-fix.functional.spec.ts | 7 +++-- test/cli-fix.system.spec.ts | 26 ------------------- .../validate-fix-command-is-supported.spec.ts | 18 ++++++++++--- 4 files changed, 18 insertions(+), 35 deletions(-) diff --git a/src/cli/modes.ts b/src/cli/modes.ts index 8e99579929..e769ff3ba9 100644 --- a/src/cli/modes.ts +++ b/src/cli/modes.ts @@ -8,7 +8,7 @@ interface ModeData { const modes: Record = { source: { - allowedCommands: ['test', 'monitor', 'fix'], + allowedCommands: ['test', 'monitor'], config: (args): [] => { args['source'] = true; return args; diff --git a/test/cli-fix.functional.spec.ts b/test/cli-fix.functional.spec.ts index 3b3f4bf9ab..25acd5350f 100644 --- a/test/cli-fix.functional.spec.ts +++ b/test/cli-fix.functional.spec.ts @@ -3,8 +3,7 @@ import * as fs from 'fs'; import cli = require('../src/cli/commands'); import * as snyk from '../src/lib'; - -import * as validateFixSupported from '../src/cli/commands/fix/validate-fix-command-is-supported'; +import * as featureFlags from '../src/lib/feature-flags'; import stripAnsi from 'strip-ansi'; @@ -49,8 +48,8 @@ const pipWithRemediation = JSON.parse( describe('snyk fix (functional tests)', () => { beforeAll(async () => { jest - .spyOn(validateFixSupported, 'validateFixCommandIsSupported') - .mockResolvedValue(true); + .spyOn(featureFlags, 'isFeatureFlagSupportedForOrg') + .mockResolvedValue({ ok: true }); }); afterAll(async () => { diff --git a/test/cli-fix.system.spec.ts b/test/cli-fix.system.spec.ts index 2c1de030f3..1aa28b4f99 100644 --- a/test/cli-fix.system.spec.ts +++ b/test/cli-fix.system.spec.ts @@ -143,32 +143,6 @@ describe('snyk fix (system tests)', () => { testTimeout, ); - /* this command is different - * it shows help text not an error when command is not supported - */ - it.skip( - '`shows error when called with container`', - (done) => { - exec( - `node ${main} container fix`, - { - env: { - PATH: process.env.PATH, - SNYK_TOKEN: apiKey, - SNYK_API, - SNYK_HOST, - }, - }, - (err, stdout, stderr) => { - expect(stderr).toBe(''); - expect(stdout).toMatch('COMMANDS'); - expect(err).toBe(null); - done(); - }, - ); - }, - testTimeout, - ); it( '`shows error when called with --code`', (done) => { diff --git a/test/validate-fix-command-is-supported.spec.ts b/test/validate-fix-command-is-supported.spec.ts index fe2760a098..0a59845373 100644 --- a/test/validate-fix-command-is-supported.spec.ts +++ b/test/validate-fix-command-is-supported.spec.ts @@ -1,4 +1,6 @@ import { validateFixCommandIsSupported } from '../src/cli/commands/fix/validate-fix-command-is-supported'; +import { CommandNotSupportedError } from '../src/lib/errors/command-not-supported'; +import { FeatureNotSupportedByEcosystemError } from '../src/lib/errors/not-supported-by-ecosystem'; import * as featureFlags from '../src/lib/feature-flags'; import { ShowVulnPaths } from '../src/lib/types'; describe('setDefaultTestOptions', () => { @@ -19,7 +21,9 @@ describe('setDefaultTestOptions', () => { .spyOn(featureFlags, 'isFeatureFlagSupportedForOrg') .mockResolvedValue({ ok: false }); const options = { path: '/', showVulnPaths: 'all' as ShowVulnPaths }; - expect(validateFixCommandIsSupported(options)).rejects.toThrowError(''); + expect(validateFixCommandIsSupported(options)).rejects.toThrowError( + new CommandNotSupportedError('snyk fix', undefined), + ); }); it('fix is NOT supported for --source + enabled FF', () => { @@ -31,7 +35,9 @@ describe('setDefaultTestOptions', () => { showVulnPaths: 'all' as ShowVulnPaths, source: true, }; - expect(validateFixCommandIsSupported(options)).rejects.toThrowError(''); + expect(validateFixCommandIsSupported(options)).rejects.toThrowError( + new FeatureNotSupportedByEcosystemError('snyk fix', 'cpp'), + ); }); it('fix is NOT supported for --docker + enabled FF', () => { @@ -43,7 +49,9 @@ describe('setDefaultTestOptions', () => { showVulnPaths: 'all' as ShowVulnPaths, docker: true, }; - expect(validateFixCommandIsSupported(options)).rejects.toThrowError(''); + expect(validateFixCommandIsSupported(options)).rejects.toThrowError( + new FeatureNotSupportedByEcosystemError('snyk fix', 'docker'), + ); }); it('fix is NOT supported for --code + enabled FF', () => { @@ -55,6 +63,8 @@ describe('setDefaultTestOptions', () => { showVulnPaths: 'all' as ShowVulnPaths, code: true, }; - expect(validateFixCommandIsSupported(options)).rejects.toThrowError(''); + expect(validateFixCommandIsSupported(options)).rejects.toThrowError( + new FeatureNotSupportedByEcosystemError('snyk fix', 'code'), + ); }); });