-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Render the actionable advice via new formatter
- Loading branch information
Showing
8 changed files
with
1,074 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
150 changes: 150 additions & 0 deletions
150
src/cli/commands/test/formatters/remediation-based-format-issues.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
import * as _ from 'lodash'; | ||
import chalk from 'chalk'; | ||
import { TestOptions } from '../../../../lib/types'; | ||
import { RemediationResult, PatchRemediation, | ||
DependencyUpdates, IssueData, SEVERITY, GroupedVuln } from '../../../../lib/snyk-test/legacy'; | ||
|
||
interface BasicVulnInfo { | ||
title: string; | ||
severity: SEVERITY; | ||
isNew: boolean; | ||
name: string; | ||
version: string; | ||
fixedIn: string[]; | ||
} | ||
|
||
export function formatIssuesWithRemediation( | ||
vulns: GroupedVuln[], | ||
remediationInfo: RemediationResult, | ||
options: TestOptions, | ||
): string[] { | ||
|
||
const basicVulnInfo: { | ||
[name: string]: BasicVulnInfo, | ||
} = {}; | ||
|
||
for (const vuln of vulns) { | ||
basicVulnInfo[vuln.metadata.id] = { | ||
title: vuln.title, | ||
severity: vuln.severity, | ||
isNew: vuln.isNew, | ||
name: vuln.name, | ||
version: vuln.version, | ||
fixedIn: vuln.fixedIn, | ||
}; | ||
} | ||
const results = [chalk.bold.white('Remediation advice')]; | ||
|
||
const upgradeTextArray = constructUpgradesText(remediationInfo.upgrade, basicVulnInfo); | ||
if (upgradeTextArray.length > 0) { | ||
results.push(upgradeTextArray.join('\n')); | ||
} | ||
|
||
const patchedTextArray = constructPatchesText(remediationInfo.patch, basicVulnInfo); | ||
|
||
if (patchedTextArray.length > 0) { | ||
results.push(patchedTextArray.join('\n')); | ||
} | ||
|
||
const unfixableIssuesTextArray = constructUnfixableText(remediationInfo.unresolved); | ||
|
||
if (unfixableIssuesTextArray.length > 0) { | ||
results.push(unfixableIssuesTextArray.join('\n')); | ||
} | ||
|
||
return results; | ||
} | ||
|
||
function constructPatchesText( | ||
patches: { | ||
[name: string]: PatchRemediation; | ||
}, | ||
basicVulnInfo: { | ||
[name: string]: BasicVulnInfo; | ||
}, | ||
): string[] { | ||
|
||
if (!(Object.keys(patches).length > 0)) { | ||
return []; | ||
} | ||
const patchedTextArray = [chalk.bold.green('Patchable issues:')]; | ||
for (const id of Object.keys(patches)) { | ||
// 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(id, basicVulnInfo[id].title, basicVulnInfo[id].severity, basicVulnInfo[id].isNew); | ||
patchedTextArray.push(patchedText + thisPatchFixes); | ||
} | ||
|
||
return patchedTextArray; | ||
} | ||
|
||
function constructUpgradesText( | ||
upgrades: DependencyUpdates, | ||
basicVulnInfo: { | ||
[name: string]: BasicVulnInfo; | ||
}, | ||
): string[] { | ||
|
||
if (!(Object.keys(upgrades).length > 0)) { | ||
return []; | ||
} | ||
|
||
const upgradeTextArray = [chalk.bold.green('Upgradable 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 | ||
.map((id) => formatIssue( | ||
id, | ||
basicVulnInfo[id].title, basicVulnInfo[id].severity, basicVulnInfo[id].isNew)) | ||
.join('\n'); | ||
upgradeTextArray.push(upgradeText + thisUpgradeFixes); | ||
} | ||
return upgradeTextArray; | ||
} | ||
|
||
function constructUnfixableText(unresolved: IssueData[]) { | ||
if (!(unresolved.length > 0)) { | ||
return []; | ||
} | ||
const unfixableIssuesTextArray = [chalk.bold.white('Non-fixable issues:')]; | ||
for (const issue of unresolved) { | ||
const packageNameAtVersion = chalk.bold.whiteBright(`\n ${issue.packageName}@${issue.version} \n`); | ||
unfixableIssuesTextArray | ||
.push(packageNameAtVersion + formatIssue(issue.id, issue.title, issue.severity, issue.isNew)); | ||
} | ||
|
||
return unfixableIssuesTextArray; | ||
} | ||
|
||
function formatIssue(id: string, title: string, severity: SEVERITY, isNew: boolean): string { | ||
const severitiesColourMapping = { | ||
low: { | ||
colorFunc(text) { | ||
return chalk.blueBright(text); | ||
}, | ||
}, | ||
medium: { | ||
colorFunc(text) { | ||
return chalk.yellowBright(text); | ||
}, | ||
}, | ||
high: { | ||
colorFunc(text) { | ||
return chalk.redBright(text); | ||
}, | ||
}, | ||
}; | ||
const newBadge = isNew ? ' (new)' : ''; | ||
return severitiesColourMapping[severity].colorFunc( | ||
` ✗ ${chalk.bold(title)}${newBadge} [${titleCaseText(severity)} Severity]`, | ||
) + `[${id}] `; | ||
} | ||
|
||
function titleCaseText(text) { | ||
return text[0].toUpperCase() + text.slice(1); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import * as tap from 'tap'; | ||
import * as sinon from 'sinon'; | ||
import * as _ from 'lodash'; | ||
import * as fs from 'fs'; | ||
|
||
// tslint:disable-next-line:no-var-requires | ||
const snykTest = require('../../src/cli/commands/test'); | ||
import * as snyk from '../../src/lib'; | ||
|
||
const {test} = tap; | ||
(tap as any).runOnly = false; // <- for debug. set to true, and replace a test to only(..) | ||
|
||
test('`test ruby-app` remediation displayed', async (t) => { | ||
chdirWorkspaces(); | ||
const stubbedResponse = JSON.parse( | ||
fs.readFileSync(__dirname + '/workspaces/ruby-app/test-graph-response-with-remediation.json', 'utf8'), | ||
); | ||
const snykTestStub = sinon.stub(snyk, 'test').returns(stubbedResponse); | ||
try { | ||
await snykTest('ruby-app'); | ||
} catch (error) { | ||
const res = error.message; | ||
t.match(res, 'Upgrade rack@1.6.5 to rack@1.6.11 to fix', 'upgrade advice displayed'); | ||
t.match(res, 'Tested 3 dependencies for known issues, found 6 issues, 8 vulnerable paths.'); | ||
} | ||
|
||
snykTestStub.restore(); | ||
t.end(); | ||
}); | ||
|
||
function chdirWorkspaces(subdir: string = '') { | ||
process.chdir(__dirname + '/workspaces' + (subdir ? '/' + subdir : '')); | ||
} |
Oops, something went wrong.