import * as glob from 'glob' ; import { AllowlistEntry , ExemptionReason } from './third_party/tsetse/allowlist' ; import * as minimatch from 'minimatch' ; import * as os from 'os' ; import * as path from 'path' ; import * as ts from 'typescript' ; /**
 * Stores exemption list configurations by rules. Supports commonly used Map
 * operations.
 */ export class ExemptionList { private readonly map : Map < string , AllowlistEntry > ; constructor ( copyFrom ? ) { this . map = new Map ( copyFrom ?. map . entries ( ) ?? [ ] ) ; } get ( rule ) { return this . map . get ( rule ) ; } set ( rule , allowlistEntry ) { this . map . set ( rule , allowlistEntry ) ; } entries ( ) { return this . map . entries ( ) ; } get size ( ) { return this . map . size ; } } /** Get the path of the exemption configuration file from compiler options. */ export function resolveExemptionConfigPath ( configFilePath ) { if ( ! ts . sys . fileExists ( configFilePath ) ) { configFilePath += ts . Extension . Json ; if ( ! ts . sys . fileExists ( configFilePath ) ) { return undefined ; } } const { config } = ts . readConfigFile ( configFilePath , ts . sys . readFile ) ; const options = config ?. compilerOptions ; const configFileDir = path . dirname ( configFilePath ) ; if ( Array . isArray ( options ?. plugins ) ) { for ( const plugin of options . plugins as ts . PluginImport [ ] ) { if ( plugin . name !== 'tsec' ) continue ; const { exemptionConfig } = plugin as { exemptionConfig ? : unknown ; } ; if ( typeof exemptionConfig === 'string' ) { const resolvedPath = path . resolve ( configFileDir , exemptionConfig ) ; if ( ts . sys . fileExists ( resolvedPath ) ) { return resolvedPath ; } } } } if ( typeof config . extends === 'string' ) { return resolveExemptionConfigPath ( path . resolve ( configFileDir , config . extends ) ) ; } return undefined ; } /** Create a Diagnostic for a JSON node from a configuration file */ function getDiagnosticErrorFromJsonNode ( node , file , messageText ) { const start = node . getStart ( file ) ; const length = node . getEnd ( ) - start ; return { source : 'tsec' , category : ts . DiagnosticCategory . Error , code : 21110 , file , start , length , messageText , } ; } /** Parse the content of the exemption configuration file. */ export function parseExemptionConfig ( exemptionConfigPath ) { const errors = [ ] ; const jsonContent = ts . sys . readFile ( exemptionConfigPath ) ! ; const jsonSourceFile = ts . parseJsonText ( exemptionConfigPath , jsonContent ) ; if ( ! jsonSourceFile . statements . length ) { errors . push ( { source : 'tsec' , category : ts . DiagnosticCategory . Error , code : 21110 , file : jsonSourceFile , start : 1 , length : undefined , messageText : 'Invalid exemtpion list' , } ) ; return errors ; } const jsonObj = jsonSourceFile . statements [ 0 ] . expression ; if ( ! ts . isObjectLiteralExpression ( jsonObj ) ) { errors . push ( getDiagnosticErrorFromJsonNode ( jsonObj , jsonSourceFile , 'Exemption configuration requires a value of type object' ) ) ; return errors ; } const exemption = new ExemptionList ( ) ; const baseDir = path . dirname ( exemptionConfigPath ) ; const globOptions = { cwd : baseDir , absolute : true , silent : true } ; const isWin = os . platform ( ) === 'win32' ; for ( const prop of jsonObj . properties ) { if ( ! ts . isPropertyAssignment ( prop ) ) { errors . push ( getDiagnosticErrorFromJsonNode ( prop , jsonSourceFile , 'Property assignment expected' ) ) ; continue ; } if ( prop . name === undefined ) continue ; if ( ! ts . isStringLiteral ( prop . name ) || ! prop . name . getText ( jsonSourceFile ) . startsWith ( `"` ) ) { errors . push ( getDiagnosticErrorFromJsonNode ( prop . name , jsonSourceFile , 'String literal with double quotes expected' ) ) ; continue ; } const ruleName = prop . name . text ; if ( ! ts . isArrayLiteralExpression ( prop . initializer ) ) { errors . push ( getDiagnosticErrorFromJsonNode ( prop . initializer , jsonSourceFile , `Exemption entry '${ ruleName }' requires a value of type Array` ) ) ; continue ; } const fileNames = [ ] ; const patterns = [ ] ; for ( const elem of prop . initializer . elements ) { if ( ! ts . isStringLiteral ( elem ) ) { errors . push ( getDiagnosticErrorFromJsonNode ( elem , jsonSourceFile , `Item of exemption entry '${ ruleName }' requires values of type string` ) ) ; continue ; } let pathLike = path . resolve ( baseDir , elem . text ) ; if ( isWin ) { pathLike = pathLike . replace ( /\\/g , '/' ) ; } if ( glob . hasMagic ( elem . text , globOptions ) ) { patterns . push ( minimatch . makeRe ( pathLike , { } ) . toString ( ) . slice ( 1 , - 1 ) ) ; } else { fileNames . push ( pathLike ) ; } } exemption . set ( ruleName , { reason : ExemptionReason . UNSPECIFIED , path : fileNames , regexp : patterns , } ) ; } return errors . length > 0 ? errors : exemption ; }