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/package.json b/package.json index 7840c3c887..89d8d76e0a 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "@snyk/cli-interface": "2.11.0", "@snyk/code-client": "3.4.0", "@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", 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/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 new file mode 100644 index 0000000000..4d7fa6bb9e --- /dev/null +++ b/src/cli/commands/fix/index.ts @@ -0,0 +1,109 @@ +export = fix; + +import * as Debug from 'debug'; +import * as snykFix from '@snyk/fix'; +import * as ora from 'ora'; + +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'; +import { Options, TestOptions } from '../../../lib/types'; +import { getDisplayPath } from './get-display-path'; + +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); + debug(options); + await validateFixCommandIsSupported(options); + validateTestOptions(options); + validateCredentials(options); + const results: snykFix.EntityToFix[] = []; + results.push(...(await runSnykTestLegacy(options, paths))); + + // fix + debug( + `Organization has ${snykFixFeatureFlag} feature flag enabled for experimental Snyk fix functionality`, + ); + const { dryRun, quiet } = options; + const { fixSummary, meta } = await snykFix.fix(results, { dryRun, quiet }); + if (meta.fixed === 0) { + throw new Error(fixSummary); + } + return fixSummary; +} + +/* @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: 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) { + let displayPath = path; + try { + 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. + const snykTestOptions = { + ...options, + path, + projectName: options['project-name'], + }; + + const testResults: TestResult[] = []; + + 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); + const userMessage = `Test for ${displayPath} failed with error: ${testError.message}.\nRun \`snyk test ${displayPath} -d\` for more information.`; + stdErrSpinner.fail(userMessage); + debug(userMessage); + } + } + stdOutSpinner.stop(); + stdErrSpinner.stop(); + return results; +} 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..ab6a540cdc 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,10 @@ import { import { test as iacTest } from './iac-test-shim'; import { validateCredentials } from './validate-credentials'; -import { generateSnykTestError } from './generate-snyk-test-error'; import { validateTestOptions } from './validate-test-options'; import { setDefaultTestOptions } from './set-default-test-options'; import { processCommandArgs } from '../process-command-args'; +import { formatTestError } from './format-test-error'; const debug = Debug('snyk-test'); const SEPARATOR = '\n-------------------------------------------------------\n'; @@ -108,7 +109,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/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/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..09a75a1647 --- /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..5ac9e2afa5 --- /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/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 0f9829efba..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 @@ -195,6 +196,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/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/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.functional.spec.ts b/test/cli-fix.functional.spec.ts new file mode 100644 index 0000000000..25acd5350f --- /dev/null +++ b/test/cli-fix.functional.spec.ts @@ -0,0 +1,183 @@ +import * as pathLib from 'path'; +import * as fs from 'fs'; + +import cli = require('../src/cli/commands'); +import * as snyk from '../src/lib'; +import * as featureFlags from '../src/lib/feature-flags'; + +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(featureFlags, 'isFeatureFlagSupportedForOrg') + .mockResolvedValue({ ok: 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.system.spec.ts b/test/cli-fix.system.spec.ts new file mode 100644 index 0000000000..1aa28b4f99 --- /dev/null +++ b/test/cli-fix.system.spec.ts @@ -0,0 +1,203 @@ +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'); + +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', + ); + + 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, 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'", + ); + 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, 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'", + ); + 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, 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'", + ); + 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, 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'", + ); + 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(stripAnsi(stdout)).toMatch('No successful fixes'); + expect(err.message).toMatch('Command failed'); + expect(err.code).toBe(2); + done(); + }, + ); + }, + testTimeout, + ); +}); 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 +} 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`); + }); +}); 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/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 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..0a59845373 --- /dev/null +++ b/test/validate-fix-command-is-supported.spec.ts @@ -0,0 +1,70 @@ +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', () => { + 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( + new CommandNotSupportedError('snyk fix', undefined), + ); + }); + + 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( + new FeatureNotSupportedByEcosystemError('snyk fix', 'cpp'), + ); + }); + + 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( + new FeatureNotSupportedByEcosystemError('snyk fix', 'docker'), + ); + }); + + 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( + new FeatureNotSupportedByEcosystemError('snyk fix', 'code'), + ); + }); +});