@nitra/cursor 1.8.130 → 1.8.132
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/bin/auto-rules.md +4 -0
- package/mdc/docker.mdc +66 -1
- package/mdc/js-mssql.mdc +59 -0
- package/mdc/php.mdc +132 -0
- package/package.json +1 -1
- package/scripts/auto-rules.mjs +60 -0
- package/scripts/check-docker.mjs +130 -4
- package/scripts/check-ga.mjs +207 -86
- package/scripts/check-js-mssql.mjs +203 -0
- package/scripts/check-k8s.mjs +12 -12
- package/scripts/check-php.mjs +80 -0
- package/scripts/run-php.mjs +125 -0
- package/scripts/utils/mssql-pool-scan.mjs +208 -0
package/scripts/check-k8s.mjs
CHANGED
|
@@ -370,9 +370,9 @@ function pushStringPaths(arr, acc) {
|
|
|
370
370
|
const KUSTOMIZE_CONFIG_API_PREFIX = 'kustomize.config.k8s.io/'
|
|
371
371
|
|
|
372
372
|
/**
|
|
373
|
-
* Чи послідовність непорожніх рядків
|
|
374
|
-
* @param {string[]} paths
|
|
375
|
-
* @returns {boolean}
|
|
373
|
+
* Чи послідовність непорожніх рядків відсортована за `localeCompare` (en, ascending).
|
|
374
|
+
* @param {string[]} paths рядки для перевірки
|
|
375
|
+
* @returns {boolean} `true` якщо послідовність відсортована
|
|
376
376
|
*/
|
|
377
377
|
function stringPathsAreSortedEn(paths) {
|
|
378
378
|
for (let i = 1; i < paths.length; i++) {
|
|
@@ -402,8 +402,7 @@ export function kustomizationResourcesSortedAlphabeticallyViolation(obj) {
|
|
|
402
402
|
}
|
|
403
403
|
/** @type {string[]} */
|
|
404
404
|
const paths = []
|
|
405
|
-
for (
|
|
406
|
-
const item = res[i]
|
|
405
|
+
for (const [i, item] of res.entries()) {
|
|
407
406
|
if (typeof item !== 'string') {
|
|
408
407
|
return `Kustomization.resources[${i}] — очікується рядок-шлях (k8s.mdc)`
|
|
409
408
|
}
|
|
@@ -412,7 +411,7 @@ export function kustomizationResourcesSortedAlphabeticallyViolation(obj) {
|
|
|
412
411
|
}
|
|
413
412
|
if (paths.length < 2) return null
|
|
414
413
|
if (!stringPathsAreSortedEn(paths)) {
|
|
415
|
-
const want =
|
|
414
|
+
const want = paths.toSorted((a, b) => a.localeCompare(b, 'en', { sensitivity: 'base' }))
|
|
416
415
|
return `Kustomization.resources має бути за алфавітом (en). Зараз: ${paths.join(', ')}; очікувано: ${want.join(', ')} (k8s.mdc)`
|
|
417
416
|
}
|
|
418
417
|
return null
|
|
@@ -422,17 +421,18 @@ export function kustomizationResourcesSortedAlphabeticallyViolation(obj) {
|
|
|
422
421
|
* Усі **`kustomization.yaml`**: **`resources`**, відсортовані за en.
|
|
423
422
|
* @param {string} root корінь репо
|
|
424
423
|
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
425
|
-
* @param {(msg: string) => void} fail
|
|
426
|
-
* @returns {Promise<void>}
|
|
424
|
+
* @param {(msg: string) => void} fail функція для фіксації порушення
|
|
425
|
+
* @returns {Promise<void>} завершується після перевірки всіх kustomization.yaml
|
|
427
426
|
*/
|
|
428
427
|
async function validateKustomizationResourcesSortedAlphabetically(root, yamlFilesAbs, fail) {
|
|
429
428
|
for (const kustAbs of yamlFilesAbs.filter(p => basename(p).toLowerCase() === 'kustomization.yaml')) {
|
|
430
429
|
const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
431
430
|
const kust = await readFirstYamlObject(kustAbs)
|
|
432
|
-
if (kust
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
431
|
+
if (kust !== null) {
|
|
432
|
+
const v = kustomizationResourcesSortedAlphabeticallyViolation(kust)
|
|
433
|
+
if (v !== null) {
|
|
434
|
+
fail(`${rel}: ${v}`)
|
|
435
|
+
}
|
|
436
436
|
}
|
|
437
437
|
}
|
|
438
438
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Перевіряє вимоги правила php.mdc для PHP-проєктів.
|
|
3
|
+
*
|
|
4
|
+
* Очікування:
|
|
5
|
+
* - у корені є `composer.json`;
|
|
6
|
+
* - у `package.json` є скрипт `lint-php` (рекомендовано делегувати в `run-php.mjs`);
|
|
7
|
+
* - у `.github/workflows/lint-php.yml` є крок `run: bun run lint-php` (для Bun-репозиторіїв).
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync } from 'node:fs'
|
|
10
|
+
import { readFile } from 'node:fs/promises'
|
|
11
|
+
|
|
12
|
+
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
13
|
+
import { anyRunStepIncludes, parseWorkflowYaml } from './utils/gha-workflow.mjs'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Перевіряє наявність `composer.json`.
|
|
17
|
+
* @param {import('./utils/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
|
|
18
|
+
*/
|
|
19
|
+
function checkComposer(reporter) {
|
|
20
|
+
const { pass, fail } = reporter
|
|
21
|
+
if (existsSync('composer.json')) {
|
|
22
|
+
pass('composer.json існує')
|
|
23
|
+
} else {
|
|
24
|
+
fail('composer.json не знайдено в корені — додай (php.mdc)')
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Перевіряє кореневий `package.json` на скрипт `lint-php`.
|
|
30
|
+
* @param {import('./utils/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
|
|
31
|
+
*/
|
|
32
|
+
async function checkPackageJson(reporter) {
|
|
33
|
+
const { pass, fail } = reporter
|
|
34
|
+
if (!existsSync('package.json')) {
|
|
35
|
+
fail('package.json не знайдено в корені — додай (php.mdc)')
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
const pkg = JSON.parse(await readFile('package.json', 'utf8'))
|
|
39
|
+
const lintPhp = pkg.scripts?.['lint-php']
|
|
40
|
+
if (lintPhp) {
|
|
41
|
+
pass('package.json містить скрипт lint-php')
|
|
42
|
+
} else {
|
|
43
|
+
fail('package.json: додай скрипт "lint-php" (php.mdc)')
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Перевіряє workflow `lint-php.yml`.
|
|
49
|
+
* @param {import('./utils/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
|
|
50
|
+
*/
|
|
51
|
+
async function checkWorkflow(reporter) {
|
|
52
|
+
const { pass, fail } = reporter
|
|
53
|
+
const wfPath = '.github/workflows/lint-php.yml'
|
|
54
|
+
if (!existsSync(wfPath)) {
|
|
55
|
+
fail(`${wfPath} не існує — створи згідно php.mdc`)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
const content = await readFile(wfPath, 'utf8')
|
|
59
|
+
pass('lint-php.yml існує')
|
|
60
|
+
const root = parseWorkflowYaml(content)
|
|
61
|
+
const ok = root ? anyRunStepIncludes(root, 'bun run lint-php') : content.includes('bun run lint-php')
|
|
62
|
+
if (ok) {
|
|
63
|
+
pass('lint-php.yml викликає bun run lint-php')
|
|
64
|
+
} else {
|
|
65
|
+
fail('lint-php.yml має містити крок run: bun run lint-php (php.mdc)')
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Перевіряє відповідність проєкту правилам php.mdc.
|
|
71
|
+
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
72
|
+
*/
|
|
73
|
+
export async function check() {
|
|
74
|
+
const reporter = createCheckReporter()
|
|
75
|
+
checkComposer(reporter)
|
|
76
|
+
await checkPackageJson(reporter)
|
|
77
|
+
await checkWorkflow(reporter)
|
|
78
|
+
return reporter.getExitCode()
|
|
79
|
+
}
|
|
80
|
+
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Запуск `lint-php` за правилом php.mdc: `composer audit` і, якщо встановлені пакети, запуск
|
|
3
|
+
* PHPStan, Psalm, PHP-CS-Fixer (dry-run) та PHPCS зі стандартом Security.
|
|
4
|
+
*
|
|
5
|
+
* Скрипт не вимагає, щоб усі інструменти були встановлені: якщо відповідного файла
|
|
6
|
+
* `vendor/bin/<tool>` немає, крок пропускається з повідомленням. Але якщо в корені є
|
|
7
|
+
* `composer.json`, то `composer` має бути доступний у PATH (інакше це помилка).
|
|
8
|
+
*
|
|
9
|
+
* Якщо `composer.json` у корені відсутній — вихід 0 без запуску інструментів.
|
|
10
|
+
*/
|
|
11
|
+
import { spawnSync } from 'node:child_process'
|
|
12
|
+
import { existsSync, statSync } from 'node:fs'
|
|
13
|
+
import { join, resolve } from 'node:path'
|
|
14
|
+
|
|
15
|
+
import { isRunAsCli } from './cli-entry.mjs'
|
|
16
|
+
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
17
|
+
import { resolveCmd } from './utils/resolve-cmd.mjs'
|
|
18
|
+
|
|
19
|
+
const PHPCS_CODE_DIR_CANDIDATES = ['app', 'src', 'lib', 'public', 'www']
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Каталоги коду для PHPCS (якщо типових директорій немає — перевіряємо `.`).
|
|
23
|
+
* @param {string} root корінь репозиторію
|
|
24
|
+
* @returns {string[]} перелік шляхів (відносно root), які варто передати у `phpcs`
|
|
25
|
+
*/
|
|
26
|
+
export function getPhpcsCodePaths(root) {
|
|
27
|
+
const out = []
|
|
28
|
+
for (const d of PHPCS_CODE_DIR_CANDIDATES) {
|
|
29
|
+
const p = join(root, d)
|
|
30
|
+
if (existsSync(p) && statSync(p).isDirectory()) out.push(d)
|
|
31
|
+
}
|
|
32
|
+
return out.length > 0 ? out : ['.']
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} root корінь репозиторію
|
|
37
|
+
* @param {string} name імʼя файла у vendor/bin
|
|
38
|
+
* @returns {string | null} абсолютний шлях або null, якщо файла немає
|
|
39
|
+
*/
|
|
40
|
+
function vendorBin(root, name) {
|
|
41
|
+
const p = resolve(root, 'vendor', 'bin', name)
|
|
42
|
+
return existsSync(p) ? p : null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {string} label назва кроку для повідомлень
|
|
47
|
+
* @param {string} abs абсолютний шлях до CLI
|
|
48
|
+
* @param {string[]} args аргументи
|
|
49
|
+
* @param {(msg: string) => void} pass callback pass
|
|
50
|
+
* @param {(msg: string) => void} fail callback fail
|
|
51
|
+
* @returns {boolean} true якщо OK
|
|
52
|
+
*/
|
|
53
|
+
function runTool(label, abs, args, pass, fail) {
|
|
54
|
+
const r = spawnSync(abs, args, { stdio: 'inherit', shell: false })
|
|
55
|
+
if (r.status === 0) {
|
|
56
|
+
pass(`lint-php: ${label} — OK`)
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
const code = typeof r.status === 'number' ? r.status : 1
|
|
60
|
+
fail(`lint-php: ${label} — помилка (код ${code}, php.mdc)`)
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Запускає `lint-php`.
|
|
66
|
+
* @returns {number} 0 — OK, 1 — є помилки
|
|
67
|
+
*/
|
|
68
|
+
export function run() {
|
|
69
|
+
const reporter = createCheckReporter()
|
|
70
|
+
const { pass, fail } = reporter
|
|
71
|
+
|
|
72
|
+
const root = process.cwd()
|
|
73
|
+
if (!existsSync(join(root, 'composer.json'))) {
|
|
74
|
+
pass('lint-php: немає composer.json у корені — кроки PHP пропущено')
|
|
75
|
+
return reporter.getExitCode()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const composer = resolveCmd('composer')
|
|
79
|
+
if (!composer) {
|
|
80
|
+
fail('lint-php: `composer` не знайдено в PATH (потрібен при наявному composer.json, php.mdc)')
|
|
81
|
+
return reporter.getExitCode()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!runTool('composer audit', composer, ['audit', '--no-interaction'], pass, fail)) return reporter.getExitCode()
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Запускає інструмент з `vendor/bin`, якщо він встановлений.
|
|
88
|
+
* @param {string} binName імʼя файла у vendor/bin
|
|
89
|
+
* @param {string} label назва кроку
|
|
90
|
+
* @param {string[]} args аргументи CLI
|
|
91
|
+
* @returns {boolean} true, якщо крок успішний або пропущений
|
|
92
|
+
*/
|
|
93
|
+
function runOptionalVendorTool(binName, label, args) {
|
|
94
|
+
const abs = vendorBin(root, binName)
|
|
95
|
+
if (!abs) {
|
|
96
|
+
pass(`lint-php: vendor/bin/${binName} — відсутній, крок пропущено`)
|
|
97
|
+
return true
|
|
98
|
+
}
|
|
99
|
+
return runTool(label, abs, args, pass, fail)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!runOptionalVendorTool('php-cs-fixer', 'PHP-CS-Fixer (dry-run)', ['fix', '--dry-run', '--diff'])) {
|
|
103
|
+
return reporter.getExitCode()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const phpcsPaths = getPhpcsCodePaths(root)
|
|
107
|
+
if (
|
|
108
|
+
!runOptionalVendorTool('phpcs', 'phpcs (Security)', [
|
|
109
|
+
'--standard=Security',
|
|
110
|
+
'--ignore=*/vendor/*,*/node_modules/*,*/.git/*',
|
|
111
|
+
...phpcsPaths
|
|
112
|
+
])
|
|
113
|
+
) {
|
|
114
|
+
return reporter.getExitCode()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!runOptionalVendorTool('phpstan', 'PHPStan', ['analyse', '--no-progress'])) return reporter.getExitCode()
|
|
118
|
+
if (!runOptionalVendorTool('psalm', 'Psalm', ['--no-cache'])) return reporter.getExitCode()
|
|
119
|
+
|
|
120
|
+
return reporter.getExitCode()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (isRunAsCli()) {
|
|
124
|
+
process.exitCode = run()
|
|
125
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
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
|
+
* Семантика береться з **oxc-parser** по AST, щоб не покладатися на regex.
|
|
11
|
+
* Якщо файл не парситься або містить синтаксичні помилки — повертаємо порожній
|
|
12
|
+
* результат (спочатку треба полагодити синтаксис, потім перезапустити перевірку).
|
|
13
|
+
*/
|
|
14
|
+
import { parseSync } from 'oxc-parser'
|
|
15
|
+
|
|
16
|
+
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Мова для Oxc за шляхом файлу (розширення).
|
|
20
|
+
* @param {string} filePath віртуальний або реальний шлях до файлу
|
|
21
|
+
* @returns {'js' | 'jsx' | 'ts' | 'tsx'} значення опції `lang` для `parseSync`
|
|
22
|
+
*/
|
|
23
|
+
function langFromPath(filePath) {
|
|
24
|
+
const lower = filePath.toLowerCase()
|
|
25
|
+
if (lower.endsWith('.tsx')) return 'tsx'
|
|
26
|
+
if (lower.endsWith('.ts') || lower.endsWith('.mts') || lower.endsWith('.cts')) return 'ts'
|
|
27
|
+
if (lower.endsWith('.jsx')) return 'jsx'
|
|
28
|
+
return 'js'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Номер рядка (1-based) за зміщенням у буфері.
|
|
33
|
+
* @param {string} content повний текст файлу
|
|
34
|
+
* @param {number} offset байтове зміщення початку фрагмента
|
|
35
|
+
* @returns {number} номер рядка від 1
|
|
36
|
+
*/
|
|
37
|
+
function offsetToLine(content, offset) {
|
|
38
|
+
let line = 1
|
|
39
|
+
const n = Math.min(offset, content.length)
|
|
40
|
+
for (let i = 0; i < n; i++) {
|
|
41
|
+
if (content.codePointAt(i) === 10) line++
|
|
42
|
+
}
|
|
43
|
+
return line
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Стискає пробіли для повідомлення про порушення.
|
|
48
|
+
* @param {string} s фрагмент коду
|
|
49
|
+
* @returns {string} скорочений однорядковий рядок
|
|
50
|
+
*/
|
|
51
|
+
function normalizeSnippet(s) {
|
|
52
|
+
return s.replaceAll(/\s+/g, ' ').trim().slice(0, 180)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Чи є вузол функцією.
|
|
57
|
+
* @param {unknown} node AST node
|
|
58
|
+
* @returns {boolean} true, якщо це будь-який вузол-функція
|
|
59
|
+
*/
|
|
60
|
+
function isFunctionNode(node) {
|
|
61
|
+
return (
|
|
62
|
+
!!node &&
|
|
63
|
+
typeof node === 'object' &&
|
|
64
|
+
typeof node.type === 'string' &&
|
|
65
|
+
(node.type === 'FunctionDeclaration' ||
|
|
66
|
+
node.type === 'FunctionExpression' ||
|
|
67
|
+
node.type === 'ArrowFunctionExpression')
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Рекурсивний обхід AST з предками, щоб визначати контекст (всередині функції чи ні).
|
|
73
|
+
* @param {unknown} node поточний вузол
|
|
74
|
+
* @param {unknown[]} ancestors масив предків від кореня до parent
|
|
75
|
+
* @param {(n: Record<string, unknown>, ancestors: unknown[]) => void} visit відвідувач для вузлів з `type`
|
|
76
|
+
* @returns {void}
|
|
77
|
+
*/
|
|
78
|
+
function walkAstWithAncestors(node, ancestors, visit) {
|
|
79
|
+
if (!node || typeof node !== 'object') return
|
|
80
|
+
if (Array.isArray(node)) {
|
|
81
|
+
for (const item of node) walkAstWithAncestors(item, ancestors, visit)
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const rec = /** @type {Record<string, unknown>} */ (node)
|
|
86
|
+
if (typeof rec.type === 'string') {
|
|
87
|
+
visit(rec, ancestors)
|
|
88
|
+
ancestors = [...ancestors, rec]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const key of Object.keys(node)) {
|
|
92
|
+
if (key === 'parent') {
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
const v = rec[key]
|
|
96
|
+
if (v && typeof v === 'object') {
|
|
97
|
+
walkAstWithAncestors(v, ancestors, visit)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Чи це `new sql.ConnectionPool(...)` або `new mssql.ConnectionPool(...)`.
|
|
104
|
+
* @param {unknown} node AST node
|
|
105
|
+
* @returns {boolean} true, якщо це створення ConnectionPool
|
|
106
|
+
*/
|
|
107
|
+
function isNewConnectionPool(node) {
|
|
108
|
+
if (!node || node.type !== 'NewExpression') return false
|
|
109
|
+
const callee = node.callee
|
|
110
|
+
if (!callee || callee.type !== 'MemberExpression') return false
|
|
111
|
+
if (callee.computed) return false
|
|
112
|
+
const obj = callee.object
|
|
113
|
+
const prop = callee.property
|
|
114
|
+
if (!obj || obj.type !== 'Identifier') return false
|
|
115
|
+
if (!prop || prop.type !== 'Identifier' || prop.name !== 'ConnectionPool') return false
|
|
116
|
+
return obj.name === 'sql' || obj.name === 'mssql'
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Чи це виклик `.query(...)` з TemplateLiteral як першим аргументом (`query(\`...\`)`).
|
|
121
|
+
* @param {unknown} node AST node
|
|
122
|
+
* @returns {boolean} true, якщо це небезпечний патерн `query(\`...\`)`
|
|
123
|
+
*/
|
|
124
|
+
function isUnsafeQueryCallWithTemplateLiteral(node) {
|
|
125
|
+
if (!node || node.type !== 'CallExpression') return false
|
|
126
|
+
const callee = node.callee
|
|
127
|
+
if (!callee || callee.type !== 'MemberExpression') return false
|
|
128
|
+
if (callee.computed) return false
|
|
129
|
+
const prop = callee.property
|
|
130
|
+
if (!prop || prop.type !== 'Identifier' || prop.name !== 'query') return false
|
|
131
|
+
const args = node.arguments
|
|
132
|
+
if (!Array.isArray(args) || args.length === 0) return false
|
|
133
|
+
const first = args[0]
|
|
134
|
+
return !!first && typeof first === 'object' && first.type === 'TemplateLiteral'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Знаходить створення `ConnectionPool` всередині функцій.
|
|
139
|
+
* @param {string} content вихідний код
|
|
140
|
+
* @param {string} [virtualPath] шлях для вибору `lang` (наприклад `pkg/src/db.ts`)
|
|
141
|
+
* @returns {{ line: number, snippet: string }[]} список порушень
|
|
142
|
+
*/
|
|
143
|
+
export function findMssqlPerRequestConnectionInText(content, virtualPath = 'scan.ts') {
|
|
144
|
+
const lang = langFromPath(virtualPath || 'scan.ts')
|
|
145
|
+
let result
|
|
146
|
+
try {
|
|
147
|
+
result = parseSync(virtualPath, content, { lang, sourceType: 'module' })
|
|
148
|
+
} catch {
|
|
149
|
+
return []
|
|
150
|
+
}
|
|
151
|
+
if (result.errors?.length) return []
|
|
152
|
+
|
|
153
|
+
/** @type {{ line: number, snippet: string }[]} */
|
|
154
|
+
const out = []
|
|
155
|
+
|
|
156
|
+
walkAstWithAncestors(result.program, [], (node, ancestors) => {
|
|
157
|
+
const insideFunction = ancestors.some(n => isFunctionNode(n))
|
|
158
|
+
if (!insideFunction) return
|
|
159
|
+
|
|
160
|
+
if (isNewConnectionPool(node)) {
|
|
161
|
+
out.push({
|
|
162
|
+
line: offsetToLine(content, node.start),
|
|
163
|
+
snippet: normalizeSnippet(content.slice(node.start, node.end))
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
return out
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Знаходить небезпечні виклики `query(\`...\`)` (CallExpression з TemplateLiteral-аргументом).
|
|
173
|
+
* @param {string} content вихідний код
|
|
174
|
+
* @param {string} [virtualPath] шлях для вибору `lang` (наприклад `pkg/src/db.ts`)
|
|
175
|
+
* @returns {{ line: number, snippet: string }[]} список порушень
|
|
176
|
+
*/
|
|
177
|
+
export function findUnsafeMssqlQueryTemplateCallInText(content, virtualPath = 'scan.ts') {
|
|
178
|
+
const lang = langFromPath(virtualPath || 'scan.ts')
|
|
179
|
+
let result
|
|
180
|
+
try {
|
|
181
|
+
result = parseSync(virtualPath, content, { lang, sourceType: 'module' })
|
|
182
|
+
} catch {
|
|
183
|
+
return []
|
|
184
|
+
}
|
|
185
|
+
if (result.errors?.length) return []
|
|
186
|
+
|
|
187
|
+
/** @type {{ line: number, snippet: string }[]} */
|
|
188
|
+
const out = []
|
|
189
|
+
walkAstWithAncestors(result.program, [], node => {
|
|
190
|
+
if (isUnsafeQueryCallWithTemplateLiteral(node)) {
|
|
191
|
+
out.push({
|
|
192
|
+
line: offsetToLine(content, node.start),
|
|
193
|
+
snippet: normalizeSnippet(content.slice(node.start, node.end))
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
return out
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Чи сканувати цей файл за розширенням (JS/TS-сім'я).
|
|
202
|
+
* @param {string} relativePathPosix відносний шлях (posix)
|
|
203
|
+
* @returns {boolean} `true`, якщо розширення підходить для AST-скану
|
|
204
|
+
*/
|
|
205
|
+
export function isMssqlScanSourceFile(relativePathPosix) {
|
|
206
|
+
return SOURCE_FILE_RE.test(relativePathPosix) && !relativePathPosix.endsWith('.d.ts')
|
|
207
|
+
}
|
|
208
|
+
|