MitocGroup/recink

View on GitHub
components/snyk/src/reporter/text-reporter.js

Summary

Maintainability
C
1 day
Test Coverage
'use strict';

const AbstractReporter = require('./abstract-reporter');
const snykConfig = require('snyk/lib/config');

/**
 * Text Snyk.io reporter
 */
class TextReporter extends AbstractReporter {
  /**
   * @param {*} args
   */
  constructor(...args) {
    super(...args);
    
    this._colors = true;
    this._stream = process.stdout;
  }
  
  /**
   * @param {stream.Writable} stream
   *
   * @returns {TextReporter}
   */
  writeStream(stream) {
    this._stream = stream;
    
    return this;
  }
  
  /**
   * @returns {stream.Writable}
   */
  get stream() {
    return this._stream;
  }
  
  /**
   * @returns {TextReporter}
   */
  disableColors() {
    this._colors = false;
    
    return this;
  }
  
  /**
   * @returns {TextReporter}
   */
  enableColors() {
    this._colors = true;
    
    return this;
  }
  
  /**
   * @returns {boolean}
   */
  get colors() {
    return this._colors;
  }
  
  /**
   * @returns {string}
   */
  get name() {
    return 'text';
  }
  
  /**
   * @param {*} result
   * @param {*} options
   * @param {boolean} _info
   *
   * @returns {Promise}
   */
  report(result, options, _info = true) {
    const blueUnderline = this.colors ? this.logger.chalk.blue.underline : (t => t);
    const gray = this.colors ? this.logger.chalk.gray : (t => t);
    const grayBold = this.colors ? this.logger.chalk.gray.bold : (t => t);
    const red = this.colors ? this.logger.chalk.red : (t => t);
    const redBold = this.colors ? this.logger.chalk.red.bold : (t => t);
    const yellowBold = this.colors ? this.logger.chalk.yellow.bold : (t => t);
    const greenBold = this.colors ? this.logger.chalk.green.bold : (t => t);
    const npmModule = this.npmModule;
    const emitModule = this.emitModule;
    const moduleInfo = gray(`${ grayBold(emitModule.name) }:${ npmModule.packageFileRelative }`);
    const pm = result.packageManager;
    const deps = result.dependencyCount || 0;
    const badge = (result.ok && result.vulnerabilities.length <= 0)
      ? this.logger.emoji.check 
      : this.logger.emoji.cross;
    
    if (_info) {
      this.logger.info(
        `${ badge } Snyk.io processed ${ deps } ${ pm } dependencies - ${ moduleInfo }`
      );
    }
    
    const showVulnPaths = options['show-vulnerable-paths'] === 'true';
    const reportedVulns = {};
    
    const vulnerabilities = result.vulnerabilities.map(vuln => {
      if (showVulnPaths && reportedVulns[vuln.id]) {
        return;
      }
      
      reportedVulns[vuln.id] = true;

      let res = '';
      let badge = '';
      const name = `${ vuln.name }@${ vuln.version }`;
      const severity = vuln.severity[0].toUpperCase() + vuln.severity.slice(1);
      const issue = vuln.type === 'license' ? 'issue' : 'vulnerability';
      
      switch(vuln.severity) {
        case 'high':
          badge = this.logger.emoji.moon_empty;
          break;
        case 'medium':
          badge = this.logger.emoji.moon_half;
          break;
        case 'low':
          badge = this.logger.emoji.moon_full;
          break;
      }
      
      res += badge;
      res += red(` ${ severity } severity ${ issue } found on `);
      res += grayBold(`${ name }\n`);
      res += `   ${ gray('description:') } ${ vuln.title }\n`;
      res += `   ${ gray('info:') } `;
      res += blueUnderline(`${ snykConfig.ROOT }/vuln/${ vuln.id }\n`);
      
      if (showVulnPaths) {
        res += `   ${ gray('package:') } ${ vuln.from.join(' > ') }\n`;
      }

      if (vuln.note) {
        res += `   ${ gray('note:') } ${ vuln.note }\n`;
      }

      // none of the output past this point is relevant if we're not displaying
      // vulnerable paths
      if (!showVulnPaths) {
        return res.trim();
      }

      const upgradeSteps = (vuln.upgradePath || []).filter(Boolean);

      // Remediation instructions (if we have one)
      if (upgradeSteps.length) {

        // Create upgrade text
        let upgradeText = upgradeSteps.shift();
        
        upgradeText += upgradeSteps.length 
          ? ` (triggers upgrades to ${ upgradeSteps.join(' > ') })` 
          : '';

        let fix = '';
        
        for (let idx = 0; idx < vuln.upgradePath.length; idx++) {
          const elem = vuln.upgradePath[idx];

          if (elem) {
            
            // Check if we're suggesting to upgrade to ourselves.
            if (vuln.from.length > idx && vuln.from[idx] === elem) {
              
              // This ver should get the not-vuln dependency, suggest refresh
              fix += `Your dependencies are out of date, otherwise you would` +
                ` be using a newer ${ vuln.name } than ${ name }.\n`;
              break;
            }
            if (idx === 0) {
              
              // This is an outdated version of yourself
              fix += `You've tested an outdated version of the project. Should be upgraded to ${ upgradeText }`;
            } else if (idx === 1) {
              
              // A direct dependency needs upgrade. Nothing to add.
              fix += `Upgrade direct dependency ${ vuln.from[idx] } to ${ upgradeText }`;
            } else {
              
              // A deep dependency needs to be upgraded
              res += `   ${ gray('actionable:') } `;
              res += yellowBold('No direct dependency upgrade can address this issue.\n');
            }
            break;
          }
        }
        
        if (fix.length > 0) {
          res += `   ${ gray('actionable:') } ${ greenBold(fix) }`;
        }
      } else {
        if (vuln.type === 'license') {
          
          // do not display fix (there isn't any), remove newline
          res = res.slice(0, -1);
        } else if (pm === 'npm') {
          res += `   ${ gray('actionable:') } `;
          res += redBold(
            'No fix available. Consider removing this dependency.'
          );
        }
      }
      
      return res;
    }).filter(Boolean).join(TextReporter.ISSUES_DELIMITER);
    
    this.stream.write(`${ vulnerabilities }\n\n`);
    
    return Promise.resolve();
  }
  
  /**
   * @returns {string}
   */
  static get ISSUES_DELIMITER() {
    return '\n\n';
  }
}

module.exports = TextReporter;