import { ENABLED_RULES } from './rule_groups' ; import { Checker } from './third_party/tsetse/checker' ; import * as ts from 'typescript' ; import { ExemptionList , parseExemptionConfig , resolveExemptionConfigPath } from './exemption_config' ; import { createDiagnosticsReporter } from './report' ; import { createProxy } from './utils' ; /** Check if tsec is invoked in the build mode. */ export function isInBuildMode ( cmdArgs ) { if ( cmdArgs . length && cmdArgs [ 0 ] . charAt ( 0 ) === '-' ) { const optionStart = cmdArgs [ 0 ] . charAt ( 1 ) === '-' ? 2 : 1 ; const firstOption = cmdArgs [ 0 ] . slice ( optionStart ) ; return firstOption === 'build' || firstOption === 'b' ; } return false ; } /**
 * Create a new cheker with all enabled rules registered and the exemption list
 * configured.
 */ export function getConfiguredChecker ( program ) { let exemptionList = undefined ; const exemptionConfigPath = resolveExemptionConfigPath ( program . getCompilerOptions ( ) [ 'configFilePath' ] as string ) ; const errors = [ ] ; if ( exemptionConfigPath ) { const projExemptionConfigOrErr = parseExemptionConfig ( exemptionConfigPath ) ; if ( projExemptionConfigOrErr instanceof ExemptionList ) { exemptionList = projExemptionConfigOrErr ; } else { errors . push ( ... projExemptionConfigOrErr ) ; } } const checker = new Checker ( program ) ; const wildcardAllowListEntry = exemptionList ?. get ( '*' ) ; const rules = ENABLED_RULES . map ( ruleCtr => { const allowlistEntries = [ ] ; const allowlistEntry = exemptionList ?. get ( ruleCtr . RULE_NAME ) ; if ( allowlistEntry ) { allowlistEntries . push ( allowlistEntry ) ; } if ( wildcardAllowListEntry ) { allowlistEntries . push ( wildcardAllowListEntry ) ; } return new ruleCtr ( { allowlistEntries } ) ; } ) ; for ( const rule of rules ) { rule . register ( checker ) ; } return { checker , errors } ; } /** Perform security checks on a single project. */ export function performCheck ( program ) { const { checker , errors } = getConfiguredChecker ( program ) ; for ( const sf of program . getSourceFiles ( ) ) { if ( sf . isDeclarationFile ) continue ; const tsecErrors = checker . execute ( sf ) . map ( failure => failure . toDiagnosticWithStringifiedFixes ( ) ) ; errors . push ( ... tsecErrors ) ; } return errors ; } const ALL_TSEC_RULE_NAMES = new Set < string | undefined > ( ENABLED_RULES . map ( r => r . RULE_NAME ) ) ; /** Perform checks on a monorepo. */ export function performBuild ( args ) { interface ParsedBuildCommand { buildOptions : ts . BuildOptions ; watchOptions : ts . WatchOptions | undefined ; projects : string [ ] ; errors : ts . Diagnostic [ ] ; } const parseBuildCommand = ( ts as unknown as { parseBuildCommand : ( args : readonly string [ ] ) => ParsedBuildCommand ; } ) . parseBuildCommand ; const { buildOptions , projects , errors } = parseBuildCommand ( args ) ; const reportDiagnostics = createDiagnosticsReporter ( buildOptions ) ; if ( errors . length !== 0 ) { return reportDiagnostics ( errors , true ) ; } if ( projects . length === 0 ) projects . push ( '.' ) ; ( ts as { version : string ; } ) . version += `-tsec-${ require ( '../package.json' ) . version }` ; const allTsecErrors = [ ] ; const builderErrors = [ ] ; const builderHost = ts . createSolutionBuilderHost ( ts . sys , instrumentedCreateProgram , diag => { if ( ! ALL_TSEC_RULE_NAMES . has ( diag . source ) ) { builderErrors . push ( diag ) ; } } , undefined , ( ) => { } ) ; const builder = ts . createSolutionBuilder ( builderHost , projects , buildOptions ) ; buildOptions [ 'clean' ] ? builder . clean ( ) : builder . build ( ) ; const errorCount = reportDiagnostics ( [ ... builderErrors , ... allTsecErrors ] , true ) ; return errorCount ; /**
     * The callback fed to solution builder for creating the program for each.
     * project. tsec performs security checks inside this callback, when the
     * the program is created. The API for JS code emission is instrumented so
     * that tsec errors are respected during code emission.
     */ function instrumentedCreateProgram ( rootNames , options , host ? , oldProgram ? , configFileParsingDiagnostics ? , projectReferences ? ) { const builderProgram = ts . createEmitAndSemanticDiagnosticsBuilderProgram ( rootNames , options , host , oldProgram , configFileParsingDiagnostics , projectReferences ) ; const program = builderProgram . getProgram ( ) ; const tsecErrorsInThisProgram = performCheck ( program ) ; allTsecErrors . push ( ... tsecErrorsInThisProgram ) ; const tsecErrorsByFile = new Map < string , ts . Diagnostic [ ] > ( ) ; for ( const error of tsecErrorsInThisProgram ) { const fileName = error . file ?. fileName ; if ( fileName !== undefined ) { let errors = tsecErrorsByFile . get ( fileName ) ; if ( errors === undefined ) { errors = [ ] ; tsecErrorsByFile . set ( fileName , errors ) ; } errors . push ( error ) ; } } return { ... createProxy ( builderProgram ) , emit : instrumentedEmit , } ; /**
         * Instrumented emit function. JS code won't be emitted for a file if there
         * is any tsec error in it.
         */ function instrumentedEmit ( targetSourceFile ? , writeFile ? , cancellationToken ? , emitOnlyDtsFiles ? , customTransformers ? ) { if ( targetSourceFile === undefined ) { if ( tsecErrorsInThisProgram . length !== 0 ) { return { emitSkipped : true , diagnostics : tsecErrorsInThisProgram } ; } } else { const tsecErrorsInThisFile = tsecErrorsByFile . get ( targetSourceFile . fileName ) ; if ( tsecErrorsInThisFile ?. length ) { return { emitSkipped : true , diagnostics : tsecErrorsInThisFile } ; } } return builderProgram . emit ( targetSourceFile , writeFile , cancellationToken , emitOnlyDtsFiles , customTransformers ) ; } } }