@7n/rules-lang-js 0.3.1 → 0.4.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/CHANGELOG.md +6 -0
- package/package.json +14 -4
- package/rules/bun/bunfig/bunfig.mdc +17 -0
- package/rules/bun/bunfig/bunfig.rego +29 -0
- package/rules/bun/bunfig/concern.json +9 -0
- package/rules/bun/bunfig/template/bunfig.toml.snippet.toml +2 -0
- package/rules/bun/docs/index.md +11 -0
- package/rules/bun/layout/concern.json +16 -0
- package/rules/bun/layout/docs/fix-layout.md +29 -0
- package/rules/bun/layout/docs/main.md +34 -0
- package/rules/bun/layout/fix-layout.mjs +63 -0
- package/rules/bun/layout/layout.mdc +60 -0
- package/rules/bun/layout/main.mjs +53 -0
- package/rules/bun/licensee/concern.json +7 -0
- package/rules/bun/licensee/docs/fix-licensee.md +27 -0
- package/rules/bun/licensee/docs/index.md +12 -0
- package/rules/bun/licensee/docs/main.md +30 -0
- package/rules/bun/licensee/fix-licensee.mjs +31 -0
- package/rules/bun/licensee/main.mjs +68 -0
- package/rules/bun/lint-surface/concern.json +3 -0
- package/rules/bun/lint-surface/lint-surface.mdc +13 -0
- package/rules/bun/main.json +1 -0
- package/rules/bun/main.mdc +11 -0
- package/rules/bun/package_json/concern.json +9 -0
- package/rules/bun/package_json/docs/fix-package_json.md +28 -0
- package/rules/bun/package_json/docs/index.md +9 -0
- package/rules/bun/package_json/fix-package_json.mjs +311 -0
- package/rules/bun/package_json/package_json.mdc +14 -0
- package/rules/bun/package_json/package_json.rego +64 -0
- package/rules/bun/package_json/template/package.json.deny.json +4 -0
- package/rules/js/check/check.mdc +26 -0
- package/rules/js/check/concern.json +19 -0
- package/rules/js/check/docs/eslint-config.md +56 -0
- package/rules/js/check/docs/fix-check.md +48 -0
- package/rules/js/check/docs/index.md +13 -0
- package/rules/js/check/docs/main.md +48 -0
- package/rules/js/check/eslint-config.mjs +262 -0
- package/rules/js/check/fix-check.mjs +82 -0
- package/rules/js/check/main.mjs +310 -0
- package/rules/js/dep-policy/concern.json +4 -0
- package/rules/js/dep-policy/dep-policy.mdc +36 -0
- package/rules/js/dep-policy/docs/main.md +35 -0
- package/rules/js/dep-policy/main.mjs +99 -0
- package/rules/js/docs/index.md +11 -0
- package/rules/js/eslint/concern.json +8 -0
- package/rules/js/eslint/docs/fix-eslint.md +50 -0
- package/rules/js/eslint/docs/fix-worker.md +31 -0
- package/rules/js/eslint/docs/index.md +11 -0
- package/rules/js/eslint/docs/main.md +35 -0
- package/rules/js/eslint/fix-eslint.mjs +160 -0
- package/rules/js/eslint/fix-worker.mjs +139 -0
- package/rules/js/eslint/main.mjs +115 -0
- package/rules/js/file-extensions/concern.json +3 -0
- package/rules/js/file-extensions/file-extensions.mdc +12 -0
- package/rules/js/jscpd_config/concern.json +11 -0
- package/rules/js/jscpd_config/docs/fix-jscpd_config.md +25 -0
- package/rules/js/jscpd_config/docs/index.md +9 -0
- package/rules/js/jscpd_config/fix-jscpd_config.mjs +3 -0
- package/rules/js/jscpd_config/jscpd_config.mdc +42 -0
- package/rules/js/jscpd_config/jscpd_config.rego +44 -0
- package/rules/js/jscpd_config/template/.jscpd.json.snippet.json +7 -0
- package/rules/js/jscpd_duplicates/concern.json +7 -0
- package/rules/js/jscpd_duplicates/docs/main.md +29 -0
- package/rules/js/jscpd_duplicates/main.mjs +69 -0
- package/rules/js/knip/concern.json +7 -0
- package/rules/js/knip/docs/main.md +32 -0
- package/rules/js/knip/knip.mdc +15 -0
- package/rules/js/knip/main.mjs +68 -0
- package/rules/js/lint-findings/concern.json +3 -0
- package/rules/js/lint-findings/docs/main.md +40 -0
- package/rules/js/lint-findings/main.mjs +125 -0
- package/rules/js/main.json +1 -0
- package/rules/js/main.mdc +18 -0
- package/rules/js/package_json/concern.json +9 -0
- package/rules/js/package_json/docs/fix-package_json.md +25 -0
- package/rules/js/package_json/docs/index.md +9 -0
- package/rules/js/package_json/fix-package_json.mjs +3 -0
- package/rules/js/package_json/package_json.mdc +15 -0
- package/rules/js/package_json/package_json.rego +142 -0
- package/rules/js/package_json/template/package.json.snippet.json +6 -0
- package/rules/js/tooling/concern.json +3 -0
- package/rules/js/tooling/data/tooling/knip-canonical.json +30 -0
- package/rules/js/tooling/data/tooling/oxlint-canonical.json +400 -0
- package/rules/js/tooling/docs/main.md +53 -0
- package/rules/js/tooling/main.mjs +183 -0
- package/rules/js/utils_imports/concern.json +4 -0
- package/rules/js/utils_imports/docs/main.md +50 -0
- package/rules/js/utils_imports/main.mjs +185 -0
- package/rules/js/utils_imports/utils_imports.mdc +15 -0
- package/rules/js/vscode_extensions/concern.json +11 -0
- package/rules/js/vscode_extensions/docs/fix-vscode_extensions.md +24 -0
- package/rules/js/vscode_extensions/docs/index.md +11 -0
- package/rules/js/vscode_extensions/fix-vscode_extensions.mjs +1 -0
- package/rules/js/vscode_extensions/template/extensions.json.snippet.json +6 -0
- package/rules/js/vscode_extensions/vscode_extensions.mdc +11 -0
- package/rules/js/vscode_extensions/vscode_extensions.rego +12 -0
- package/rules/js-bun-db/connection/concern.json +3 -0
- package/rules/js-bun-db/connection/connection.mdc +42 -0
- package/rules/js-bun-db/docs/index.md +11 -0
- package/rules/js-bun-db/lib/bun-sql-scan.mjs +1047 -0
- package/rules/js-bun-db/lib/docs/bun-sql-scan.md +63 -0
- package/rules/js-bun-db/lib/docs/index.md +11 -0
- package/rules/js-bun-db/main.json +1 -0
- package/rules/js-bun-db/main.mdc +8 -0
- package/rules/js-bun-db/package_json/concern.json +9 -0
- package/rules/js-bun-db/package_json/package_json.mdc +31 -0
- package/rules/js-bun-db/package_json/package_json.rego +15 -0
- package/rules/js-bun-db/package_json/template/package.json.deny.json +6 -0
- package/rules/js-bun-db/pg_format_identifiers/concern.json +3 -0
- package/rules/js-bun-db/pg_format_identifiers/pg_format_identifiers.mdc +104 -0
- package/rules/js-bun-db/safety/concern.json +4 -0
- package/rules/js-bun-db/safety/docs/main.md +34 -0
- package/rules/js-bun-db/safety/main.mjs +430 -0
- package/rules/js-bun-db/safety/safety.mdc +458 -0
- package/rules/js-bun-redis/docs/index.md +11 -0
- package/rules/js-bun-redis/imports/concern.json +4 -0
- package/rules/js-bun-redis/imports/docs/main.md +36 -0
- package/rules/js-bun-redis/imports/imports.mdc +47 -0
- package/rules/js-bun-redis/imports/main.mjs +88 -0
- package/rules/js-bun-redis/lib/docs/index.md +11 -0
- package/rules/js-bun-redis/lib/docs/redis-imports.md +227 -0
- package/rules/js-bun-redis/lib/redis-imports.mjs +130 -0
- package/rules/js-bun-redis/main.json +1 -0
- package/rules/js-bun-redis/main.mdc +8 -0
- package/rules/js-bun-redis/package_json/concern.json +9 -0
- package/rules/js-bun-redis/package_json/package_json.mdc +11 -0
- package/rules/js-bun-redis/package_json/package_json.rego +15 -0
- package/rules/js-bun-redis/package_json/template/package.json.deny.json +12 -0
- package/rules/js-mssql/deps/concern.json +4 -0
- package/rules/js-mssql/deps/docs/main.md +33 -0
- package/rules/js-mssql/deps/main.mjs +297 -0
- package/rules/js-mssql/docs/index.md +11 -0
- package/rules/js-mssql/lib/docs/index.md +11 -0
- package/rules/js-mssql/lib/docs/mssql-pool-scan.md +380 -0
- package/rules/js-mssql/lib/mssql-pool-scan.mjs +610 -0
- package/rules/js-mssql/main.json +1 -0
- package/rules/js-mssql/main.mdc +144 -0
- package/rules/js-mssql/mssql-tvp/concern.json +3 -0
- package/rules/js-mssql/mssql-tvp/mssql-tvp.mdc +77 -0
- package/rules/js-mssql/package_json/concern.json +9 -0
- package/rules/js-mssql/package_json/package_json.mdc +9 -0
- package/rules/js-mssql/package_json/package_json.rego +57 -0
- package/rules/js-run/configmap/concern.json +9 -0
- package/rules/js-run/configmap/configmap.mdc +37 -0
- package/rules/js-run/configmap/configmap.rego +21 -0
- package/rules/js-run/configmap/template/configmap.yaml.contains.yml +4 -0
- package/rules/js-run/docs/index.md +11 -0
- package/rules/js-run/jsconfig/concern.json +9 -0
- package/rules/js-run/jsconfig/docs/fix-jsconfig.md +28 -0
- package/rules/js-run/jsconfig/docs/index.md +9 -0
- package/rules/js-run/jsconfig/fix-jsconfig.mjs +119 -0
- package/rules/js-run/jsconfig/jsconfig.mdc +48 -0
- package/rules/js-run/jsconfig/jsconfig.rego +59 -0
- package/rules/js-run/jsconfig/template/jsconfig.json.snippet.json +10 -0
- package/rules/js-run/lib/bunyan-imports.mjs +98 -0
- package/rules/js-run/lib/check-env-scan.mjs +338 -0
- package/rules/js-run/lib/conn-file-rules.mjs +214 -0
- package/rules/js-run/lib/conn-imports-scan.mjs +154 -0
- package/rules/js-run/lib/docs/bunyan-imports.md +121 -0
- package/rules/js-run/lib/docs/check-env-scan.md +438 -0
- package/rules/js-run/lib/docs/conn-file-rules.md +304 -0
- package/rules/js-run/lib/docs/conn-imports-scan.md +208 -0
- package/rules/js-run/lib/docs/index.md +16 -0
- package/rules/js-run/lib/docs/promise-settimeout-scan.md +334 -0
- package/rules/js-run/lib/docs/temporal-scan.md +29 -0
- package/rules/js-run/lib/promise-settimeout-scan.mjs +128 -0
- package/rules/js-run/lib/temporal-scan.mjs +52 -0
- package/rules/js-run/main.json +1 -0
- package/rules/js-run/main.mdc +16 -0
- package/rules/js-run/package_json/concern.json +9 -0
- package/rules/js-run/package_json/package_json.mdc +44 -0
- package/rules/js-run/package_json/package_json.rego +37 -0
- package/rules/js-run/package_json/template/package.json.deny.json +22 -0
- package/rules/js-run/project-structure/concern.json +3 -0
- package/rules/js-run/project-structure/project-structure.mdc +11 -0
- package/rules/js-run/runtime/concern.json +12 -0
- package/rules/js-run/runtime/docs/fix-runtime.md +27 -0
- package/rules/js-run/runtime/docs/main.md +35 -0
- package/rules/js-run/runtime/fix-runtime.mjs +46 -0
- package/rules/js-run/runtime/main.mjs +496 -0
- package/rules/js-run/runtime/runtime.mdc +184 -0
- package/rules/js-run/scope/concern.json +3 -0
- package/rules/js-run/scope/scope.mdc +11 -0
- package/rules/npm-module/docs/index.md +11 -0
- package/rules/npm-module/emit_types_config/concern.json +9 -0
- package/rules/npm-module/emit_types_config/docs/fix-emit_types_config.md +25 -0
- package/rules/npm-module/emit_types_config/docs/index.md +9 -0
- package/rules/npm-module/emit_types_config/emit_types_config.mdc +43 -0
- package/rules/npm-module/emit_types_config/emit_types_config.rego +28 -0
- package/rules/npm-module/emit_types_config/fix-emit_types_config.mjs +5 -0
- package/rules/npm-module/emit_types_config/template/tsconfig.emit-types.json.snippet.json +9 -0
- package/rules/npm-module/header_doc_pointer/concern.json +5 -0
- package/rules/npm-module/header_doc_pointer/docs/main.md +40 -0
- package/rules/npm-module/header_doc_pointer/header_doc_pointer.mdc +18 -0
- package/rules/npm-module/header_doc_pointer/main.mjs +131 -0
- package/rules/npm-module/main.json +1 -0
- package/rules/npm-module/main.mdc +36 -0
- package/rules/npm-module/npm_package_json/concern.json +9 -0
- package/rules/npm-module/npm_package_json/docs/fix-npm_package_json.md +26 -0
- package/rules/npm-module/npm_package_json/docs/index.md +9 -0
- package/rules/npm-module/npm_package_json/fix-npm_package_json.mjs +5 -0
- package/rules/npm-module/npm_package_json/npm_package_json.mdc +57 -0
- package/rules/npm-module/npm_package_json/npm_package_json.rego +73 -0
- package/rules/npm-module/npm_package_json/template/package.json.snippet.json +1 -0
- package/rules/npm-module/package_structure/concern.json +5 -0
- package/rules/npm-module/package_structure/docs/main.md +37 -0
- package/rules/npm-module/package_structure/main.mjs +448 -0
- package/rules/npm-module/package_structure/package_structure.mdc +63 -0
- package/rules/npm-module/root_package_json/concern.json +9 -0
- package/rules/npm-module/root_package_json/docs/fix-root_package_json.md +25 -0
- package/rules/npm-module/root_package_json/docs/index.md +9 -0
- package/rules/npm-module/root_package_json/fix-root_package_json.mjs +5 -0
- package/rules/npm-module/root_package_json/root_package_json.mdc +41 -0
- package/rules/npm-module/root_package_json/root_package_json.rego +28 -0
- package/rules/npm-module/root_package_json/template/package.json.snippet.json +1 -0
- package/rules/npm-module/rule_meta/concern.json +8 -0
- package/rules/npm-module/rule_meta/docs/main.md +35 -0
- package/rules/npm-module/rule_meta/main.mjs +119 -0
- package/rules/npm-module/rule_meta/rule_meta.mdc +11 -0
- package/rules/npm-module/skill_meta/concern.json +5 -0
- package/rules/npm-module/skill_meta/docs/main.md +149 -0
- package/rules/npm-module/skill_meta/main.mjs +91 -0
- package/rules/npm-module/skill_meta/skill_meta.mdc +11 -0
- package/rules/tool-surface/docs/index.md +11 -0
- package/rules/tool-surface/main.json +6 -0
- package/rules/tool-surface/main.mdc +72 -0
- package/rules/vue/composition-api/composition-api.mdc +82 -0
- package/rules/vue/composition-api/concern.json +3 -0
- package/rules/vue/docs/index.md +11 -0
- package/rules/vue/lib/docs/index.md +11 -0
- package/rules/vue/lib/docs/vue-forbidden-imports.md +265 -0
- package/rules/vue/lib/vue-forbidden-imports.mjs +240 -0
- package/rules/vue/main.json +1 -0
- package/rules/vue/main.mdc +18 -0
- package/rules/vue/nheader-layout/concern.json +3 -0
- package/rules/vue/nheader-layout/nheader-layout.mdc +171 -0
- package/rules/vue/package_json/concern.json +9 -0
- package/rules/vue/package_json/package_json.mdc +30 -0
- package/rules/vue/package_json/package_json.rego +140 -0
- package/rules/vue/packages/concern.json +6 -0
- package/rules/vue/packages/docs/index.md +11 -0
- package/rules/vue/packages/docs/main.md +35 -0
- package/rules/vue/packages/main.mjs +575 -0
- package/rules/vue/packages/packages.mdc +56 -0
- package/rules/vue/quasar-ui/concern.json +3 -0
- package/rules/vue/quasar-ui/quasar-ui.mdc +32 -0
- package/rules/vue/structure/concern.json +3 -0
- package/rules/vue/structure/structure.mdc +101 -0
- package/rules/vue/testing/concern.json +3 -0
- package/rules/vue/testing/testing.mdc +40 -0
- package/rules/vue/tfm-translations/concern.json +7 -0
- package/rules/vue/tfm-translations/docs/main.md +29 -0
- package/rules/vue/tfm-translations/main.mjs +55 -0
- package/rules/vue/tfm-translations/tfm-translations.mdc +32 -0
- package/rules/vue/vite-config/concern.json +3 -0
- package/rules/vue/vite-config/vite-config.mdc +153 -0
- package/rules/vue/vite-env/concern.json +3 -0
- package/rules/vue/vite-env/vite-env.mdc +61 -0
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Знаходить небезпечні патерни використання `mssql`, які створюють підключення/пул
|
|
3
|
+
* всередині функцій (наприклад handler на кожен запит), замість того щоб мати один
|
|
4
|
+
* singleton `sql.ConnectionPool` на рівні модуля та повторно використовувати його.
|
|
5
|
+
*
|
|
6
|
+
* Також знаходить небезпечний виклик `query(\`...\`)` — це НЕ tagged template, а звичайний
|
|
7
|
+
* виклик з інтерполяцією рядка, який може призвести до SQL injection. Натомість має
|
|
8
|
+
* використовуватись tagged template `query\`...\`` (див. js-mssql.mdc).
|
|
9
|
+
*
|
|
10
|
+
* Додатково знаходить:
|
|
11
|
+
* - shared `Request` (наприклад `export const request = pool.request()`), який не можна
|
|
12
|
+
* повторно використовувати між запитами.
|
|
13
|
+
* - небезпечні “динамічні списки” в SQL, коли в TemplateLiteral/TaggedTemplateExpression
|
|
14
|
+
* підставляють рядки, зібрані через `.join(',')` (типово для `IN (...)` або `VALUES (...)`).
|
|
15
|
+
* - підстановки `IN (${expr})`, де `expr` не пройшов числовий парсер (parseInt/Number/BigInt
|
|
16
|
+
* /parseFloat або унарний `+`) і не є літеральним масивом чисел — навіть у tagged template
|
|
17
|
+
* це додатковий шар захисту від SQL injection (див. js-mssql.mdc).
|
|
18
|
+
*
|
|
19
|
+
* Семантика береться з **oxc-parser** по AST, щоб не покладатися на regex.
|
|
20
|
+
* Якщо файл не парситься або містить синтаксичні помилки — повертаємо порожній
|
|
21
|
+
* результат (спочатку треба полагодити синтаксис, потім перезапустити перевірку).
|
|
22
|
+
*/
|
|
23
|
+
import { parseSync } from 'oxc-parser'
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
isFunctionNode,
|
|
27
|
+
isJoinCall,
|
|
28
|
+
isSqlListContextTemplate,
|
|
29
|
+
langFromPath,
|
|
30
|
+
normalizeSnippet,
|
|
31
|
+
offsetToLine,
|
|
32
|
+
walkAstWithAncestors
|
|
33
|
+
} from '@7n/rules/scripts/utils/ast-scan-utils.mjs'
|
|
34
|
+
|
|
35
|
+
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/
|
|
36
|
+
const IN_PLACEHOLDER_END_RE = /\bin\s*\(\s*$/iu
|
|
37
|
+
const NUMERIC_PARSE_FN_NAMES = new Set(['parseInt', 'parseFloat', 'Number', 'BigInt'])
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Чи є вузол літералом числа `0` (як NumericLiteral, так і generic Literal).
|
|
41
|
+
* @param {unknown} node AST-вузол
|
|
42
|
+
* @returns {boolean} true для `0`
|
|
43
|
+
*/
|
|
44
|
+
function isZeroLiteral(node) {
|
|
45
|
+
return (
|
|
46
|
+
!!node &&
|
|
47
|
+
typeof node === 'object' &&
|
|
48
|
+
((node.type === 'NumericLiteral' && node.value === 0) || (node.type === 'Literal' && node.value === 0))
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Чи `node` — це MemberExpression `name.length` (некомп'ютерований Identifier.Identifier).
|
|
54
|
+
* @param {unknown} node AST-вузол
|
|
55
|
+
* @param {string} name очікуване імʼя змінної-власника
|
|
56
|
+
* @returns {boolean} true для `<name>.length`
|
|
57
|
+
*/
|
|
58
|
+
function isLengthMemberOf(node, name) {
|
|
59
|
+
return (
|
|
60
|
+
!!node &&
|
|
61
|
+
typeof node === 'object' &&
|
|
62
|
+
node.type === 'MemberExpression' &&
|
|
63
|
+
!node.computed &&
|
|
64
|
+
!!node.object &&
|
|
65
|
+
node.object.type === 'Identifier' &&
|
|
66
|
+
node.object.name === name &&
|
|
67
|
+
!!node.property &&
|
|
68
|
+
node.property.type === 'Identifier' &&
|
|
69
|
+
node.property.name === 'length'
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const EMPTY_LIST_BINARY_OPERATORS = new Set(['===', '==', '<=', '<'])
|
|
74
|
+
const EMPTY_LIST_REVERSED_OPERATORS = new Set(['===', '=='])
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Чи `BinaryExpression`-тест з оператором з {@link EMPTY_LIST_BINARY_OPERATORS}
|
|
78
|
+
* порівнює `<name>.length` з літералом `0` (у будь-якому порядку для `===`/`==`).
|
|
79
|
+
* @param {Record<string, unknown>} test BinaryExpression вузол
|
|
80
|
+
* @param {string} name імʼя змінної списку
|
|
81
|
+
* @returns {boolean} true для умов виду `name.length === 0`, `name.length <= 0`, `0 === name.length` тощо
|
|
82
|
+
*/
|
|
83
|
+
function isEmptyListBinaryTest(test, name) {
|
|
84
|
+
const { left, right, operator } = test
|
|
85
|
+
if (!EMPTY_LIST_BINARY_OPERATORS.has(operator)) return false
|
|
86
|
+
if (isLengthMemberOf(left, name) && isZeroLiteral(right)) return true
|
|
87
|
+
return EMPTY_LIST_REVERSED_OPERATORS.has(operator) && isZeroLiteral(left) && isLengthMemberOf(right, name)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Чи містить тест if-умови перевірку “список порожній”.
|
|
92
|
+
* Підтримує базові форми:
|
|
93
|
+
* - `if (!ids.length) ...`
|
|
94
|
+
* - `if (ids.length === 0) ...` / `<= 0` / `< 1`
|
|
95
|
+
* @param {unknown} test IfStatement.test
|
|
96
|
+
* @param {string} name імʼя змінної списку
|
|
97
|
+
* @returns {boolean} true, якщо тест перевіряє, що `name` — порожній масив
|
|
98
|
+
*/
|
|
99
|
+
function isEmptyListTest(test, name) {
|
|
100
|
+
if (!test || typeof test !== 'object') return false
|
|
101
|
+
if (test.type === 'UnaryExpression' && test.operator === '!') {
|
|
102
|
+
return isLengthMemberOf(test.argument, name)
|
|
103
|
+
}
|
|
104
|
+
if (test.type === 'BinaryExpression') {
|
|
105
|
+
return isEmptyListBinaryTest(test, name)
|
|
106
|
+
}
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Чи є в consequent (або в його BlockStatement) ThrowStatement.
|
|
112
|
+
* @param {unknown} consequent IfStatement.consequent
|
|
113
|
+
* @returns {boolean} true, якщо гілка consequent містить `throw`
|
|
114
|
+
*/
|
|
115
|
+
function consequentHasThrow(consequent) {
|
|
116
|
+
if (!consequent || typeof consequent !== 'object') return false
|
|
117
|
+
if (consequent.type === 'ThrowStatement') return true
|
|
118
|
+
if (consequent.type === 'BlockStatement' && Array.isArray(consequent.body)) {
|
|
119
|
+
return consequent.body.some(s => s && typeof s === 'object' && s.type === 'ThrowStatement')
|
|
120
|
+
}
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Шукає “guard” `if (empty) throw` перед statementIndex у межах того ж BlockStatement.
|
|
126
|
+
* @param {unknown} block BlockStatement
|
|
127
|
+
* @param {number} statementIndex індекс statement, перед яким шукаємо guard
|
|
128
|
+
* @param {string} name імʼя змінної списку
|
|
129
|
+
* @returns {boolean} true, якщо знайдено guard `if (empty(name)) throw` до statementIndex
|
|
130
|
+
*/
|
|
131
|
+
function hasEmptyGuardBefore(block, statementIndex, name) {
|
|
132
|
+
if (!block || typeof block !== 'object' || block.type !== 'BlockStatement') return false
|
|
133
|
+
const body = block.body
|
|
134
|
+
if (!Array.isArray(body)) return false
|
|
135
|
+
for (let i = 0; i < statementIndex; i++) {
|
|
136
|
+
const st = body[i]
|
|
137
|
+
if (!st || typeof st !== 'object') continue
|
|
138
|
+
if (st.type !== 'IfStatement') continue
|
|
139
|
+
if (!isEmptyListTest(st.test, name)) continue
|
|
140
|
+
if (!consequentHasThrow(st.consequent)) continue
|
|
141
|
+
return true
|
|
142
|
+
}
|
|
143
|
+
return false
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Знаходить найближчий enclosing BlockStatement і statement всередині нього.
|
|
148
|
+
* @param {unknown[]} ancestors ancestors масив з walkAstWithAncestors
|
|
149
|
+
* @returns {{ block: unknown, index: number } | null} пара (BlockStatement, індекс statement) або null
|
|
150
|
+
*/
|
|
151
|
+
function findEnclosingBlockAndStatementIndex(ancestors) {
|
|
152
|
+
if (!Array.isArray(ancestors) || ancestors.length === 0) return null
|
|
153
|
+
|
|
154
|
+
// statement — перший зверху вузол, який лежить у block.body
|
|
155
|
+
// шукаємо пару (block, statement), де statement ∈ block.body
|
|
156
|
+
for (let i = ancestors.length - 1; i >= 1; i--) {
|
|
157
|
+
const maybeStatement = ancestors[i]
|
|
158
|
+
const maybeBlock = ancestors[i - 1]
|
|
159
|
+
if (!maybeBlock || typeof maybeBlock !== 'object' || maybeBlock.type !== 'BlockStatement') continue
|
|
160
|
+
if (!Array.isArray(maybeBlock.body)) continue
|
|
161
|
+
const idx = maybeBlock.body.indexOf(maybeStatement)
|
|
162
|
+
if (idx !== -1) return { block: maybeBlock, index: idx }
|
|
163
|
+
}
|
|
164
|
+
return null
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Чи це `new sql.ConnectionPool(...)` або `new mssql.ConnectionPool(...)`.
|
|
169
|
+
* @param {unknown} node AST node
|
|
170
|
+
* @returns {boolean} true, якщо це створення ConnectionPool
|
|
171
|
+
*/
|
|
172
|
+
function isNewConnectionPool(node) {
|
|
173
|
+
if (!node || node.type !== 'NewExpression') return false
|
|
174
|
+
const callee = node.callee
|
|
175
|
+
if (!callee || callee.type !== 'MemberExpression') return false
|
|
176
|
+
if (callee.computed) return false
|
|
177
|
+
const prop = callee.property
|
|
178
|
+
if (!prop || prop.type !== 'Identifier' || prop.name !== 'ConnectionPool') return false
|
|
179
|
+
const obj = callee.object
|
|
180
|
+
if (!obj || obj.type !== 'Identifier') return false
|
|
181
|
+
return obj.name === 'sql' || obj.name === 'mssql'
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Чи це виклик `.query(...)` з TemplateLiteral як першим аргументом (`query(\`...\`)`).
|
|
186
|
+
* @param {unknown} node AST node
|
|
187
|
+
* @returns {boolean} true, якщо це небезпечний патерн `query(\`...\`)`
|
|
188
|
+
*/
|
|
189
|
+
function isUnsafeQueryCallWithTemplateLiteral(node) {
|
|
190
|
+
if (!node || node.type !== 'CallExpression') return false
|
|
191
|
+
const callee = node.callee
|
|
192
|
+
if (!callee || callee.type !== 'MemberExpression') return false
|
|
193
|
+
if (callee.computed) return false
|
|
194
|
+
const prop = callee.property
|
|
195
|
+
if (!prop || prop.type !== 'Identifier' || prop.name !== 'query') return false
|
|
196
|
+
const args = node.arguments
|
|
197
|
+
if (!Array.isArray(args) || args.length === 0) return false
|
|
198
|
+
const first = args[0]
|
|
199
|
+
return !!first && typeof first === 'object' && first.type === 'TemplateLiteral'
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Чи це `something.request()` (наприклад `pool.request()`), яку не можна шарити між запитами.
|
|
204
|
+
* @param {unknown} node AST node
|
|
205
|
+
* @returns {boolean} true, якщо це CallExpression з `.request()`
|
|
206
|
+
*/
|
|
207
|
+
function isRequestFactoryCall(node) {
|
|
208
|
+
if (!node || node.type !== 'CallExpression') return false
|
|
209
|
+
const callee = node.callee
|
|
210
|
+
if (!callee || callee.type !== 'MemberExpression') return false
|
|
211
|
+
if (callee.computed) return false
|
|
212
|
+
const prop = callee.property
|
|
213
|
+
return !!prop && prop.type === 'Identifier' && prop.name === 'request'
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Знаходить створення `ConnectionPool` всередині функцій.
|
|
218
|
+
* @param {string} content вихідний код
|
|
219
|
+
* @param {string} [virtualPath] шлях для вибору `lang` (наприклад `pkg/src/db.ts`)
|
|
220
|
+
* @returns {{ line: number, snippet: string }[]} список порушень
|
|
221
|
+
*/
|
|
222
|
+
export function findMssqlPerRequestConnectionInText(content, virtualPath = 'scan.ts') {
|
|
223
|
+
const lang = langFromPath(virtualPath || 'scan.ts')
|
|
224
|
+
let result
|
|
225
|
+
try {
|
|
226
|
+
result = parseSync(virtualPath, content, { lang, sourceType: 'module' })
|
|
227
|
+
} catch {
|
|
228
|
+
return []
|
|
229
|
+
}
|
|
230
|
+
if (result.errors?.length) return []
|
|
231
|
+
|
|
232
|
+
/** @type {{ line: number, snippet: string }[]} */
|
|
233
|
+
const out = []
|
|
234
|
+
|
|
235
|
+
walkAstWithAncestors(result.program, [], (node, ancestors) => {
|
|
236
|
+
const insideFunction = ancestors.some(n => isFunctionNode(n))
|
|
237
|
+
if (!insideFunction) return
|
|
238
|
+
|
|
239
|
+
if (isNewConnectionPool(node)) {
|
|
240
|
+
out.push({
|
|
241
|
+
line: offsetToLine(content, node.start),
|
|
242
|
+
snippet: normalizeSnippet(content.slice(node.start, node.end))
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
return out
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Знаходить небезпечні виклики `query(\`...\`)` (CallExpression з TemplateLiteral-аргументом).
|
|
252
|
+
* @param {string} content вихідний код
|
|
253
|
+
* @param {string} [virtualPath] шлях для вибору `lang` (наприклад `pkg/src/db.ts`)
|
|
254
|
+
* @returns {{ line: number, snippet: string }[]} список порушень
|
|
255
|
+
*/
|
|
256
|
+
export function findUnsafeMssqlQueryTemplateCallInText(content, virtualPath = 'scan.ts') {
|
|
257
|
+
const lang = langFromPath(virtualPath || 'scan.ts')
|
|
258
|
+
let result
|
|
259
|
+
try {
|
|
260
|
+
result = parseSync(virtualPath, content, { lang, sourceType: 'module' })
|
|
261
|
+
} catch {
|
|
262
|
+
return []
|
|
263
|
+
}
|
|
264
|
+
if (result.errors?.length) return []
|
|
265
|
+
|
|
266
|
+
/** @type {{ line: number, snippet: string }[]} */
|
|
267
|
+
const out = []
|
|
268
|
+
walkAstWithAncestors(result.program, [], node => {
|
|
269
|
+
if (isUnsafeQueryCallWithTemplateLiteral(node)) {
|
|
270
|
+
out.push({
|
|
271
|
+
line: offsetToLine(content, node.start),
|
|
272
|
+
snippet: normalizeSnippet(content.slice(node.start, node.end))
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
return out
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Знаходить shared Request (`export const request = pool.request()` та подібні), які не можна
|
|
281
|
+
* повторно використовувати між запитами.
|
|
282
|
+
* @param {string} content вихідний код
|
|
283
|
+
* @param {string} [virtualPath] шлях для вибору `lang`
|
|
284
|
+
* @returns {{ line: number, snippet: string }[]} список порушень
|
|
285
|
+
*/
|
|
286
|
+
export function findSharedMssqlRequestInText(content, virtualPath = 'scan.ts') {
|
|
287
|
+
const lang = langFromPath(virtualPath || 'scan.ts')
|
|
288
|
+
let result
|
|
289
|
+
try {
|
|
290
|
+
result = parseSync(virtualPath, content, { lang, sourceType: 'module' })
|
|
291
|
+
} catch {
|
|
292
|
+
return []
|
|
293
|
+
}
|
|
294
|
+
if (result.errors?.length) return []
|
|
295
|
+
|
|
296
|
+
/** @type {{ line: number, snippet: string }[]} */
|
|
297
|
+
const out = []
|
|
298
|
+
walkAstWithAncestors(result.program, [], node => {
|
|
299
|
+
if (node.type !== 'VariableDeclarator') return
|
|
300
|
+
const id = node.id
|
|
301
|
+
if (!id || id.type !== 'Identifier') return
|
|
302
|
+
if (id.name !== 'request') return
|
|
303
|
+
const init = node.init
|
|
304
|
+
if (!init || typeof init !== 'object') return
|
|
305
|
+
if (!isRequestFactoryCall(init)) return
|
|
306
|
+
|
|
307
|
+
out.push({
|
|
308
|
+
line: offsetToLine(content, node.start),
|
|
309
|
+
snippet: normalizeSnippet(content.slice(node.start, node.end))
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
return out
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Знаходить небезпечні динамічні списки в SQL, коли у TemplateLiteral/TaggedTemplateExpression
|
|
317
|
+
* підставляють рядки, зібрані через `.join(...)` у контексті `IN (...)` або `VALUES (...)`.
|
|
318
|
+
*
|
|
319
|
+
* Цей патерн небезпечний навіть якщо зовні використовується tagged template, бо в запит
|
|
320
|
+
* потрапляє “готовий шматок SQL”, а не параметризовані значення.
|
|
321
|
+
* @param {string} content вихідний код
|
|
322
|
+
* @param {string} [virtualPath] шлях для вибору `lang`
|
|
323
|
+
* @returns {{ line: number, snippet: string }[]} список порушень
|
|
324
|
+
*/
|
|
325
|
+
export function findUnsafeMssqlDynamicSqlListInText(content, virtualPath = 'scan.ts') {
|
|
326
|
+
const lang = langFromPath(virtualPath || 'scan.ts')
|
|
327
|
+
let result
|
|
328
|
+
try {
|
|
329
|
+
result = parseSync(virtualPath, content, { lang, sourceType: 'module' })
|
|
330
|
+
} catch {
|
|
331
|
+
return []
|
|
332
|
+
}
|
|
333
|
+
if (result.errors?.length) return []
|
|
334
|
+
|
|
335
|
+
/** @type {{ line: number, snippet: string }[]} */
|
|
336
|
+
const out = []
|
|
337
|
+
walkAstWithAncestors(result.program, [], node => {
|
|
338
|
+
/** @type {unknown} */
|
|
339
|
+
let template = null
|
|
340
|
+
|
|
341
|
+
if (node.type === 'TemplateLiteral') {
|
|
342
|
+
template = node
|
|
343
|
+
} else if (node.type === 'TaggedTemplateExpression') {
|
|
344
|
+
template = node.quasi
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!template || typeof template !== 'object' || template.type !== 'TemplateLiteral') return
|
|
348
|
+
if (!isSqlListContextTemplate(template)) return
|
|
349
|
+
const expressions = template.expressions
|
|
350
|
+
if (!Array.isArray(expressions) || expressions.length === 0) return
|
|
351
|
+
|
|
352
|
+
const hasJoin = expressions.some(expr => isJoinCall(expr))
|
|
353
|
+
if (!hasJoin) return
|
|
354
|
+
|
|
355
|
+
out.push({
|
|
356
|
+
line: offsetToLine(content, template.start),
|
|
357
|
+
snippet: normalizeSnippet(content.slice(template.start, template.end))
|
|
358
|
+
})
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
return out
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Чи елементи літерального масиву — лише числові (numeric/bigint) літерали.
|
|
366
|
+
* Такі масиви безпечні в `IN (...)` навіть без явного парсера.
|
|
367
|
+
* @param {unknown} node AST node
|
|
368
|
+
* @returns {boolean} true, якщо це непорожній масив суто числових літералів
|
|
369
|
+
*/
|
|
370
|
+
function isLiteralNumericArrayExpression(node) {
|
|
371
|
+
if (!node || typeof node !== 'object' || node.type !== 'ArrayExpression') return false
|
|
372
|
+
const elements = node.elements
|
|
373
|
+
if (!Array.isArray(elements) || elements.length === 0) return false
|
|
374
|
+
return elements.every(el => {
|
|
375
|
+
if (!el || typeof el !== 'object') return false
|
|
376
|
+
if (el.type === 'NumericLiteral' || el.type === 'BigIntLiteral') return true
|
|
377
|
+
return el.type === 'Literal' && (typeof el.value === 'number' || typeof el.value === 'bigint')
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Чи це безпосередній виклик числового парсера (parseInt/parseFloat/Number/BigInt)
|
|
383
|
+
* або обʼєктний доступ до них (наприклад `Number.parseInt(...)`).
|
|
384
|
+
* @param {Record<string, unknown>} node AST CallExpression
|
|
385
|
+
* @returns {boolean} true, якщо callee — числовий парсер
|
|
386
|
+
*/
|
|
387
|
+
function isNumericParseCallExpression(node) {
|
|
388
|
+
if (node.type !== 'CallExpression') return false
|
|
389
|
+
const callee = node.callee
|
|
390
|
+
if (!callee) return false
|
|
391
|
+
if (callee.type === 'Identifier' && NUMERIC_PARSE_FN_NAMES.has(callee.name)) return true
|
|
392
|
+
if (callee.type === 'MemberExpression' && !callee.computed) {
|
|
393
|
+
const prop = callee.property
|
|
394
|
+
return !!prop && prop.type === 'Identifier' && NUMERIC_PARSE_FN_NAMES.has(prop.name)
|
|
395
|
+
}
|
|
396
|
+
return false
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Чи містить піддерево виклик числового парсера (parseInt/parseFloat/Number/BigInt)
|
|
401
|
+
* або унарний `+` (приведення до Number). Це сигнал, що значення гарантовано числове
|
|
402
|
+
* і не може містити SQL-метасимволи.
|
|
403
|
+
* @param {unknown} node AST node
|
|
404
|
+
* @returns {boolean} true, якщо знайдено числовий парсер у піддереві
|
|
405
|
+
*/
|
|
406
|
+
function subtreeHasNumericParseCall(node) {
|
|
407
|
+
if (!node || typeof node !== 'object') return false
|
|
408
|
+
if (Array.isArray(node)) return node.some(item => subtreeHasNumericParseCall(item))
|
|
409
|
+
|
|
410
|
+
if (isNumericParseCallExpression(node)) return true
|
|
411
|
+
if (node.type === 'UnaryExpression' && node.operator === '+') return true
|
|
412
|
+
|
|
413
|
+
for (const [key, v] of Object.entries(node)) {
|
|
414
|
+
if (key === 'parent') continue
|
|
415
|
+
if (v && typeof v === 'object' && subtreeHasNumericParseCall(v)) return true
|
|
416
|
+
}
|
|
417
|
+
return false
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Збирає всі VariableDeclarator-вузли в AST (для трасування Identifier-ів до їх init).
|
|
422
|
+
* @param {unknown} programNode AST root (Program)
|
|
423
|
+
* @returns {Array<Record<string, unknown>>} список VariableDeclarator-ів
|
|
424
|
+
*/
|
|
425
|
+
function collectVariableDeclarators(programNode) {
|
|
426
|
+
/** @type {Array<Record<string, unknown>>} */
|
|
427
|
+
const out = []
|
|
428
|
+
walkAstWithAncestors(programNode, [], node => {
|
|
429
|
+
if (node.type === 'VariableDeclarator') out.push(node)
|
|
430
|
+
})
|
|
431
|
+
return out
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Чи виглядає вираз, який підставляється в `IN (${...})`, як «безпечно розпарсений»:
|
|
436
|
+
* - літеральний масив чисел;
|
|
437
|
+
* - саме піддерево містить виклик числового парсера (parseInt/Number/BigInt/parseFloat/+x);
|
|
438
|
+
* - Identifier, чий init у файлі рекурсивно задовольняє ці умови.
|
|
439
|
+
*
|
|
440
|
+
* Якщо для Identifier немає видимого init (наприклад параметр функції чи import),
|
|
441
|
+
* вираз вважається не парсованим — потрібен явний парсер на місці підстановки.
|
|
442
|
+
* @param {unknown} expr вираз з template.expressions
|
|
443
|
+
* @param {Array<Record<string, unknown>>} declarators VariableDeclarator-и файлу
|
|
444
|
+
* @param {Set<string>} [seen] іменa Identifier-ів, що вже трасуються (анти-цикл)
|
|
445
|
+
* @returns {boolean} true, якщо вираз можна вважати безпечно числовим
|
|
446
|
+
*/
|
|
447
|
+
function isInListExpressionParsed(expr, declarators, seen = new Set()) {
|
|
448
|
+
if (!expr || typeof expr !== 'object') return false
|
|
449
|
+
if (isLiteralNumericArrayExpression(expr)) return true
|
|
450
|
+
if (subtreeHasNumericParseCall(expr)) return true
|
|
451
|
+
|
|
452
|
+
if (expr.type === 'Identifier' && typeof expr.name === 'string') {
|
|
453
|
+
if (seen.has(expr.name)) return false
|
|
454
|
+
const nextSeen = new Set(seen)
|
|
455
|
+
nextSeen.add(expr.name)
|
|
456
|
+
const inits = declarators
|
|
457
|
+
.filter(d => {
|
|
458
|
+
const id = d.id
|
|
459
|
+
return !!id && typeof id === 'object' && id.type === 'Identifier' && id.name === expr.name && !!d.init
|
|
460
|
+
})
|
|
461
|
+
.map(d => d.init)
|
|
462
|
+
if (inits.length === 0) return false
|
|
463
|
+
return inits.every(init => isInListExpressionParsed(init, declarators, nextSeen))
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return false
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Сирий текст quasi-елемента TemplateLiteral на позиції перед expressions[i].
|
|
471
|
+
* @param {unknown} q quasi-елемент TemplateLiteral
|
|
472
|
+
* @returns {string} `q.value.raw` або порожній рядок, якщо структура не підходить
|
|
473
|
+
*/
|
|
474
|
+
function quasiRawText(q) {
|
|
475
|
+
return q && typeof q === 'object' && q.value && typeof q.value === 'object' && typeof q.value.raw === 'string'
|
|
476
|
+
? q.value.raw
|
|
477
|
+
: ''
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Збирає порушення для одного TemplateLiteral вузла: знаходить expressions, що
|
|
482
|
+
* стоять одразу після `IN (` без числового парсера значень.
|
|
483
|
+
* @param {Record<string, unknown>} node TemplateLiteral
|
|
484
|
+
* @param {string} content вихідний код
|
|
485
|
+
* @param {Array<Record<string, unknown>>} declarators VariableDeclarator-и для трасування
|
|
486
|
+
* @param {{ line: number, snippet: string }[]} out буфер результатів
|
|
487
|
+
*/
|
|
488
|
+
function collectInListUnparsedFromTemplate(node, content, declarators, out) {
|
|
489
|
+
if (node.type !== 'TemplateLiteral') return
|
|
490
|
+
const quasis = node.quasis
|
|
491
|
+
const expressions = node.expressions
|
|
492
|
+
if (!Array.isArray(quasis) || !Array.isArray(expressions) || expressions.length === 0) return
|
|
493
|
+
|
|
494
|
+
for (const [i, expr] of expressions.entries()) {
|
|
495
|
+
if (!IN_PLACEHOLDER_END_RE.test(quasiRawText(quasis[i]))) continue
|
|
496
|
+
if (!expr || typeof expr !== 'object') continue
|
|
497
|
+
if (isJoinCall(expr)) continue
|
|
498
|
+
if (isInListExpressionParsed(expr, declarators)) continue
|
|
499
|
+
|
|
500
|
+
const startOffset = (typeof expr.start === 'number' ? expr : node).start
|
|
501
|
+
out.push({
|
|
502
|
+
line: offsetToLine(content, startOffset),
|
|
503
|
+
snippet: normalizeSnippet(content.slice(node.start, node.end))
|
|
504
|
+
})
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Збирає порушення для одного TemplateLiteral: якщо у `IN (${...})`:
|
|
510
|
+
* - `${...}` не є Identifier (значення не винесені у змінну);
|
|
511
|
+
* - або це Identifier, але перед запитом немає guard `if (empty) throw`.
|
|
512
|
+
* @param {Record<string, unknown>} node TemplateLiteral
|
|
513
|
+
* @param {unknown[]} ancestors ancestors від walkAstWithAncestors
|
|
514
|
+
* @param {string} content вихідний код
|
|
515
|
+
* @param {{ line: number, snippet: string, reason: 'not_var' | 'missing_guard', name?: string }[]} out буфер результатів
|
|
516
|
+
*/
|
|
517
|
+
function collectInListMissingEmptyGuardFromTemplate(node, ancestors, content, out) {
|
|
518
|
+
if (node.type !== 'TemplateLiteral') return
|
|
519
|
+
const quasis = node.quasis
|
|
520
|
+
const expressions = node.expressions
|
|
521
|
+
if (!Array.isArray(quasis) || !Array.isArray(expressions) || expressions.length === 0) return
|
|
522
|
+
|
|
523
|
+
for (const [i, expr] of expressions.entries()) {
|
|
524
|
+
if (!IN_PLACEHOLDER_END_RE.test(quasiRawText(quasis[i]))) continue
|
|
525
|
+
if (!expr || typeof expr !== 'object') continue
|
|
526
|
+
|
|
527
|
+
if (expr.type !== 'Identifier' || typeof expr.name !== 'string') {
|
|
528
|
+
out.push({
|
|
529
|
+
line: offsetToLine(content, node.start),
|
|
530
|
+
snippet: normalizeSnippet(content.slice(node.start, node.end)),
|
|
531
|
+
reason: 'not_var'
|
|
532
|
+
})
|
|
533
|
+
continue
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const place = findEnclosingBlockAndStatementIndex(ancestors)
|
|
537
|
+
if (!place || !hasEmptyGuardBefore(place.block, place.index, expr.name)) {
|
|
538
|
+
out.push({
|
|
539
|
+
line: offsetToLine(content, node.start),
|
|
540
|
+
snippet: normalizeSnippet(content.slice(node.start, node.end)),
|
|
541
|
+
reason: 'missing_guard',
|
|
542
|
+
name: expr.name
|
|
543
|
+
})
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Знаходить підстановки IN (вираз) у TemplateLiteral, де вираз не пройшов числовий парсер.
|
|
550
|
+
*
|
|
551
|
+
* Навіть у безпечному tagged template pool.request().query краще явно парсити значення (parseInt,
|
|
552
|
+
* Number, BigInt, parseFloat) та фільтрувати NaN. Див. також findUnsafeMssqlDynamicSqlListInText для
|
|
553
|
+
* випадків arr.join у списках.
|
|
554
|
+
* @param {string} content вихідний код
|
|
555
|
+
* @param {string} [virtualPath] шлях для вибору мови парсера (lang)
|
|
556
|
+
* @returns {{ line: number, snippet: string }[]} список порушень
|
|
557
|
+
*/
|
|
558
|
+
export function findUnsafeMssqlInListUnparsedInText(content, virtualPath = 'scan.ts') {
|
|
559
|
+
const lang = langFromPath(virtualPath || 'scan.ts')
|
|
560
|
+
let result
|
|
561
|
+
try {
|
|
562
|
+
result = parseSync(virtualPath, content, { lang, sourceType: 'module' })
|
|
563
|
+
} catch {
|
|
564
|
+
return []
|
|
565
|
+
}
|
|
566
|
+
if (result.errors?.length) return []
|
|
567
|
+
|
|
568
|
+
const declarators = collectVariableDeclarators(result.program)
|
|
569
|
+
|
|
570
|
+
/** @type {{ line: number, snippet: string }[]} */
|
|
571
|
+
const out = []
|
|
572
|
+
walkAstWithAncestors(result.program, [], node => collectInListUnparsedFromTemplate(node, content, declarators, out))
|
|
573
|
+
|
|
574
|
+
return out
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Знаходить підстановки списків у `IN (${...})`, які:
|
|
579
|
+
* - не винесені в окрему змінну (в `${...}` стоїть не Identifier);
|
|
580
|
+
* - або винесені, але перед запитом немає перевірки на пустоту з `throw`.
|
|
581
|
+
* @param {string} content вихідний код
|
|
582
|
+
* @param {string} [virtualPath] шлях для вибору мови парсера (lang)
|
|
583
|
+
* @returns {{ line: number, snippet: string, reason: 'not_var' | 'missing_guard', name?: string }[]} список порушень
|
|
584
|
+
*/
|
|
585
|
+
export function findUnsafeMssqlInListMissingEmptyGuardInText(content, virtualPath = 'scan.ts') {
|
|
586
|
+
const lang = langFromPath(virtualPath || 'scan.ts')
|
|
587
|
+
let result
|
|
588
|
+
try {
|
|
589
|
+
result = parseSync(virtualPath, content, { lang, sourceType: 'module' })
|
|
590
|
+
} catch {
|
|
591
|
+
return []
|
|
592
|
+
}
|
|
593
|
+
if (result.errors?.length) return []
|
|
594
|
+
|
|
595
|
+
/** @type {{ line: number, snippet: string, reason: 'not_var' | 'missing_guard', name?: string }[]} */
|
|
596
|
+
const out = []
|
|
597
|
+
walkAstWithAncestors(result.program, [], (node, ancestors) =>
|
|
598
|
+
collectInListMissingEmptyGuardFromTemplate(node, ancestors, content, out)
|
|
599
|
+
)
|
|
600
|
+
return out
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Чи сканувати цей файл за розширенням (JS/TS-сім'я).
|
|
605
|
+
* @param {string} relativePathPosix відносний шлях (posix)
|
|
606
|
+
* @returns {boolean} `true`, якщо розширення підходить для AST-скану
|
|
607
|
+
*/
|
|
608
|
+
export function isMssqlScanSourceFile(relativePathPosix) {
|
|
609
|
+
return SOURCE_FILE_RE.test(relativePathPosix) && !relativePathPosix.endsWith('.d.ts')
|
|
610
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "auto": { "predicate": "depInAnyPackageJson", "arg": ["mssql"] } }
|