@kitajs/ts-html-plugin 4.1.4 → 5.0.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -17
- package/bin/index.js +8 -0
- package/dist/cli.d.ts +1 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +9 -8
- package/dist/cli.js.map +1 -1
- package/dist/errors.d.ts +4 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +15 -18
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/util.d.ts +2 -4
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +101 -38
- package/dist/util.js.map +1 -1
- package/package.json +22 -14
- package/src/cli.ts +229 -0
- package/src/errors.ts +34 -0
- package/src/index.ts +43 -0
- package/src/util.ts +438 -0
- package/dist/tsconfig.build.tsbuildinfo +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kitajs/ts-html-plugin",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0-next.0",
|
|
4
4
|
"homepage": "https://github.com/kitajs/html/tree/master/packages/ts-html-plugin#readme",
|
|
5
5
|
"bugs": "https://github.com/kitajs/html/issues",
|
|
6
6
|
"repository": {
|
|
@@ -16,30 +16,38 @@
|
|
|
16
16
|
"main": "dist/index.js",
|
|
17
17
|
"types": "dist/index.d.ts",
|
|
18
18
|
"bin": {
|
|
19
|
-
"ts-html-plugin": "
|
|
20
|
-
"xss-scan": "
|
|
19
|
+
"ts-html-plugin": "bin/index.js",
|
|
20
|
+
"xss-scan": "bin/index.js"
|
|
21
21
|
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"src",
|
|
25
|
+
"!dist/*.tsbuildinfo"
|
|
26
|
+
],
|
|
22
27
|
"dependencies": {
|
|
23
28
|
"chalk": "^5.6.2",
|
|
24
29
|
"tslib": "^2.8.1",
|
|
25
30
|
"yargs": "^18.0.0"
|
|
26
31
|
},
|
|
27
32
|
"devDependencies": {
|
|
28
|
-
"@
|
|
29
|
-
"@
|
|
30
|
-
"@
|
|
31
|
-
"@
|
|
33
|
+
"@types/node": "^24.10.13",
|
|
34
|
+
"@types/yargs": "^17.0.35",
|
|
35
|
+
"@typescript/native-preview": "^7.0.0-dev.20260122.2",
|
|
36
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
32
37
|
"fast-defer": "^1.1.9",
|
|
33
|
-
"self": "
|
|
38
|
+
"self-ts-plugin": "link:",
|
|
39
|
+
"typescript": "^5.9.3",
|
|
40
|
+
"vitest": "^4.0.18",
|
|
41
|
+
"@kitajs/html": "^5.0.0-next.0"
|
|
34
42
|
},
|
|
35
43
|
"peerDependencies": {
|
|
36
|
-
"
|
|
37
|
-
"
|
|
44
|
+
"typescript": "^5.9.3",
|
|
45
|
+
"@kitajs/html": "^5.0.0-next.0"
|
|
38
46
|
},
|
|
39
47
|
"scripts": {
|
|
40
|
-
"build": "
|
|
41
|
-
"
|
|
42
|
-
"test": "
|
|
43
|
-
"test-
|
|
48
|
+
"build": "tsgo -p tsconfig.build.json",
|
|
49
|
+
"pretest": "pnpm run build",
|
|
50
|
+
"test": "vitest --coverage --typecheck --run",
|
|
51
|
+
"test-types": "tsgo --noEmit"
|
|
44
52
|
}
|
|
45
53
|
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import ts from 'typescript';
|
|
5
|
+
import yargs from 'yargs';
|
|
6
|
+
import { hideBin } from 'yargs/helpers';
|
|
7
|
+
import { recursiveDiagnoseJsxElements } from './util';
|
|
8
|
+
|
|
9
|
+
const { version } = require('../package.json');
|
|
10
|
+
|
|
11
|
+
const help = `
|
|
12
|
+
|
|
13
|
+
@kitajs/ts-html-plugin v${version} - A CLI tool & TypeScript LSP for finding XSS vulnerabilities in your TypeScript code.
|
|
14
|
+
|
|
15
|
+
Usage: xss-scan [options] <file> <file>...
|
|
16
|
+
ts-html-plugin [options] <file> <file>...
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--cwd <path> The current working directory to use (defaults to process.cwd())
|
|
20
|
+
-p, --project <path> The path to the tsconfig.json file to use (defaults to 'tsconfig.json')
|
|
21
|
+
-s, --simplified Use simplified diagnostics
|
|
22
|
+
-h, --help Show this help message
|
|
23
|
+
--version Show the version number
|
|
24
|
+
<file> <file>... The files to check (defaults to all files in tsconfig.json)
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
$ xss-scan
|
|
28
|
+
$ xss-scan --cwd src
|
|
29
|
+
$ xss-scan --project tsconfig.build.json
|
|
30
|
+
$ xss-scan src/index.tsx src/App.tsx
|
|
31
|
+
|
|
32
|
+
Exit codes:
|
|
33
|
+
0 - No XSS vulnerabilities were found
|
|
34
|
+
1 - XSS vulnerabilities were found
|
|
35
|
+
2 - Only warnings were found
|
|
36
|
+
|
|
37
|
+
`.trim();
|
|
38
|
+
|
|
39
|
+
function readCompilerOptions(tsconfigPath: string) {
|
|
40
|
+
const { config, error } = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
|
|
41
|
+
|
|
42
|
+
if (error) {
|
|
43
|
+
return { errors: [error] };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { options, errors, fileNames } = ts.parseJsonConfigFileContent(
|
|
47
|
+
config,
|
|
48
|
+
ts.sys,
|
|
49
|
+
path.dirname(tsconfigPath),
|
|
50
|
+
undefined,
|
|
51
|
+
tsconfigPath
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (errors.length) {
|
|
55
|
+
return { errors };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { options, fileNames, errors: undefined };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function prettyPrintErrorCount(diagnostics: ts.Diagnostic[], root: string) {
|
|
62
|
+
const files = new Map<string, number>();
|
|
63
|
+
|
|
64
|
+
// Counts the amount of errors per file
|
|
65
|
+
for (const diagnostic of diagnostics) {
|
|
66
|
+
if (!diagnostic.file) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const file = files.get(diagnostic.file.fileName);
|
|
71
|
+
|
|
72
|
+
if (file !== undefined) {
|
|
73
|
+
files.set(diagnostic.file.fileName, file + 1);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
files.set(diagnostic.file.fileName, 1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (files.size > 1) {
|
|
81
|
+
console.error(
|
|
82
|
+
chalk.red(`Found a total of ${diagnostics.length} errors in ${files.size} files\n`)
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const [file, amount] of files.entries()) {
|
|
87
|
+
console.error(
|
|
88
|
+
chalk.red(
|
|
89
|
+
`Found ${amount} error${amount === 1 ? '' : 's'} in ${path.relative(root, file)}`
|
|
90
|
+
)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function fileExists(p: string) {
|
|
96
|
+
try {
|
|
97
|
+
fs.statSync(p);
|
|
98
|
+
return true;
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function main() {
|
|
105
|
+
const args = await yargs(hideBin(process.argv)).help(false).version(version).argv;
|
|
106
|
+
|
|
107
|
+
if (args.help || args.h) {
|
|
108
|
+
console.log(help);
|
|
109
|
+
return process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Detects unknown arguments
|
|
113
|
+
for (const key in args) {
|
|
114
|
+
if (key === '_' || key === '$0') {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
switch (key) {
|
|
119
|
+
case 'cwd':
|
|
120
|
+
case 'project':
|
|
121
|
+
case 'p':
|
|
122
|
+
case 's':
|
|
123
|
+
case 'simplified':
|
|
124
|
+
continue;
|
|
125
|
+
default:
|
|
126
|
+
console.error(`Unknown argument: ${key}. Run --help for more information.`);
|
|
127
|
+
return process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const root = args.cwd ? String(args.cwd) : process.cwd();
|
|
132
|
+
|
|
133
|
+
const tsconfigPath = String(args.project || args.p || 'tsconfig.json');
|
|
134
|
+
|
|
135
|
+
const simplified = !!(args.simplified || args.s);
|
|
136
|
+
|
|
137
|
+
const diagnosticFormatter =
|
|
138
|
+
!process.stdout.isTTY || simplified
|
|
139
|
+
? ts.formatDiagnostics
|
|
140
|
+
: ts.formatDiagnosticsWithColorAndContext;
|
|
141
|
+
|
|
142
|
+
if (!fileExists(tsconfigPath)) {
|
|
143
|
+
console.error((!simplified ? chalk.red : String)(`Could not find ${tsconfigPath}`));
|
|
144
|
+
return process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const tsconfig = readCompilerOptions(tsconfigPath);
|
|
148
|
+
|
|
149
|
+
const diagnosticHost: ts.FormatDiagnosticsHost = {
|
|
150
|
+
getCurrentDirectory: ts.sys.getCurrentDirectory,
|
|
151
|
+
getCanonicalFileName: (fileName) => fileName,
|
|
152
|
+
getNewLine: () => ts.sys.newLine
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
if (tsconfig.errors) {
|
|
156
|
+
console.error(diagnosticFormatter(tsconfig.errors, diagnosticHost));
|
|
157
|
+
return process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let files = tsconfig.fileNames;
|
|
161
|
+
|
|
162
|
+
if (args._.length) {
|
|
163
|
+
// Prefer the files passed as arguments, otherwise use the files in tsconfig.json
|
|
164
|
+
files = [];
|
|
165
|
+
|
|
166
|
+
for (let i = 0; i < args._.length; i++) {
|
|
167
|
+
const file = String(args._[i]);
|
|
168
|
+
|
|
169
|
+
if (!fileExists(file)) {
|
|
170
|
+
console.error(
|
|
171
|
+
(!simplified ? chalk.red : String)(`Could not find provided '${file}' file.`)
|
|
172
|
+
);
|
|
173
|
+
return process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!file.match(/(t|j)sx$/)) {
|
|
177
|
+
console.warn(
|
|
178
|
+
(!simplified ? chalk.yellow : String)(
|
|
179
|
+
`Provided '${file}' file is not a TSX/JSX file.`
|
|
180
|
+
)
|
|
181
|
+
);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
files.push(file);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!files.length) {
|
|
190
|
+
console.error((!simplified ? chalk.red : String)('No files were found to check.'));
|
|
191
|
+
return process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const program = ts.createProgram(files, tsconfig.options);
|
|
195
|
+
const typeChecker = program.getTypeChecker();
|
|
196
|
+
const sources = program.getSourceFiles();
|
|
197
|
+
|
|
198
|
+
const diagnostics: ts.Diagnostic[] = [];
|
|
199
|
+
|
|
200
|
+
for (const source of sources) {
|
|
201
|
+
const filename = source.fileName;
|
|
202
|
+
|
|
203
|
+
// Not a tsx file, so don't do anything
|
|
204
|
+
if (!filename.match(/(t|j)sx$/)) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
ts.forEachChild(source, function loopSourceNodes(node) {
|
|
209
|
+
recursiveDiagnoseJsxElements(ts, node, typeChecker, diagnostics);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (diagnostics.length) {
|
|
214
|
+
const hasError = diagnostics.some(
|
|
215
|
+
(diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
console.error(diagnosticFormatter(diagnostics, diagnosticHost));
|
|
219
|
+
|
|
220
|
+
if (!simplified) {
|
|
221
|
+
prettyPrintErrorCount(diagnostics, root);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
process.exit(hasError ? 1 : 2);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log(chalk.green(`No XSS vulnerabilities found in ${files.length} files!`));
|
|
228
|
+
process.exit(0);
|
|
229
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function createError(code: number, message: string) {
|
|
2
|
+
return {
|
|
3
|
+
code,
|
|
4
|
+
message:
|
|
5
|
+
process.env.INTERNAL_DISABLE_URL_DOCS !== 'true'
|
|
6
|
+
? `${message}\nSee https://html.kitajs.org/TS${code}`
|
|
7
|
+
: message
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 88600 is the base code for all errors in this plugin.
|
|
12
|
+
// KITA in ASCII
|
|
13
|
+
// 75 * 105 * 116 * 97 = 88609500
|
|
14
|
+
// Simplify to 88600
|
|
15
|
+
|
|
16
|
+
export const Xss = createError(
|
|
17
|
+
88601,
|
|
18
|
+
`Content may introduce an XSS vulnerability and must be marked with the \`safe\` attribute.`
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export const DoubleEscape = createError(
|
|
22
|
+
88602,
|
|
23
|
+
`The \`safe\` attribute causes this content to be escaped more than once.`
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export const ComponentXss = createError(
|
|
27
|
+
88603,
|
|
28
|
+
`Content inside a Component must be escaped using escapeHtml().`
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export const UnusedSafe = createError(
|
|
32
|
+
88604,
|
|
33
|
+
`The \`safe\` attribute is unused in this context.`
|
|
34
|
+
);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { default as TS, server } from 'typescript/lib/tsserverlibrary';
|
|
2
|
+
import { proxyObject, recursiveDiagnoseJsxElements } from './util';
|
|
3
|
+
|
|
4
|
+
export = function (modules: { typescript: typeof TS }) {
|
|
5
|
+
const ts = modules.typescript;
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
create(info: Pick<server.PluginCreateInfo, 'languageService'>) {
|
|
9
|
+
const proxy = proxyObject(info.languageService);
|
|
10
|
+
|
|
11
|
+
proxy.getSemanticDiagnostics = function clonedSemanticDiagnostics(filename) {
|
|
12
|
+
const diagnostics = info.languageService.getSemanticDiagnostics(filename);
|
|
13
|
+
|
|
14
|
+
// Not a tsx file, so don't do anything
|
|
15
|
+
if (!filename.endsWith('.tsx') && !filename.endsWith('.jsx')) {
|
|
16
|
+
return diagnostics;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const program = info.languageService.getProgram();
|
|
20
|
+
|
|
21
|
+
if (!program) {
|
|
22
|
+
return diagnostics;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const source = program.getSourceFile(filename);
|
|
26
|
+
|
|
27
|
+
if (!source) {
|
|
28
|
+
return diagnostics;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const typeChecker = program.getTypeChecker();
|
|
32
|
+
|
|
33
|
+
ts.forEachChild(source, function loopSourceNodes(node) {
|
|
34
|
+
recursiveDiagnoseJsxElements(ts, node, typeChecker, diagnostics);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return diagnostics;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return proxy;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
};
|