fileoverview import * as fs from 'fs' ; import { promisify } from 'util' ; import groupBy = require ( 'lodash/groupBy' ) ; import * as semver from 'semver' ; import { debug as d } from '@hint/utils-debug' ; import { readFileAsync , writeFileAsync } from '@hint/utils-fs' ; import * as logger from '@hint/utils/dist/src/logging' ; import { requestAsync } from '@hint/utils-network' ; import { IHint , CanEvaluateScript } from 'hint/dist/src/lib/types' ; import { HintContext } from 'hint/dist/src/lib/hint-context' ; import { Severity } from '@hint/utils-types' ; import { Library , Vulnerability } from './types' ; import meta from './meta' ; import { getMessage } from './i18n.import' ; const debug = d ( __filename ) ; type SnykSeverity = 'low' | 'medium' | 'high' ; export default class NoVulnerableJavascriptLibrariesHint implements IHint { public static readonly meta = meta ; public constructor ( context ) { let minimumSeverity = 'low' ; /**
         * Creates the script that will be injected in the connector to detect what libraries are run
         * using [`js-library-detector`](https://npmjs.com/package/js-library-detector).
         */ const createScript = async ( ) : Promise < string > => { debug ( 'Creating script to inject' ) ; const libraryDetector = await readFileAsync ( require . resolve ( 'js-library-detector' ) ) ; const script = `/*RunInPageContext*/
            (function (){
                ${ libraryDetector };

                const libraries = Object.entries(d41d8cd98f00b204e9800998ecf8427e_LibraryDetectorTests);
                const detectedLibraries = libraries.reduce((detected, [name, lib]) => {
                    try {
                        const result = lib.test(window);
                        if (result) {
                            detected.push({
                                name,
                                version: result.version,
                                npmPkgName: lib.npm
                            });
                        }
                    }
                    catch (e) {}

                    return detected;
                }, []);

                return detectedLibraries;
            }())` ; return script ; } ; /** Loads the local copy of the snyk database and updates it if the file is older than 24h. */ const loadSnyk = async ( ) => { const oneDay = 3600000 * 24 ; const now = Date . now ( ) ; if ( process . env . webpack ) { return require ( './snyk-snapshot.json' ) ; } try { const snykDBPath = require . resolve ( './snyk-snapshot.json' ) ; const snykStat = await promisify ( fs . stat ) ( snykDBPath ) ; const modified = new Date ( snykStat . mtime ) . getTime ( ) ; if ( now - modified > oneDay ) { debug ( 'snkyDB is older than 24h.' ) ; debug ( 'Updating snykDB' ) ; const res = await requestAsync ( 'https://snyk.io/partners/api/v2/vulndb/clientside.json' ) ; await writeFileAsync ( snykDBPath , res ) ; } } catch ( e ) { debug ( e ) ; debug ( `Error loading snyk's data` ) ; } return require ( './snyk-snapshot.json' ) ; } ; const toSeverity = ( severity ) => { switch ( severity ) { case 'high' : return Severity . error ; case 'medium' : return Severity . warning ; default : return Severity . hint ; } } ; /** If a used library has vulnerability that meets the minimum threshold, it gets reported.  */ const reportLibrary = ( library , vulns , resource ) => { let vulnerabilities = vulns ; debug ( 'Filtering vulnerabilities' ) ; let maxSeverity = Severity . off ; vulnerabilities = vulnerabilities . filter ( ( vulnerability ) => { const { severity } = vulnerability ; let fails = false ; maxSeverity = Math . max ( maxSeverity , toSeverity ( severity as SnykSeverity ) ) ; switch ( minimumSeverity ) { case 'medium' : fails = severity === 'medium' || severity === 'high' ; break ; case 'high' : fails = severity === 'high' ; break ; default : fails = true ; break ; } return fails ; } ) ; if ( vulnerabilities . length === 0 ) { return ; } const vulnerabilitiesBySeverity = groupBy ( vulnerabilities , 'severity' ) ; const detail = Object . entries ( vulnerabilitiesBySeverity ) . map ( ( [ severity , entries ] ) => { return `${ ( entries as any [ ] ) . length } ${ severity }` ; } ) . join ( ', ' ) ; if ( detail ) { let message ; if ( vulnerabilities . length === 1 ) { message = getMessage ( 'vulnerability' , context . language , [ `${ library . name }@${ library . version }` , vulnerabilities . length . toString ( ) , detail ] ) ; } else { message = getMessage ( 'vulnerabilities' , context . language , [ `${ library . name }@${ library . version }` , vulnerabilities . length . toString ( ) , detail ] ) ; } const documentation = vulnerabilities . map ( ( vuln ) => { return { link : `https://snyk.io/vuln/${ vuln . id }` , text : getMessage ( 'learnMore' , context . language , [ vuln . id , vuln . severity ] ) } ; } ) ; context . report ( resource , message , { documentation , severity : maxSeverity } ) ; } } ; /** Removes any tags from a version. E.g.: 2.0.0rc --> 2.0.0, 2.0.1-pre --> 2.0.1 */ const removeTagsFromVersion = ( version ) : string | null => { const match = ( /(\d+\.?)+/ ) . exec ( version ) ; return match && match [ 0 ] ; } ; /** Given a list of libraries, reports the ones that have known vulnerabilities. */ const detectAndReportVulnerableLibraries = async ( libraries , resource ) => { const snykDB = await loadSnyk ( ) ; for ( const lib of libraries ) { const snykInfo = snykDB . npm [ lib . npmPkgName ] as Vulnerability [ ] ; if ( ! snykInfo ) { continue ; } const vulnerabilities = snykInfo . reduce ( ( vulns , vuln ) => { const version = removeTagsFromVersion ( lib . version ) || '' ; try { vuln . semver . vulnerable . forEach ( ( vulnVersion ) => { if ( semver . satisfies ( version , vulnVersion ) ) { vulns . push ( vuln ) ; } } ) ; } catch ( e ) { logger . error ( getMessage ( 'versionNotCompliant' , context . language , [ version , lib . name ] ) ) ; } return vulns ; } , [ ] as Vulnerability [ ] ) ; reportLibrary ( lib , vulnerabilities , resource ) ; } } ; /** Checks if the JS libraries used by a website have known vulnerabilities. */ const validateLibraries = async ( canEvaluateScript ) => { const script = await createScript ( ) ; const resource = canEvaluateScript . resource ; let detectedLibraries ; try { detectedLibraries = ( await context . evaluate ( script ) ) as Library [ ] ; } catch ( e ) { let message ; if ( e . message . includes ( 'evaluation exceeded' ) ) { message = getMessage ( 'notFastEnough' , context . language ) ; } else { message = getMessage ( 'errorExecuting' , context . language , e . message ) ; } message = getMessage ( 'tryAgainLater' , context . language , message ) ; context . report ( resource , message , { severity : Severity . warning } ) ; debug ( 'Error executing script' , e ) ; return ; } if ( detectedLibraries . length === 0 ) { return ; } await detectAndReportVulnerableLibraries ( detectedLibraries , canEvaluateScript . resource ) ; return ; } ; minimumSeverity = ( context . hintOptions && context . hintOptions . severity ) || 'low' ; context . on ( 'can-evaluate::script' , validateLibraries ) ; } }