Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: (experimental) show pinning remediation advice for Python #755

Merged
merged 1 commit into from
Sep 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 1 addition & 20 deletions src/cli/commands/monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import chalk from 'chalk';
import * as pathUtil from 'path';
import * as spinner from '../../lib/spinner';

import request = require('../../lib/request');
import * as detect from '../../lib/detect';
import * as plugins from '../../lib/plugins';
import {ModuleInfo} from '../../lib/module-info'; // TODO(kyegupov): fix import
Expand All @@ -28,14 +27,10 @@ import {
UnsupportedFeatureFlagError,
} from '../../lib/errors';
import { legacyPlugin as pluginApi } from '@snyk/cli-interface';
import { isFeatureFlagSupportedForOrg } from '../../lib/feature-flags';

const SEPARATOR = '\n-------------------------------------------------------\n';

interface OrgFeatureFlagResponse {
ok: boolean;
userMessage?: string;
}

interface GoodResult {
ok: true;
data: string;
Expand Down Expand Up @@ -292,17 +287,3 @@ function formatMonitorOutput(
packageManager,
})) : strOutput;
}

async function isFeatureFlagSupportedForOrg(featureFlag: string): Promise<OrgFeatureFlagResponse> {
const response = await request({
method: 'GET',
headers: {
Authorization: `token ${snyk.api}`,
},
url: `${config.API}/cli-config/feature-flags/${featureFlag}`,
gzip: true,
json: true,
});

return (response as any).body;
}
6 changes: 3 additions & 3 deletions src/cli/commands/test/formatters/legacy-format-issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as config from '../../../../lib/config';
import {Options, TestOptions, ShowVulnPaths} from '../../../../lib/types';
import {isLocalFolder} from '../../../../lib/detect';
import { WIZARD_SUPPORTED_PACKAGE_MANAGERS } from '../../../../lib/package-managers';
import { GroupedVuln, AnnotatedIssue } from '../../../../lib/snyk-test/legacy';
import { GroupedVuln, AnnotatedIssue, DockerIssue } from '../../../../lib/snyk-test/legacy';

