Skip to content

Commit 1449c57

Browse files
authoredMar 30, 2021
Merge pull request #1707 from snyk/feat/snyk-fix
Feat: snyk fix v1 (Python) in behind FF
2 parents 5ebd685 + 3d872fb commit 1449c57

33 files changed

+2281
-13
lines changed
 

‎.circleci/config.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ jobs:
441441
- run:
442442
name: Lerna Publish
443443
command: |
444-
lerna publish minor --yes --no-push --no-git-tag-version
444+
lerna publish minor --yes --no-push --no-git-tag-version --exact
445445
- run:
446446
name: Install osslsigncode
447447
command: sudo apt-get install -y osslsigncode

‎package.json

+2
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
"@snyk/cli-interface": "2.11.0",
7575
"@snyk/code-client": "3.4.0",
7676
"@snyk/dep-graph": "^1.27.1",
77+
"@snyk/fix": "1.501.0",
7778
"@snyk/gemfile": "1.2.0",
7879
"@snyk/graphlib": "^2.1.9-patch.3",
7980
"@snyk/inquirer": "^7.3.3-patch",
@@ -106,6 +107,7 @@
106107
"micromatch": "4.0.2",
107108
"needle": "2.6.0",
108109
"open": "^7.0.3",
110+
"ora": "5.3.0",
109111
"os-name": "^3.0.0",
110112
"promise-queue": "^2.2.5",
111113
"proxy-agent": "^3.1.1",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { DepGraphData } from '@snyk/dep-graph';
2+
import { TestResult } from '../../../lib/ecosystems/types';
3+
import { TestResult as LegacyTestResult } from '../../../lib/snyk-test/legacy';
4+
5+
export function convertLegacyTestResultToNew(
6+
testResult: LegacyTestResult,
7+
): TestResult {
8+
return {
9+
issuesData: {} as any, // TODO: add converter
10+
issues: [], // TODO: add converter
11+
remediation: testResult.remediation,
12+
// TODO: grab this once Ecosystems flow starts sending back ScanResult
13+
depGraphData: {} as DepGraphData,
14+
};
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ScanResult } from '../../../lib/ecosystems/types';
2+
import { TestResult } from '../../../lib/snyk-test/legacy';
3+
4+
export function convertLegacyTestResultToScanResult(
5+
testResult: TestResult,
6+
): ScanResult {
7+
if (!testResult.packageManager) {
8+
throw new Error(
9+
'Only results with packageManagers are supported for conversion',
10+
);
11+
}
12+
return {
13+
identity: {
14+
type: testResult.packageManager,
15+
// this is because not all plugins send it back today, but we should always have it
16+
targetFile: testResult.targetFile || testResult.displayTargetFile,
17+
},
18+
name: testResult.projectName,
19+
// TODO: grab this once Ecosystems flow starts sending back ScanResult
20+
facts: [],
21+
policy: testResult.policy,
22+
// TODO: grab this once Ecosystems flow starts sending back ScanResult
23+
target: {} as any,
24+
};
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as fs from 'fs';
2+
import * as pathLib from 'path';
3+
import { convertLegacyTestResultToNew } from './convert-legacy-test-result-to-new';
4+
import { convertLegacyTestResultToScanResult } from './convert-legacy-test-result-to-scan-result';
5+
import { TestResult } from '../../../lib/snyk-test/legacy';
6+
7+
export function convertLegacyTestResultToFixEntities(
8+
testResults: (TestResult | TestResult[]) | Error,
9+
root: string,
10+
): any {
11+
if (testResults instanceof Error) {
12+
return [];
13+
}
14+
const oldResults = Array.isArray(testResults) ? testResults : [testResults];
15+
return oldResults.map((res) => ({
16+
workspace: {
17+
readFile: async (path: string) => {
18+
return fs.readFileSync(pathLib.resolve(root, path), 'utf8');
19+
},
20+
writeFile: async (path: string, content: string) => {
21+
return fs.writeFileSync(pathLib.resolve(root, path), content, 'utf8');
22+
},
23+
},
24+
scanResult: convertLegacyTestResultToScanResult(res),
25+
testResult: convertLegacyTestResultToNew(res),
26+
}));
27+
}
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as pathLib from 'path';
2+
3+
import { isLocalFolder } from '../../../lib/detect';
4+
5+
export function getDisplayPath(path: string): string {
6+
if (!isLocalFolder(path)) {
7+
return path;
8+
}
9+
if (path === process.cwd()) {
10+
return '.';
11+
}
12+
return pathLib.relative(process.cwd(), path);
13+
}

‎src/cli/commands/fix/index.ts

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
export = fix;
2+
3+
import * as Debug from 'debug';
4+
import * as snykFix from '@snyk/fix';
5+
import * as ora from 'ora';
6+
7+
import { MethodArgs } from '../../args';
8+
import * as snyk from '../../../lib';
9+
import { TestResult } from '../../../lib/snyk-test/legacy';
10+
11+
import { convertLegacyTestResultToFixEntities } from './convert-legacy-tests-results-to-fix-entities';
12+
import { formatTestError } from '../test/format-test-error';
13+
import { processCommandArgs } from '../process-command-args';
14+
import { validateCredentials } from '../test/validate-credentials';
15+
import { validateTestOptions } from '../test/validate-test-options';
16+
import { setDefaultTestOptions } from '../test/set-default-test-options';
17+
import { validateFixCommandIsSupported } from './validate-fix-command-is-supported';
18+
import { Options, TestOptions } from '../../../lib/types';
19+
import { getDisplayPath } from './get-display-path';
20+
21+
const debug = Debug('snyk-fix');
22+
const snykFixFeatureFlag = 'cliSnykFix';
23+
24+
interface FixOptions {
25+
dryRun?: boolean;
26+
quiet?: boolean;
27+
}
28+
async function fix(...args: MethodArgs): Promise<string> {
29+
const { options: rawOptions, paths } = await processCommandArgs<FixOptions>(
30+
...args,
31+
);
32+
const options = setDefaultTestOptions<FixOptions>(rawOptions);
33+
debug(options);
34+
await validateFixCommandIsSupported(options);
35+
validateTestOptions(options);
36+
validateCredentials(options);
37+
const results: snykFix.EntityToFix[] = [];
38+
results.push(...(await runSnykTestLegacy(options, paths)));
39+
40+
// fix
41+
debug(
42+
`Organization has ${snykFixFeatureFlag} feature flag enabled for experimental Snyk fix functionality`,
43+
);
44+
const { dryRun, quiet } = options;
45+
const { fixSummary, meta } = await snykFix.fix(results, { dryRun, quiet });
46+
if (meta.fixed === 0) {
47+
throw new Error(fixSummary);
48+
}
49+
return fixSummary;
50+
}
51+
52+
/* @deprecated
53+
* TODO: once project envelope is default all code below will be deleted
54+
* we should be calling test via new Ecosystems instead
55+
*/
56+
async function runSnykTestLegacy(
57+
options: Options & TestOptions & FixOptions,
58+
paths: string[],
59+
): Promise<snykFix.EntityToFix[]> {
60+
const results: snykFix.EntityToFix[] = [];
61+
const stdOutSpinner = ora({
62+
isSilent: options.quiet,
63+
stream: process.stdout,
64+
});
65+
const stdErrSpinner = ora({
66+
isSilent: options.quiet,
67+
stream: process.stdout,
68+
});
69+
stdErrSpinner.start();
70+
stdOutSpinner.start();
71+
72+
for (const path of paths) {
73+
let displayPath = path;
74+
try {
75+
displayPath = getDisplayPath(path);
76+
stdOutSpinner.info(`Running \`snyk test\` for ${displayPath}`);
77+
// Create a copy of the options so a specific test can
78+
// modify them i.e. add `options.file` etc. We'll need
79+
// these options later.
80+
const snykTestOptions = {
81+
...options,
82+
path,
83+
projectName: options['project-name'],
84+
};
85+
86+
const testResults: TestResult[] = [];
87+
88+
const testResultForPath: TestResult | TestResult[] = await snyk.test(
89+
path,
90+
{ ...snykTestOptions, quiet: true },
91+
);
92+
testResults.push(
93+
...(Array.isArray(testResultForPath)
94+
? testResultForPath
95+
: [testResultForPath]),
96+
);
97+
const newRes = convertLegacyTestResultToFixEntities(testResults, path);
98+
results.push(...newRes);
99+
} catch (error) {
100+
const testError = formatTestError(error);
101+
const userMessage = `Test for ${displayPath} failed with error: ${testError.message}.\nRun \`snyk test ${displayPath} -d\` for more information.`;
102+
stdErrSpinner.fail(userMessage);
103+
debug(userMessage);
104+
}
105+
}
106+
stdOutSpinner.stop();
107+
stdErrSpinner.stop();
108+
return results;
109+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import * as Debug from 'debug';
2+
3+
import { getEcosystemForTest } from '../../../lib/ecosystems';
4+
5+
import { isFeatureFlagSupportedForOrg } from '../../../lib/feature-flags';
6+
import { CommandNotSupportedError } from '../../../lib/errors/command-not-supported';
7+
import { FeatureNotSupportedByEcosystemError } from '../../../lib/errors/not-supported-by-ecosystem';
8+
import { Options, TestOptions } from '../../../lib/types';
9+
10+
const debug = Debug('snyk-fix');
11+
const snykFixFeatureFlag = 'cliSnykFix';
12+
13+
export async function validateFixCommandIsSupported(
14+
options: Options & TestOptions,
15+
): Promise<boolean> {
16+
if (options.docker) {
17+
throw new FeatureNotSupportedByEcosystemError('snyk fix', 'docker');
18+
}
19+
20+
const ecosystem = getEcosystemForTest(options);
21+
if (ecosystem) {
22+
throw new FeatureNotSupportedByEcosystemError('snyk fix', ecosystem);
23+
}
24+
25+
const snykFixSupported = await isFeatureFlagSupportedForOrg(
26+
snykFixFeatureFlag,
27+
options.org,
28+
);
29+
30+
if (!snykFixSupported.ok) {
31+
debug(snykFixSupported.userMessage);
32+
throw new CommandNotSupportedError('snyk fix', options.org || undefined);
33+
}
34+
35+
return true;
36+
}

‎src/cli/commands/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const commands = {
1212
ignore: hotload('./ignore'),
1313
modules: hotload('./modules'),
1414
monitor: hotload('./monitor'),
15+
fix: hotload('./fix'),
1516
policy: hotload('./policy'),
1617
protect: hotload('./protect'),
1718
test: hotload('./test'),

‎src/cli/commands/test/generate-snyk-test-error.ts ‎src/cli/commands/test/format-test-error.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export function generateSnykTestError(error) {
1+
export function formatTestError(error) {
22
// Possible error cases:
33
// - the test found some vulns. `error.message` is a
44
// JSON-stringified

‎src/cli/commands/test/index.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
export = test;
22

3+
import * as Debug from 'debug';
4+
import * as pathLib from 'path';
35
const cloneDeep = require('lodash.clonedeep');
46
const assign = require('lodash.assign');
57
import chalk from 'chalk';
8+
69
import * as snyk from '../../../lib';
710
import { isCI } from '../../../lib/is-ci';
8-
import * as Debug from 'debug';
9-
import * as pathLib from 'path';
1011
import {
1112
IacFileInDirectory,
1213
Options,
@@ -51,10 +52,10 @@ import {
5152

5253
import { test as iacTest } from './iac-test-shim';
5354
import { validateCredentials } from './validate-credentials';
54-
import { generateSnykTestError } from './generate-snyk-test-error';
5555
import { validateTestOptions } from './validate-test-options';
5656
import { setDefaultTestOptions } from './set-default-test-options';
5757
import { processCommandArgs } from '../process-command-args';
58+
import { formatTestError } from './format-test-error';
5859

5960
const debug = Debug('snyk-test');
6061
const SEPARATOR = '\n-------------------------------------------------------\n';
@@ -108,7 +109,9 @@ async function test(...args: MethodArgs): Promise<TestCommandResult> {
108109
res = await snyk.test(path, testOpts);
109110
}
110111
} catch (error) {
111-
res = generateSnykTestError(error);
112+
// not throwing here but instead returning error response
113+
// for legacy flow reasons.
114+
res = formatTestError(error);
112115
}
113116

114117
// Not all test results are arrays in order to be backwards compatible

‎src/cli/commands/test/set-default-test-options.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import * as config from '../../../lib/config';
22
import { Options, ShowVulnPaths, TestOptions } from '../../../lib/types';
33

4-
export function setDefaultTestOptions(options: Options): Options & TestOptions {
4+
export function setDefaultTestOptions<CommandOptions>(
5+
options: Options & CommandOptions,
6+
): Options & TestOptions & CommandOptions {
57
const svpSupplied = (options['show-vulnerable-paths'] || '')
68
.toString()
79
.toLowerCase();

‎src/lib/ecosystems/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { DepGraphData } from '@snyk/dep-graph';
2+
import { RemediationChanges } from '../snyk-test/legacy';
23
import { Options } from '../types';
34

45
export type Ecosystem = 'cpp' | 'docker' | 'code';
@@ -71,6 +72,7 @@ export interface TestResult {
7172
issues: Issue[];
7273
issuesData: IssuesData;
7374
depGraphData: DepGraphData;
75+
remediation?: RemediationChanges;
7476
}
7577

7678
export interface EcosystemPlugin {
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { CustomError } from './custom-error';
2+
3+
export class CommandNotSupportedError extends CustomError {
4+
public readonly command: string;
5+
public readonly org?: string;
6+
7+
constructor(command: string, org?: string) {
8+
super(`${command} is not supported for org ${org}.`);
9+
this.code = 422;
10+
this.command = command;
11+
this.org = org;
12+
13+
this.userMessage = `\`${command}\` is not supported ${
14+
org ? `for org '${org}'` : ''
15+
}`;
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { CustomError } from './custom-error';
2+
import { SupportedPackageManagers } from '../package-managers';
3+
import { Ecosystem } from '../ecosystems/types';
4+
5+
export class FeatureNotSupportedByEcosystemError extends CustomError {
6+
public readonly feature: string;
7+
8+
constructor(
9+
feature: string,
10+
ecosystem: SupportedPackageManagers | Ecosystem,
11+
) {
12+
super(`Unsupported ecosystem ${ecosystem} for ${feature}.`);
13+
this.code = 422;
14+
this.feature = feature;
15+
16+
this.userMessage = `\`${feature}\` is not supported for ecosystem '${ecosystem}'`;
17+
}
18+
}

‎src/lib/plugins/get-deps-from-plugin.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export async function getDepsFromPlugin(
8585
root,
8686
);
8787

88-
if (!options.json && userWarningMessage) {
88+
if (!options.json && !options.quiet && userWarningMessage) {
8989
console.warn(chalk.bold.red(userWarningMessage));
9090
}
9191
return inspectRes;

‎src/lib/snyk-test/assemble-payloads.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ export async function assembleEcosystemPayloads(
3131
path.relative('..', '.') + ' project dir');
3232

3333
spinner.clear<void>(spinnerLbl)();
34-
await spinner(spinnerLbl);
34+
if (!options.quiet) {
35+
await spinner(spinnerLbl);
36+
}
3537

3638
try {
3739
const plugin = getPlugin(ecosystem);

0 commit comments

Comments
 (0)
Please sign in to comment.