Skip to content

Commit eac27c1

Browse files
committed
fix(ast): lazy TS import; chore: typescript as runtime dep; release: 1.0.1
1 parent 00e146c commit eac27c1

File tree

3 files changed

+43
-22
lines changed

3 files changed

+43
-22
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
## 1.0.0 — 2025-08-23
2+
-
3+
## 1.0.1 — 2025-08-23
4+
5+
### Fixed
6+
- Avoid runtime crash when installed globally by lazily requiring TypeScript in AST scanner. If `typescript` is not present, AST-based checks are skipped gracefully instead of failing.
7+
- Move `typescript` to runtime dependencies to support global installs.
28

39
Initial stable release.
410

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ubon",
3-
"version": "1.0.0",
3+
"version": "1.0.1",
44
"description": "Security scanner for AI-generated React/Next.js and Python apps. Catches hardcoded secrets, accessibility issues, and vulnerabilities that traditional linters miss.",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
@@ -67,7 +67,8 @@
6767
"dependencies": {
6868
"chalk": "^4.1.2",
6969
"commander": "^11.1.0",
70-
"glob": "^10.3.10"
70+
"glob": "^10.3.10",
71+
"typescript": "^5.3.3"
7172
},
7273
"optionalDependencies": {
7374
"puppeteer": "^23.6.0"
@@ -79,8 +80,7 @@
7980
"eslint": "^8.56.0",
8081
"jest": "^29.7.0",
8182
"@types/jest": "^29.5.11",
82-
"ts-jest": "^29.1.1",
83-
"typescript": "^5.3.3"
83+
"ts-jest": "^29.1.1"
8484
},
8585
"engines": {
8686
"node": ">=16.0.0"
@@ -100,4 +100,4 @@
100100
"description": "npm downloads"
101101
}
102102
]
103-
}
103+
}

src/scanners/ast-security-scanner.ts

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
import { glob } from 'glob';
22
import { readFileSync } from 'fs';
3-
import ts from 'typescript';
3+
import type ts from 'typescript';
44
import { Scanner, ScanOptions, ScanResult } from '../types';
55
import { RULES } from '../types/rules';
66

77
export 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

Comments
 (0)