export function formatIssues(vuln: GroupedVuln, options: Options & TestOptions) {
const vulnID = vuln.list[0].id;
Expand Down Expand Up @@ -132,8 +132,8 @@ function createTruncatedVulnsPathsText(vulnList: AnnotatedIssue[], show: ShowVul
}

function createFixedInText(vuln: GroupedVuln): string {
if (vuln.nearestFixedInVersion) {
return chalk.bold('\n Fixed in: ' + vuln.nearestFixedInVersion);
if ((vuln as DockerIssue).nearestFixedInVersion) {
return chalk.bold('\n Fixed in: ' + (vuln as DockerIssue).nearestFixedInVersion);
} else if (vuln.fixedIn && vuln.fixedIn.length > 0) {
return chalk.bold('\n Fixed in: ' + vuln.fixedIn.join(', '));
}
Expand Down
156 changes: 128 additions & 28 deletions src/cli/commands/test/formatters/remediation-based-format-issues.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import * as _ from 'lodash';
import chalk from 'chalk';
import * as wrap from 'wrap-ansi';
import * as config from '../../../../lib/config';
import { TestOptions, ShowVulnPaths } from '../../../../lib/types';
import { RemediationResult, PatchRemediation,
DependencyUpdates, IssueData, SEVERITY, GroupedVuln } from '../../../../lib/snyk-test/legacy';
import {
RemediationChanges, PatchRemediation,
DependencyUpdates, IssueData, SEVERITY, GroupedVuln,
DependencyPins,
UpgradeRemediation,
PinRemediation,
} from '../../../../lib/snyk-test/legacy';
import { SEVERITIES } from '../../../../lib/snyk-test/common';

interface BasicVulnInfo {
Expand All @@ -19,9 +23,18 @@ interface BasicVulnInfo {
paths: string[][];
}

interface TopLevelPackageUpgrade {
name: string;
version: string;
}

interface UpgradesByAffectedPackage {
[pkgNameAndVersion: string]: TopLevelPackageUpgrade[];
}

export function formatIssuesWithRemediation(
vulns: GroupedVuln[],
remediationInfo: RemediationResult,
remediationInfo: RemediationChanges,
options: TestOptions,
): string[] {

Expand Down Expand Up @@ -55,7 +68,28 @@ export function formatIssuesWithRemediation(

const results = [chalk.bold.white('Remediation advice')];

const upgradeTextArray = constructUpgradesText(remediationInfo.upgrade, basicVulnInfo, options);
let upgradeTextArray: string[];
if (remediationInfo.pin && Object.keys(remediationInfo.pin).length) {
const upgradesByAffected: UpgradesByAffectedPackage = {};
for (const topLevelPkg of Object.keys(remediationInfo.upgrade)) {
for (const targetPkgStr of remediationInfo.upgrade[topLevelPkg].upgrades) {
if (!upgradesByAffected[targetPkgStr]) {
upgradesByAffected[targetPkgStr] = [];
}
upgradesByAffected[targetPkgStr].push({
name: topLevelPkg,
version: remediationInfo.upgrade[topLevelPkg].upgradeTo,
});
}
}
upgradeTextArray = constructPinText(remediationInfo.pin, upgradesByAffected, basicVulnInfo, options);
const allVulnIds = new Set();
Object.keys(remediationInfo.pin).forEach(
(name) => remediationInfo.pin[name].issues.forEach((vid) => allVulnIds.add(vid)));
remediationInfo.unresolved = remediationInfo.unresolved.filter((issue) => !allVulnIds.has(issue.id));
} else {
upgradeTextArray = constructUpgradesText(remediationInfo.upgrade, basicVulnInfo, options);
}
if (upgradeTextArray.length > 0) {
results.push(upgradeTextArray.join('\n'));
}
Expand Down Expand Up @@ -140,8 +174,7 @@ function constructPatchesText(
// todo: add vulnToPatch package name
const packageAtVersion = `${basicVulnInfo[id].name}@${basicVulnInfo[id].version}`;
const patchedText = `\n Patch available for ${chalk.bold.whiteBright(packageAtVersion)}\n`;
const thisPatchFixes =
formatIssue(
const thisPatchFixes = formatIssue(
id,
basicVulnInfo[id].title,
basicVulnInfo[id].severity,
Expand All @@ -157,6 +190,40 @@ function constructPatchesText(
return patchedTextArray;
}

function thisUpgradeFixes(vulnIds: string[], basicVulnInfo: Record<string, BasicVulnInfo>, testOptions: TestOptions) {
return vulnIds
.sort((a, b) => getSeverityValue(basicVulnInfo[a].severity) - getSeverityValue(basicVulnInfo[b].severity))
.filter((id) => basicVulnInfo[id].type !== 'license')
.map((id) => formatIssue(
id,
basicVulnInfo[id].title,
basicVulnInfo[id].severity,
basicVulnInfo[id].isNew,
undefined,
`${basicVulnInfo[id].name}@${basicVulnInfo[id].version}`,
basicVulnInfo[id].paths,
testOptions,
))
.join('\n');
}

function processUpgrades(
sink: string[],
upgradesByDep: DependencyUpdates | DependencyPins,
deps: string[],
basicVulnInfo: Record<string, BasicVulnInfo>,
testOptions: TestOptions,
) {
for (const dep of deps) {
const data = upgradesByDep[dep];
const upgradeDepTo = data.upgradeTo;
const vulnIds = (data as UpgradeRemediation).vulns || (data as PinRemediation).issues;
const upgradeText =
`\n Upgrade ${chalk.bold.whiteBright(dep)} to ${chalk.bold.whiteBright(upgradeDepTo)} to fix\n`;
sink.push(upgradeText + thisUpgradeFixes(vulnIds, basicVulnInfo, testOptions));
}
}

function constructUpgradesText(
upgrades: DependencyUpdates,
basicVulnInfo: {
Expand All @@ -169,28 +236,61 @@ function constructUpgradesText(
return [];
}

const upgradeTextArray = [chalk.bold.green('\nUpgradable Issues:')];
for (const upgrade of Object.keys(upgrades)) {
const upgradeDepTo = _.get(upgrades, [upgrade, 'upgradeTo']);
const vulnIds = _.get(upgrades, [upgrade, 'vulns']);
const upgradeText =
`\n Upgrade ${chalk.bold.whiteBright(upgrade)} to ${chalk.bold.whiteBright(upgradeDepTo)} to fix\n`;
const thisUpgradeFixes = vulnIds
.sort((a, b) => getSeverityValue(basicVulnInfo[a].severity) - getSeverityValue(basicVulnInfo[b].severity))
.filter((id) => basicVulnInfo[id].type !== 'license')
.map((id) => formatIssue(
id,
basicVulnInfo[id].title,
basicVulnInfo[id].severity,
basicVulnInfo[id].isNew,
undefined,
`${basicVulnInfo[id].name}@${basicVulnInfo[id].version}`,
basicVulnInfo[id].paths,
testOptions,
))
.join('\n');
upgradeTextArray.push(upgradeText + thisUpgradeFixes);
const upgradeTextArray = [chalk.bold.green('\nIssues to fix by upgrading:')];
processUpgrades(upgradeTextArray, upgrades, Object.keys(upgrades), basicVulnInfo, testOptions);
return upgradeTextArray;
}

function constructPinText(
pins: DependencyPins,
upgradesByAffected: UpgradesByAffectedPackage, // classical "remediation via top-level dep" upgrades
basicVulnInfo: Record<string, BasicVulnInfo>,
testOptions: TestOptions,
): string[] {

if (!(Object.keys(pins).length)) {
return [];
}

// First, direct upgrades
const upgradeTextArray: string[] = [];

const upgradeables = Object.keys(pins).filter((name) => !pins[name].isTransitive);
if (upgradeables.length) {
upgradeTextArray.push(chalk.bold.green('\nIssues to fix by upgrading existing dependencies:'));
processUpgrades(upgradeTextArray, pins, upgradeables, basicVulnInfo, testOptions);
}

// Second, pins
const pinables = Object.keys(pins).filter((name) => pins[name].isTransitive);

if (pinables.length) {
upgradeTextArray.push(chalk.bold.green('\nIssues to fix by pinning sub-dependencies:'));

for (const pkgName of pinables) {
const data = pins[pkgName];
const vulnIds = data.issues;
const upgradeDepTo = data.upgradeTo;
const upgradeText =
`\n Pin ${chalk.bold.whiteBright(pkgName)} to ${chalk.bold.whiteBright(upgradeDepTo)} to fix`;
upgradeTextArray.push(upgradeText);
upgradeTextArray.push(thisUpgradeFixes(vulnIds, basicVulnInfo, testOptions));

// Finally, if we have some upgrade paths that fix the same issues, suggest them as well.
const topLevelUpgradesAlreadySuggested = new Set();
for (const vid of vulnIds) {
for (const topLevelPkg of upgradesByAffected[pkgName + '@' + basicVulnInfo[vid].version] || []) {
const setKey = `${topLevelPkg.name}\n${topLevelPkg.version}`;
if (!topLevelUpgradesAlreadySuggested.has(setKey)) {
topLevelUpgradesAlreadySuggested.add(setKey);
upgradeTextArray.push(' The issues above can also be fixed by upgrading top-level dependency ' +
`${topLevelPkg.name} to ${topLevelPkg.version}`);
}
}
}
}
}

return upgradeTextArray;
}

Expand Down
22 changes: 22 additions & 0 deletions src/lib/feature-flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import request = require('./request');
import snyk = require('.'); // TODO(kyegupov): fix import
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe time had come ? :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no :( that index.js file is absolutely insane.

import * as config from './config';

interface OrgFeatureFlagResponse {
ok: boolean;
userMessage?: string;
}

export async function isFeatureFlagSupportedForOrg(featureFlag: string): Promise<OrgFeatureFlagResponse> {
const response = await request({
method: 'GET',
headers: {
Authorization: `token ${snyk.api}`,
},
url: `${config.API}/cli-config/feature-flags/${featureFlag}`,
gzip: true,
json: true,
});

return (response as any).body;
}
4 changes: 4 additions & 0 deletions src/lib/package-managers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ export const PROTECT_SUPPORTED_PACKAGE_MANAGERS: SupportedPackageManagers[]
= ['yarn', 'npm'];
export const GRAPH_SUPPORTED_PACKAGE_MANAGERS: SupportedPackageManagers[]
= ['npm', 'sbt'];
// For ecosystems with a flat set of libraries (e.g. Python, JVM), one can
// "pin" a transitive dependency
export const PINNING_SUPPORTED_PACKAGE_MANAGERS: SupportedPackageManagers[]
= ['pip'];
Loading