11import { glob } from 'glob' ;
22import { readFileSync } from 'fs' ;
3- import ts from 'typescript' ;
3+ import type ts from 'typescript' ;
44import { Scanner , ScanOptions , ScanResult } from '../types' ;
55import { RULES } from '../types/rules' ;
66
77export class AstSecurityScanner implements Scanner {
88 name = 'AST Security Scanner' ;
99
1010 async scan ( options : ScanOptions ) : Promise < ScanResult [ ] > {
11+ // Lazily require TypeScript at runtime; if unavailable (global install without dev deps), skip AST checks gracefully
12+ let tsReal : typeof import ( 'typescript' ) ;
13+ try {
14+ // eslint-disable-next-line @typescript-eslint/no-var-requires
15+ tsReal = require ( 'typescript' ) as typeof import ( 'typescript' ) ;
16+ } catch {
17+ return [ ] ;
18+ }
19+
1120 const results : ScanResult [ ] = [ ] ;
1221 const files = await glob ( '**/*.{js,jsx,ts,tsx}' , {
1322 cwd : options . directory ,
@@ -17,7 +26,13 @@ export class AstSecurityScanner implements Scanner {
1726 for ( const file of files ) {
1827 let sourceText = '' ;
1928 try { sourceText = readFileSync ( `${ options . directory } /${ file } ` , 'utf-8' ) ; } catch { continue ; }
20- const sf = ts . createSourceFile ( file , sourceText , ts . ScriptTarget . ES2020 , true , file . endsWith ( '.tsx' ) ? ts . ScriptKind . TSX : file . endsWith ( '.ts' ) ? ts . ScriptKind . TS : ts . ScriptKind . JS ) ;
29+ const sf = tsReal . createSourceFile (
30+ file ,
31+ sourceText ,
32+ tsReal . ScriptTarget . ES2020 ,
33+ true ,
34+ file . endsWith ( '.tsx' ) ? tsReal . ScriptKind . TSX : file . endsWith ( '.ts' ) ? tsReal . ScriptKind . TS : tsReal . ScriptKind . JS
35+ ) ;
2136
2237 const add = ( metaKey : keyof typeof RULES , node : ts . Node , message ?: string ) => {
2338 const meta = RULES [ metaKey as string ] ;
@@ -42,55 +57,55 @@ export class AstSecurityScanner implements Scanner {
4257
4358 const visit = ( node : ts . Node ) => {
4459 // eval(...)
45- if ( ts . isCallExpression ( node ) ) {
60+ if ( tsReal . isCallExpression ( node ) ) {
4661 const expr = node . expression ;
47- if ( ts . isIdentifier ( expr ) && expr . text === 'eval' ) {
62+ if ( tsReal . isIdentifier ( expr ) && expr . text === 'eval' ) {
4863 add ( 'SEC016' , node ) ;
4964 }
5065 // React.createElement(userInput)
51- if ( ts . isPropertyAccessExpression ( expr ) ) {
52- if ( ts . isIdentifier ( expr . expression ) && expr . expression . text === 'React' && expr . name . text === 'createElement' ) {
66+ if ( tsReal . isPropertyAccessExpression ( expr ) ) {
67+ if ( tsReal . isIdentifier ( expr . expression ) && expr . expression . text === 'React' && expr . name . text === 'createElement' ) {
5368 const first = node . arguments [ 0 ] ;
54- if ( first && ! ts . isStringLiteralLike ( first ) ) {
69+ if ( first && ! tsReal . isStringLiteralLike ( first ) ) {
5570 add ( 'SEC019' , node ) ;
5671 }
5772 }
5873 }
5974 // dynamic import(userControlled)
60- if ( ts . isCallExpression ( node ) && node . expression . kind === ts . SyntaxKind . ImportKeyword ) {
75+ if ( tsReal . isCallExpression ( node ) && node . expression . kind === tsReal . SyntaxKind . ImportKeyword ) {
6176 const arg = node . arguments [ 0 ] ;
62- if ( arg && ! ts . isStringLiteralLike ( arg ) ) {
77+ if ( arg && ! tsReal . isStringLiteralLike ( arg ) ) {
6378 add ( 'NEXT004' , node ) ;
6479 }
6580 }
6681 // fetch(...)
67- if ( ts . isIdentifier ( expr ) && expr . text === 'fetch' ) {
82+ if ( tsReal . isIdentifier ( expr ) && expr . text === 'fetch' ) {
6883 const second = node . arguments [ 1 ] ;
6984 let hasSignal = false ;
70- if ( second && ts . isObjectLiteralExpression ( second ) ) {
71- hasSignal = second . properties . some ( p => ts . isPropertyAssignment ( p ) && ts . isIdentifier ( p . name ) && p . name . text === 'signal' ) ;
85+ if ( second && tsReal . isObjectLiteralExpression ( second ) ) {
86+ hasSignal = second . properties . some ( p => tsReal . isPropertyAssignment ( p ) && tsReal . isIdentifier ( p . name ) && p . name . text === 'signal' ) ;
7287 }
7388 if ( ! hasSignal ) add ( 'JSNET001' , node , RULES . JSNET001 . message ) ;
7489 }
7590 }
7691
7792 // dangerouslySetInnerHTML
78- if ( ts . isIdentifier ( node ) && node . text === 'dangerouslySetInnerHTML' ) {
93+ if ( tsReal . isIdentifier ( node ) && node . text === 'dangerouslySetInnerHTML' ) {
7994 add ( 'SEC017' , node ) ;
8095 }
8196
8297 // process.env.X || 'fallback'
83- if ( ts . isBinaryExpression ( node ) && node . operatorToken . kind === ts . SyntaxKind . BarBarToken ) {
98+ if ( tsReal . isBinaryExpression ( node ) && node . operatorToken . kind === tsReal . SyntaxKind . BarBarToken ) {
8499 const left = node . left ;
85100 const right = node . right ;
86- const isEnv = ts . isPropertyAccessExpression ( left ) && ts . isPropertyAccessExpression ( left . expression ) && ts . isIdentifier ( left . expression . expression ) && left . expression . expression . text === 'process' && ts . isIdentifier ( left . expression . name ) && left . expression . name . text === 'env' ;
87- const isLiteral = ts . isStringLiteralLike ( right ) ;
101+ const isEnv = tsReal . isPropertyAccessExpression ( left ) && tsReal . isPropertyAccessExpression ( left . expression ) && tsReal . isIdentifier ( left . expression . expression ) && left . expression . expression . text === 'process' && tsReal . isIdentifier ( left . expression . name ) && left . expression . name . text === 'env' ;
102+ const isLiteral = tsReal . isStringLiteralLike ( right ) ;
88103 if ( isEnv && isLiteral ) {
89104 add ( 'SEC008' , node ) ;
90105 }
91106 }
92107
93- ts . forEachChild ( node , visit ) ;
108+ tsReal . forEachChild ( node , visit ) ;
94109 } ;
95110
96111 visit ( sf ) ;
0 commit comments