"Fossies" - the Fresh Open Source Software Archive

Member "cli-1.1260.0/src/lib/formatters/remediation-based-format-issues.ts" (4 Dec 2023, 13040 Bytes) of package /linux/misc/snyk-cli-1.1260.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) TypeScript source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file.

    1 import chalk from 'chalk';
    2 import { icon } from '../theme';
    3 import { TestOptions } from '../../lib/types';
    4 import {
    5   DependencyPins,
    6   DependencyUpdates,
    7   GroupedVuln,
    8   IssueData,
    9   LegalInstruction,
   10   PatchRemediation,
   11   PinRemediation,
   12   RemediationChanges,
   13   SEVERITY,
   14   UpgradeRemediation,
   15 } from '../../lib/snyk-test/legacy';
   16 import { colorTextBySeverity } from '../../lib/snyk-test/common';
   17 import { formatLegalInstructions } from './legal-license-instructions';
   18 import { BasicVulnInfo, UpgradesByAffectedPackage } from './types';
   19 import { PATH_SEPARATOR } from '../constants';
   20 import { getSeverityValue } from './get-severity-value';
   21 import { getVulnerabilityUrl } from './get-vuln-url';
   22 
   23 export function formatIssuesWithRemediation(
   24   vulns: GroupedVuln[],
   25   remediationInfo: RemediationChanges,
   26   options: TestOptions,
   27 ): string[] {
   28   const basicVulnInfo: {
   29     [name: string]: BasicVulnInfo;
   30   } = {};
   31 
   32   const basicLicenseInfo: {
   33     [name: string]: BasicVulnInfo;
   34   } = {};
   35   for (const vuln of vulns) {
   36     const vulnData = {
   37       title: vuln.title,
   38       severity: vuln.severity,
   39       originalSeverity: vuln.originalSeverity,
   40       isNew: vuln.isNew,
   41       name: vuln.name,
   42       type: vuln.metadata.type,
   43       version: vuln.version,
   44       fixedIn: vuln.fixedIn,
   45       note: vuln.note,
   46       legalInstructions: vuln.legalInstructionsArray,
   47       paths: vuln.list.map((v) => v.from),
   48     };
   49 
   50     if (vulnData.type === 'license') {
   51       basicLicenseInfo[vuln.metadata.id] = vulnData;
   52     } else {
   53       basicVulnInfo[vuln.metadata.id] = vulnData;
   54     }
   55   }
   56 
   57   const results = [''];
   58 
   59   let upgradeTextArray: string[];
   60   if (remediationInfo.pin && Object.keys(remediationInfo.pin).length) {
   61     const upgradesByAffected: UpgradesByAffectedPackage = {};
   62     for (const topLevelPkg of Object.keys(remediationInfo.upgrade)) {
   63       for (const targetPkgStr of remediationInfo.upgrade[topLevelPkg]
   64         .upgrades) {
   65         if (!upgradesByAffected[targetPkgStr]) {
   66           upgradesByAffected[targetPkgStr] = [];
   67         }
   68         upgradesByAffected[targetPkgStr].push({
   69           name: topLevelPkg,
   70           version: remediationInfo.upgrade[topLevelPkg].upgradeTo,
   71         });
   72       }
   73     }
   74     upgradeTextArray = constructPinText(
   75       remediationInfo.pin,
   76       upgradesByAffected,
   77       basicVulnInfo,
   78       options,
   79     );
   80     const allVulnIds = new Set();
   81     Object.keys(remediationInfo.pin).forEach((name) =>
   82       remediationInfo.pin[name].vulns.forEach((vid) => allVulnIds.add(vid)),
   83     );
   84     remediationInfo.unresolved = remediationInfo.unresolved.filter(
   85       (issue) => !allVulnIds.has(issue.id),
   86     );
   87   } else {
   88     upgradeTextArray = constructUpgradesText(
   89       remediationInfo.upgrade,
   90       basicVulnInfo,
   91       options,
   92     );
   93   }
   94   if (upgradeTextArray.length > 0) {
   95     results.push(upgradeTextArray.join('\n'));
   96   }
   97 
   98   const patchedTextArray = constructPatchesText(
   99     remediationInfo.patch,
  100     basicVulnInfo,
  101     options,
  102   );
  103 
  104   if (patchedTextArray.length > 0) {
  105     results.push(patchedTextArray.join('\n'));
  106   }
  107 
  108   const unfixableIssuesTextArray = constructUnfixableText(
  109     remediationInfo.unresolved,
  110     basicVulnInfo,
  111     options,
  112   );
  113 
  114   if (unfixableIssuesTextArray.length > 0) {
  115     results.push(unfixableIssuesTextArray.join('\n'));
  116   }
  117 
  118   const licenseIssuesTextArray = constructLicenseText(
  119     basicLicenseInfo,
  120     options,
  121   );
  122 
  123   if (licenseIssuesTextArray.length > 0) {
  124     results.push(licenseIssuesTextArray.join('\n'));
  125   }
  126 
  127   return results;
  128 }
  129 
  130 function constructLicenseText(
  131   basicLicenseInfo: {
  132     [name: string]: BasicVulnInfo;
  133   },
  134   testOptions: TestOptions,
  135 ): string[] {
  136   if (!(Object.keys(basicLicenseInfo).length > 0)) {
  137     return [];
  138   }
  139 
  140   const licenseTextArray = [chalk.bold.green('\nLicense issues:')];
  141 
  142   for (const id of Object.keys(basicLicenseInfo)) {
  143     const licenseText = formatIssue(
  144       id,
  145       basicLicenseInfo[id].title,
  146       basicLicenseInfo[id].severity,
  147       basicLicenseInfo[id].isNew,
  148       `${basicLicenseInfo[id].name}@${basicLicenseInfo[id].version}`,
  149       basicLicenseInfo[id].paths,
  150       testOptions,
  151       basicLicenseInfo[id].note,
  152       undefined, // We can never override license rules, so no originalSeverity here
  153       basicLicenseInfo[id].legalInstructions,
  154     );
  155     licenseTextArray.push('\n' + licenseText);
  156   }
  157   return licenseTextArray;
  158 }
  159 
  160 function constructPatchesText(
  161   patches: {
  162     [name: string]: PatchRemediation;
  163   },
  164   basicVulnInfo: {
  165     [name: string]: BasicVulnInfo;
  166   },
  167   testOptions: TestOptions,
  168 ): string[] {
  169   if (!(Object.keys(patches).length > 0)) {
  170     return [];
  171   }
  172   const patchedTextArray = [chalk.bold.green('\nPatchable issues:')];
  173   for (const id of Object.keys(patches)) {
  174     if (!basicVulnInfo[id]) {
  175       continue;
  176     }
  177     if (basicVulnInfo[id].type === 'license') {
  178       continue;
  179     }
  180 
  181     // todo: add vulnToPatch package name
  182     const packageAtVersion = `${basicVulnInfo[id].name}@${basicVulnInfo[id].version}`;
  183     const patchedText = `\n  Patch available for ${chalk.bold.whiteBright(
  184       packageAtVersion,
  185     )}\n`;
  186     const thisPatchFixes = formatIssue(
  187       id,
  188       basicVulnInfo[id].title,
  189       basicVulnInfo[id].severity,
  190       basicVulnInfo[id].isNew,
  191       `${basicVulnInfo[id].name}@${basicVulnInfo[id].version}`,
  192       basicVulnInfo[id].paths,
  193       testOptions,
  194       basicVulnInfo[id].note,
  195       basicVulnInfo[id].originalSeverity,
  196     );
  197     patchedTextArray.push(patchedText + thisPatchFixes);
  198   }
  199 
  200   return patchedTextArray;
  201 }
  202 
  203 function thisUpgradeFixes(
  204   vulnIds: string[],
  205   basicVulnInfo: Record<string, BasicVulnInfo>,
  206   testOptions: TestOptions,
  207 ) {
  208   return vulnIds
  209     .filter((id) => basicVulnInfo[id]) // basicVulnInfo only contains issues with the specified severity levels
  210     .sort(
  211       (a, b) =>
  212         getSeverityValue(basicVulnInfo[a].severity) -
  213         getSeverityValue(basicVulnInfo[b].severity),
  214     )
  215     .filter((id) => basicVulnInfo[id].type !== 'license')
  216     .map((id) =>
  217       formatIssue(
  218         id,
  219         basicVulnInfo[id].title,
  220         basicVulnInfo[id].severity,
  221         basicVulnInfo[id].isNew,
  222         `${basicVulnInfo[id].name}@${basicVulnInfo[id].version}`,
  223         basicVulnInfo[id].paths,
  224         testOptions,
  225         basicVulnInfo[id].note,
  226         basicVulnInfo[id].originalSeverity,
  227         [],
  228       ),
  229     )
  230     .join('\n');
  231 }
  232 
  233 function processUpgrades(
  234   sink: string[],
  235   upgradesByDep: DependencyUpdates | DependencyPins,
  236   deps: string[],
  237   basicVulnInfo: Record<string, BasicVulnInfo>,
  238   testOptions: TestOptions,
  239 ) {
  240   for (const dep of deps) {
  241     const data = upgradesByDep[dep];
  242     const upgradeDepTo = data.upgradeTo;
  243     const vulnIds =
  244       (data as UpgradeRemediation).vulns || (data as PinRemediation).vulns;
  245     const upgradeText = `\n  Upgrade ${chalk.bold.whiteBright(
  246       dep,
  247     )} to ${chalk.bold.whiteBright(upgradeDepTo)} to fix\n`;
  248     sink.push(
  249       upgradeText + thisUpgradeFixes(vulnIds, basicVulnInfo, testOptions),
  250     );
  251   }
  252 }
  253 
  254 function constructUpgradesText(
  255   upgrades: DependencyUpdates,
  256   basicVulnInfo: {
  257     [name: string]: BasicVulnInfo;
  258   },
  259   testOptions: TestOptions,
  260 ): string[] {
  261   if (!(Object.keys(upgrades).length > 0)) {
  262     return [];
  263   }
  264 
  265   const upgradeTextArray = [chalk.bold.green('\nIssues to fix by upgrading:')];
  266   processUpgrades(
  267     upgradeTextArray,
  268     upgrades,
  269     Object.keys(upgrades),
  270     basicVulnInfo,
  271     testOptions,
  272   );
  273   return upgradeTextArray;
  274 }
  275 
  276 function constructPinText(
  277   pins: DependencyPins,
  278   upgradesByAffected: UpgradesByAffectedPackage, // classical "remediation via top-level dep" upgrades
  279   basicVulnInfo: Record<string, BasicVulnInfo>,
  280   testOptions: TestOptions,
  281 ): string[] {
  282   if (!Object.keys(pins).length) {
  283     return [];
  284   }
  285 
  286   const upgradeTextArray: string[] = [];
  287   upgradeTextArray.push(
  288     chalk.bold.green('\nIssues to fix by upgrading dependencies:'),
  289   );
  290 
  291   // First, direct upgrades
  292 
  293   const upgradeables = Object.keys(pins).filter(
  294     (name) => !pins[name].isTransitive,
  295   );
  296   if (upgradeables.length) {
  297     processUpgrades(
  298       upgradeTextArray,
  299       pins,
  300       upgradeables,
  301       basicVulnInfo,
  302       testOptions,
  303     );
  304   }
  305 
  306   // Second, pins
  307   const pinables = Object.keys(pins).filter((name) => pins[name].isTransitive);
  308 
  309   if (pinables.length) {
  310     for (const pkgName of pinables) {
  311       const data = pins[pkgName];
  312       const vulnIds = data.vulns;
  313       const upgradeDepTo = data.upgradeTo;
  314       const upgradeText = `\n  Pin ${chalk.bold.whiteBright(
  315         pkgName,
  316       )} to ${chalk.bold.whiteBright(upgradeDepTo)} to fix`;
  317       upgradeTextArray.push(upgradeText);
  318       upgradeTextArray.push(
  319         thisUpgradeFixes(vulnIds, basicVulnInfo, testOptions),
  320       );
  321 
  322       // Finally, if we have some upgrade paths that fix the same issues, suggest them as well.
  323       const topLevelUpgradesAlreadySuggested = new Set();
  324       for (const vid of vulnIds) {
  325         for (const topLevelPkg of upgradesByAffected[
  326           pkgName + '@' + basicVulnInfo[vid].version
  327         ] || []) {
  328           const setKey = `${topLevelPkg.name}\n${topLevelPkg.version}`;
  329           if (!topLevelUpgradesAlreadySuggested.has(setKey)) {
  330             topLevelUpgradesAlreadySuggested.add(setKey);
  331             upgradeTextArray.push(
  332               '  The issues above can also be fixed by upgrading top-level dependency ' +
  333                 `${topLevelPkg.name} to ${topLevelPkg.version}`,
  334             );
  335           }
  336         }
  337       }
  338     }
  339   }
  340 
  341   return upgradeTextArray;
  342 }
  343 
  344 function constructUnfixableText(
  345   unresolved: IssueData[],
  346   basicVulnInfo: Record<string, BasicVulnInfo>,
  347   testOptions: TestOptions,
  348 ) {
  349   if (!(unresolved.length > 0)) {
  350     return [];
  351   }
  352   const unfixableIssuesTextArray = [
  353     chalk.bold.white('\nIssues with no direct upgrade or patch:'),
  354   ];
  355   for (const issue of unresolved) {
  356     const issueInfo = basicVulnInfo[issue.id];
  357     if (!issueInfo) {
  358       // basicVulnInfo only contains issues with the specified severity levels
  359       continue;
  360     }
  361 
  362     const extraInfo =
  363       issue.fixedIn && issue.fixedIn.length
  364         ? `\n  This issue was fixed in versions: ${chalk.bold(
  365             issue.fixedIn.join(', '),
  366           )}`
  367         : '\n  No upgrade or patch available';
  368     unfixableIssuesTextArray.push(
  369       formatIssue(
  370         issue.id,
  371         issue.title,
  372         issue.severity,
  373         issue.isNew,
  374         `${issue.packageName}@${issue.version}`,
  375         issueInfo.paths,
  376         testOptions,
  377         issueInfo.note,
  378         issueInfo.originalSeverity,
  379         [],
  380       ) + `${extraInfo}`,
  381     );
  382   }
  383 
  384   if (unfixableIssuesTextArray.length === 1) {
  385     // seems we still only have
  386     // the initial section title, so nothing to return
  387     return [];
  388   }
  389 
  390   return unfixableIssuesTextArray;
  391 }
  392 
  393 export function printPath(path: string[], slice = 1) {
  394   return path.slice(slice).join(PATH_SEPARATOR);
  395 }
  396 
  397 export function formatIssue(
  398   id: string,
  399   title: string,
  400   severity: SEVERITY,
  401   isNew: boolean,
  402   vulnerableModule: string,
  403   paths: string[][],
  404   testOptions: TestOptions,
  405   note: string | false,
  406   originalSeverity?: SEVERITY,
  407   legalInstructions?: LegalInstruction[],
  408 ): string {
  409   const newBadge = isNew ? ' (new)' : '';
  410   const name = vulnerableModule ? ` in ${chalk.bold(vulnerableModule)}` : '';
  411   let legalLicenseInstructionsText;
  412   if (legalInstructions) {
  413     legalLicenseInstructionsText = formatLegalInstructions(legalInstructions);
  414   }
  415 
  416   let introducedBy = '';
  417 
  418   if (
  419     testOptions.showVulnPaths === 'some' &&
  420     paths &&
  421     paths.find((p) => p.length > 1)
  422   ) {
  423     // In this mode, we show only one path by default, for compactness
  424     const pathStr = printPath(paths[0]);
  425     introducedBy =
  426       paths.length === 1
  427         ? `\n    introduced by ${pathStr}`
  428         : `\n    introduced by ${pathStr} and ${chalk.cyanBright(
  429             '' + (paths.length - 1),
  430           )} other path(s)`;
  431   } else if (testOptions.showVulnPaths === 'all' && paths) {
  432     introducedBy =
  433       '\n    introduced by:' +
  434       paths
  435         .slice(0, 1000)
  436         .map((p) => '\n    ' + printPath(p))
  437         .join('');
  438     if (paths.length > 1000) {
  439       introducedBy += `\n    and ${chalk.cyanBright(
  440         '' + (paths.length - 1),
  441       )} other path(s)`;
  442     }
  443   }
  444 
  445   let originalSeverityStr = '';
  446   if (originalSeverity && originalSeverity !== severity) {
  447     originalSeverityStr = ` (originally ${titleCaseText(originalSeverity)})`;
  448   }
  449 
  450   return (
  451     colorTextBySeverity(
  452       severity,
  453       `  ${icon.ISSUE} ${chalk.bold(title)}${newBadge} [${titleCaseText(
  454         severity,
  455       )} Severity${originalSeverityStr}]`,
  456     ) +
  457     `[${getVulnerabilityUrl(id)}]` +
  458     name +
  459     introducedBy +
  460     (legalLicenseInstructionsText
  461       ? `${chalk.bold(
  462           '\n    Legal instructions',
  463         )}:\n    ${legalLicenseInstructionsText}`
  464       : '') +
  465     (note ? `${chalk.bold('\n    Note')}:\n    ${note}` : '')
  466   );
  467 }
  468 
  469 function titleCaseText(text) {
  470   return text[0].toUpperCase() + text.slice(1);
  471 }