@nitra/cursor 1.8.147 → 1.8.151
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/n-cursor.js +1 -1
- package/bin/rename-yaml-extensions.mjs +0 -1
- package/mdc/docker.mdc +2 -0
- package/mdc/js-bun-db.mdc +34 -1
- package/mdc/js-mssql.mdc +27 -1
- package/mdc/k8s.mdc +1 -1
- package/package.json +1 -1
- package/scripts/check-capacitor.mjs +4 -4
- package/scripts/check-js-bun-db.mjs +26 -2
- package/scripts/check-js-lint.mjs +5 -1
- package/scripts/check-js-mssql.mjs +20 -2
- package/scripts/check-k8s.mjs +43 -10
- package/scripts/check-php.mjs +0 -1
- package/scripts/check-text.mjs +1 -1
- package/scripts/utils/ast-scan-utils.mjs +154 -0
- package/scripts/utils/bun-sql-scan.mjs +204 -113
- package/scripts/utils/bunyan-imports.mjs +2 -36
- package/scripts/utils/mssql-pool-scan.mjs +167 -116
- package/skills/lint/SKILL.md +1 -1
package/bin/n-cursor.js
CHANGED
|
@@ -561,7 +561,7 @@ function buildClaudeLintParallelismSectionLines() {
|
|
|
561
561
|
'## Лінт і ESLint (без паралельних запусків)',
|
|
562
562
|
'',
|
|
563
563
|
'Щоб не запускати **кілька** одночасних **`eslint`** (і не перевантажувати диск/CPU), **заборонено** стартувати `bun run lint` / `lint-js` / `eslint` **паралельно** в різних Bash-задачах, **фонових** shells чи **субагентах** (Task тощо). Має бути **один** послідовний прогон на сесію; команда **`/n-lint`** — **не** ділити на паралельні підзадачі. Деталі: `.cursor/skills/n-lint/SKILL.md`.',
|
|
564
|
-
''
|
|
564
|
+
''
|
|
565
565
|
]
|
|
566
566
|
}
|
|
567
567
|
|
package/mdc/docker.mdc
CHANGED
package/mdc/js-bun-db.mdc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Використання pg / mysql2 / Bun SQL у Node.js та Bun
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.2'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Підтримувані версії баз даних
|
|
@@ -72,6 +72,22 @@ const ids = [1, 2, 3]
|
|
|
72
72
|
await sql`SELECT * FROM users WHERE id IN ${sql(ids)}`
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
## `IN (...)`: значення з template literal — тільки через змінну + guard на пустоту
|
|
76
|
+
|
|
77
|
+
Якщо список для `IN (...)` підставляється через `${...}` у template literal, його **потрібно**:
|
|
78
|
+
|
|
79
|
+
- винести в **окрему змінну** (не підставляти вираз напряму в `${...}`);
|
|
80
|
+
- **перевірити на пустоту** перед запитом і **throw** (щоб не виконувати некоректний SQL або запит з неочікуваною семантикою).
|
|
81
|
+
|
|
82
|
+
Приклад:
|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
const ids = inputIds.map(Number).filter(n => Number.isFinite(n))
|
|
86
|
+
if (!ids.length) throw new Error('ids is empty')
|
|
87
|
+
|
|
88
|
+
await sql`SELECT * FROM users WHERE id IN ${sql(ids)}`
|
|
89
|
+
```
|
|
90
|
+
|
|
75
91
|
Транзакції — через `sql.begin` (auto-commit/rollback), вкладені — через `tx.savepoint`:
|
|
76
92
|
|
|
77
93
|
```javascript
|
|
@@ -81,6 +97,23 @@ await sql.begin(async tx => {
|
|
|
81
97
|
})
|
|
82
98
|
```
|
|
83
99
|
|
|
100
|
+
## Коментар під час виправлення SQL injection
|
|
101
|
+
|
|
102
|
+
Коли виправляєш місце з потенційним **SQL injection** (наприклад, заміна конкатенації/`.join(',')` на `sql(ids)` або перехід з `sql.unsafe(...)` на tagged template), **додай поруч короткий коментар** з описом причини.
|
|
103
|
+
|
|
104
|
+
Вимоги до коментаря:
|
|
105
|
+
|
|
106
|
+
- пояснити **що саме було небезпечно** (конкатенація, підмішування user input, динамічний `IN (...)`, тощо);
|
|
107
|
+
- пояснити **чому новий варіант безпечний** (параметризація через tagged template / `sql(...)`);
|
|
108
|
+
- без “романів”: 1–2 рядки, достатньо для ревʼю.
|
|
109
|
+
|
|
110
|
+
Приклад:
|
|
111
|
+
|
|
112
|
+
```javascript
|
|
113
|
+
// SQLi fix: не конкатенуємо значення в `IN (...)`; Bun parameterize через `sql(ids)`.
|
|
114
|
+
await sql`SELECT * FROM users WHERE id IN ${sql(ids)}`
|
|
115
|
+
```
|
|
116
|
+
|
|
84
117
|
## Що НЕ робити
|
|
85
118
|
|
|
86
119
|
### Не використовувати `sql.unsafe(...)` з конкатенацією
|
package/mdc/js-mssql.mdc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Використання mssql в nodejs
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.3'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Підтримувана версія SQL Server
|
|
@@ -17,6 +17,7 @@ version: '1.2'
|
|
|
17
17
|
## Як виконувати запити (безпечно)
|
|
18
18
|
|
|
19
19
|
tagged template треба викликати на request-обʼєкті цього пулу:
|
|
20
|
+
|
|
20
21
|
```javascript
|
|
21
22
|
javascript// db.js
|
|
22
23
|
import sql from 'mssql';
|
|
@@ -53,11 +54,29 @@ const result = await pool.request().query`
|
|
|
53
54
|
|
|
54
55
|
Ключове: pool.request().query\...`— бекті́ки післяquery`, без круглих дужок. Це той самий tagged template, тільки контекст — конкретний пул, а не глобальний.
|
|
55
56
|
|
|
57
|
+
## Коментар під час виправлення SQL injection
|
|
58
|
+
|
|
59
|
+
Коли виправляєш місце з потенційним **SQL injection** (наприклад, заміна `query(\`...\`)` на `query\`...\`` або прибирання динамічних списків/конкатенації), **додай поруч короткий коментар** з описом причини.
|
|
60
|
+
|
|
61
|
+
Вимоги до коментаря:
|
|
62
|
+
|
|
63
|
+
- вказати **що було небезпечно** (звичайна інтерполяція в рядок, конкатенація, динамічний список);
|
|
64
|
+
- вказати **чому новий варіант безпечний** (tagged template / параметризація / TVP);
|
|
65
|
+
- 1–2 рядки, без дублювання очевидного.
|
|
66
|
+
|
|
67
|
+
Приклад:
|
|
68
|
+
|
|
69
|
+
```javascript
|
|
70
|
+
// SQLi fix: query`...` (tagged template) параметризує значення; query(`...`) небезпечний через інтерполяцію.
|
|
71
|
+
await pool.request().query`SELECT * FROM users WHERE id = ${userId}`
|
|
72
|
+
```
|
|
73
|
+
|
|
56
74
|
## Що НЕ робити
|
|
57
75
|
|
|
58
76
|
### Не робити `query(\`...\`)`
|
|
59
77
|
|
|
60
78
|
javascript// ❌ Це не tagged template — це конкатенація рядка перед викликом
|
|
79
|
+
|
|
61
80
|
```javascript
|
|
62
81
|
await pool.request().query(`SELECT * FROM users WHERE id = ${userId}`);
|
|
63
82
|
// ↑ круглі дужки замість бекті́ків = звичайна інтерполяція = SQL injection
|
|
@@ -164,6 +183,11 @@ WHERE NOT EXISTS (
|
|
|
164
183
|
|
|
165
184
|
Якщо `IN (...)` все ж використовується (а не `JOIN` на TVP), значення в `${...}` **обовʼязково** мають бути попередньо приведені числовим парсером і відфільтровані від `NaN`. Це знімає будь-яку можливість SQL injection: SQL-метасимволи в `Number`/`parseInt(...)` перетворюються на `NaN` і відсіюються.
|
|
166
185
|
|
|
186
|
+
Додатково:
|
|
187
|
+
|
|
188
|
+
- значення для `IN (${...})` потрібно **винести в окрему змінну** перед запитом (не підставляти вираз напряму в `${...}`);
|
|
189
|
+
- цю змінну потрібно **перевірити на пустоту** і якщо список порожній — **throw error** (щоб не виконувати некоректний запит).
|
|
190
|
+
|
|
167
191
|
```javascript
|
|
168
192
|
// ❌ НЕ МОЖНА: значення з req.body / зовнішнього джерела без парсингу
|
|
169
193
|
const outIds = pgQ.rows.flatMap(x => x.req_body.Orders.map(o => o.OutletId))
|
|
@@ -174,9 +198,11 @@ await pool.query(/* sql */ String.raw`
|
|
|
174
198
|
|
|
175
199
|
```javascript
|
|
176
200
|
// ✅ МОЖНА: parseInt + filter(!isNaN) гарантує, що в SQL потраплять лише числа
|
|
201
|
+
// і перед запитом робимо guard на пустоту, щоб не виконувати некоректний SQL.
|
|
177
202
|
const outIds = pgQ.rows
|
|
178
203
|
.flatMap(x => x.req_body.Orders.map(o => parseInt(o.OutletId)))
|
|
179
204
|
.filter(n => !isNaN(n))
|
|
205
|
+
if (!outIds.length) throw new Error('outIds is empty')
|
|
180
206
|
await pool.request().query`
|
|
181
207
|
SELECT ... WHERE so.OutletId IN (${outIds})
|
|
182
208
|
`
|
package/mdc/k8s.mdc
CHANGED
|
@@ -314,7 +314,7 @@ data:
|
|
|
314
314
|
|
|
315
315
|
### Прод-оверрайди у `kustomization.yaml`
|
|
316
316
|
|
|
317
|
-
Оскільки `base/` (і dev-like середовища) тримають dev-значення (`1`/`1`/`0`), у **кожному** прод-накладенні `kustomization.yaml` у `patches[]` **обов'язково** має бути
|
|
317
|
+
Оскільки `base/` (і dev-like середовища) тримають dev-значення (`1`/`1`/`0`), у **кожному** прод-накладенні `kustomization.yaml` у `patches[]` **обов'язково** має бути перевизначення **лише якщо** цей оверлей наслідує base-дерево, де є **Deployment** і **HPA/PDB** (тобто base реально дає dev-like HPA/PDB, які треба підняти в проді):
|
|
318
318
|
|
|
319
319
|
- для `HorizontalPodAutoscaler`: `spec.minReplicas` **і** `spec.maxReplicas` (щоб у проді вийшло ≥2).
|
|
320
320
|
- для `PodDisruptionBudget`: `spec.minAvailable` (щоб у проді вийшло ≥1).
|
package/package.json
CHANGED
|
@@ -376,9 +376,7 @@ function extractNitraObjectBodySource(source) {
|
|
|
376
376
|
* @returns {boolean} **true**, якщо в тілі є **iosCocoaPods**…**:** **true**
|
|
377
377
|
*/
|
|
378
378
|
function nitraObjectBodyStringAllowsCocoaPodsExempt(objectBody) {
|
|
379
|
-
return (
|
|
380
|
-
RE_COCOAPODS_EXEMPT_SPM.test(objectBody) === true || RE_COCOAPODS_EXEMPT_ALLOW.test(objectBody) === true
|
|
381
|
-
)
|
|
379
|
+
return RE_COCOAPODS_EXEMPT_SPM.test(objectBody) === true || RE_COCOAPODS_EXEMPT_ALLOW.test(objectBody) === true
|
|
382
380
|
}
|
|
383
381
|
|
|
384
382
|
/**
|
|
@@ -443,7 +441,9 @@ export async function check() {
|
|
|
443
441
|
const { byPath, anyCapacitor } = acc
|
|
444
442
|
|
|
445
443
|
if (!isCapacitorRelevantForCheck(root, anyCapacitor)) {
|
|
446
|
-
pass(
|
|
444
|
+
pass(
|
|
445
|
+
'Capacitor не виявлено (без capacitor.config у корені, без @capacitor/ у package.json) — check capacitor пропущено'
|
|
446
|
+
)
|
|
447
447
|
return getExitCode()
|
|
448
448
|
}
|
|
449
449
|
|
|
@@ -20,6 +20,7 @@ import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
|
20
20
|
import {
|
|
21
21
|
findBunSqlPerRequestConnectionInText,
|
|
22
22
|
findUnsafeBunSqlDynamicSqlListInText,
|
|
23
|
+
findUnsafeBunSqlInListMissingEmptyGuardInText,
|
|
23
24
|
findUnsafeBunSqlUnsafeCallInText,
|
|
24
25
|
isBunSqlScanSourceFile,
|
|
25
26
|
textHasBunSqlImport
|
|
@@ -125,6 +126,7 @@ async function scanSourcesForBunSqlPatterns(sourcePaths, repoRoot, reporter) {
|
|
|
125
126
|
let perRequest = 0
|
|
126
127
|
let unsafeCall = 0
|
|
127
128
|
let dynamicList = 0
|
|
129
|
+
let inListGuard = 0
|
|
128
130
|
|
|
129
131
|
for (const absPath of sourcePaths) {
|
|
130
132
|
const rel = relative(repoRoot, absPath).split('\\').join('/')
|
|
@@ -154,9 +156,28 @@ async function scanSourcesForBunSqlPatterns(sourcePaths, repoRoot, reporter) {
|
|
|
154
156
|
`у IN (...) / VALUES (...); використовуй sql([...]) (js-bun-db.mdc): ${v.snippet}`
|
|
155
157
|
)
|
|
156
158
|
}
|
|
159
|
+
for (const v of findUnsafeBunSqlInListMissingEmptyGuardInText(content, rel)) {
|
|
160
|
+
inListGuard++
|
|
161
|
+
if (v.reason === 'missing_guard') {
|
|
162
|
+
fail(
|
|
163
|
+
`js-bun-db: ${rel}:${v.line} — перед IN-списком ${JSON.stringify(v.name)} потрібна перевірка на пустоту ` +
|
|
164
|
+
`з throw (наприклад if (!${v.name}.length) throw ...), інакше можливі некоректні запити (js-bun-db.mdc): ${v.snippet}`
|
|
165
|
+
)
|
|
166
|
+
} else if (v.reason === 'sql_helper_not_var') {
|
|
167
|
+
fail(
|
|
168
|
+
`js-bun-db: ${rel}:${v.line} — IN-список у \${sql(...)} має підставлятись зі змінної (Identifier) ` +
|
|
169
|
+
`після валідації на пустоту + throw (js-bun-db.mdc): ${v.snippet}`
|
|
170
|
+
)
|
|
171
|
+
} else {
|
|
172
|
+
fail(
|
|
173
|
+
`js-bun-db: ${rel}:${v.line} — значення для IN (...) у template literal треба винести в окрему змінну ` +
|
|
174
|
+
`і перевірити на пустоту (throw), не підставляти вираз напряму (js-bun-db.mdc): ${v.snippet}`
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
157
178
|
}
|
|
158
179
|
|
|
159
|
-
return { hasBunSqlImport, perRequest, unsafeCall, dynamicList }
|
|
180
|
+
return { hasBunSqlImport, perRequest, unsafeCall, dynamicList, inListGuard }
|
|
160
181
|
}
|
|
161
182
|
|
|
162
183
|
/**
|
|
@@ -188,7 +209,7 @@ export async function check() {
|
|
|
188
209
|
return reporter.getExitCode()
|
|
189
210
|
}
|
|
190
211
|
|
|
191
|
-
const { hasBunSqlImport, perRequest, unsafeCall, dynamicList } = await scanSourcesForBunSqlPatterns(
|
|
212
|
+
const { hasBunSqlImport, perRequest, unsafeCall, dynamicList, inListGuard } = await scanSourcesForBunSqlPatterns(
|
|
192
213
|
sourcePaths,
|
|
193
214
|
repoRoot,
|
|
194
215
|
reporter
|
|
@@ -208,6 +229,9 @@ export async function check() {
|
|
|
208
229
|
if (dynamicList === 0) {
|
|
209
230
|
pass("js-bun-db: немає небезпечних динамічних SQL-списків через .join(',') у IN/VALUES")
|
|
210
231
|
}
|
|
232
|
+
if (inListGuard === 0) {
|
|
233
|
+
pass('js-bun-db: усі IN-списки винесені у змінні та мають перевірку на пустоту з throw')
|
|
234
|
+
}
|
|
211
235
|
|
|
212
236
|
return reporter.getExitCode()
|
|
213
237
|
}
|
|
@@ -18,7 +18,11 @@ import { parseWorkflowYaml, verifyLintJsWorkflowStructure } from './utils/gha-wo
|
|
|
18
18
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
19
19
|
|
|
20
20
|
/** Шлях до канонічного oxlint JSON у цьому пакеті (для перевірки та тестів). */
|
|
21
|
-
export const OXLINT_CANONICAL_JSON_PATH = join(
|
|
21
|
+
export const OXLINT_CANONICAL_JSON_PATH = join(
|
|
22
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
23
|
+
'utils',
|
|
24
|
+
'oxlint-canonical.json'
|
|
25
|
+
)
|
|
22
26
|
|
|
23
27
|
/** Очікуваний локальний скрипт. */
|
|
24
28
|
export const CANONICAL_LINT_JS = 'bunx oxlint --fix && bunx eslint --fix . && bunx jscpd .'
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
findUnsafeMssqlQueryTemplateCallInText,
|
|
20
20
|
findUnsafeMssqlDynamicSqlListInText,
|
|
21
21
|
findUnsafeMssqlInListUnparsedInText,
|
|
22
|
+
findUnsafeMssqlInListMissingEmptyGuardInText,
|
|
22
23
|
isMssqlScanSourceFile
|
|
23
24
|
} from './utils/mssql-pool-scan.mjs'
|
|
24
25
|
import { walkDir } from './utils/walkDir.mjs'
|
|
@@ -203,6 +204,20 @@ function scanMssqlOneSourceFile(rel, content, counters, fail) {
|
|
|
203
204
|
`js-mssql: ${rel}:${v.line} — у SQL IN (\${...}) значення мають бути попередньо приведені числовим парсером (parseInt/Number/BigInt/parseFloat) і відфільтровані від NaN, інакше можливий SQL injection (js-mssql.mdc): ${v.snippet}`
|
|
204
205
|
)
|
|
205
206
|
}
|
|
207
|
+
for (const v of findUnsafeMssqlInListMissingEmptyGuardInText(content, rel)) {
|
|
208
|
+
counters.inListGuardViolations++
|
|
209
|
+
if (v.reason === 'missing_guard') {
|
|
210
|
+
fail(
|
|
211
|
+
`js-mssql: ${rel}:${v.line} — перед IN-списком ${JSON.stringify(v.name)} потрібна перевірка на пустоту з throw ` +
|
|
212
|
+
`(наприклад if (!${v.name}.length) throw ...), інакше можливі некоректні запити (js-mssql.mdc): ${v.snippet}`
|
|
213
|
+
)
|
|
214
|
+
} else {
|
|
215
|
+
fail(
|
|
216
|
+
`js-mssql: ${rel}:${v.line} — значення для IN (\${...}) у template literal треба винести в окрему змінну ` +
|
|
217
|
+
`і перевірити на пустоту (throw), не підставляти вираз напряму (js-mssql.mdc): ${v.snippet}`
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
206
221
|
}
|
|
207
222
|
|
|
208
223
|
/**
|
|
@@ -226,6 +241,9 @@ function reportZeroMssqlSourceViolations(counters, pass) {
|
|
|
226
241
|
if (counters.unparsedInLists === 0) {
|
|
227
242
|
pass(`js-mssql: немає підстановок IN (\${...}) без числового парсера значень`)
|
|
228
243
|
}
|
|
244
|
+
if (counters.inListGuardViolations === 0) {
|
|
245
|
+
pass('js-mssql: усі IN-списки винесені у змінні та мають перевірку на пустоту з throw')
|
|
246
|
+
}
|
|
229
247
|
}
|
|
230
248
|
|
|
231
249
|
/**
|
|
@@ -247,7 +265,8 @@ async function auditMssqlSources(repoRoot, pass, fail) {
|
|
|
247
265
|
sharedRequestViolations: 0,
|
|
248
266
|
unsafeQueryCalls: 0,
|
|
249
267
|
unsafeDynamicSqlLists: 0,
|
|
250
|
-
unparsedInLists: 0
|
|
268
|
+
unparsedInLists: 0,
|
|
269
|
+
inListGuardViolations: 0
|
|
251
270
|
}
|
|
252
271
|
for (const absPath of sourcePaths) {
|
|
253
272
|
const rel = relative(repoRoot, absPath).split('\\').join('/')
|
|
@@ -292,4 +311,3 @@ export async function check() {
|
|
|
292
311
|
|
|
293
312
|
return reporter.getExitCode()
|
|
294
313
|
}
|
|
295
|
-
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -4803,26 +4803,59 @@ function checkProdOverridesInKustomization(kust, rel, fail, passFn) {
|
|
|
4803
4803
|
}
|
|
4804
4804
|
}
|
|
4805
4805
|
|
|
4806
|
+
/**
|
|
4807
|
+
* Чи прод-оверлей **реально потребує** overrides для HPA/PDB.
|
|
4808
|
+
*
|
|
4809
|
+
* Overrides потрібні лише якщо оверлей (non-dev-like) посилається на `…/k8s/…/base` і у **base**-дереві
|
|
4810
|
+
* одночасно є:
|
|
4811
|
+
* - `Deployment` (у шарі `…/k8s/…/base/`), і
|
|
4812
|
+
* - `HorizontalPodAutoscaler` і/або `PodDisruptionBudget`.
|
|
4813
|
+
*
|
|
4814
|
+
* Тоді base зазвичай тримає dev-like значення (`1`/`1`/`0`), і прод-оверлей має їх підняти (див. k8s.mdc).
|
|
4815
|
+
* @param {string} rootNorm нормалізований корінь репозиторію
|
|
4816
|
+
* @param {string} kustAbs абсолютний шлях до kustomization.yaml
|
|
4817
|
+
* @returns {Promise<boolean>} true якщо потрібні overrides, інакше false
|
|
4818
|
+
*/
|
|
4819
|
+
export async function prodOverlayNeedsHpaPdbOverrides(rootNorm, kustAbs) {
|
|
4820
|
+
const rel = (relative(rootNorm, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
4821
|
+
const segment = k8sEnvSegmentFromRelPath(rel)
|
|
4822
|
+
if (segment === null || isDevLikeK8sEnvSegment(segment)) return false
|
|
4823
|
+
|
|
4824
|
+
const kust = await readFirstYamlObject(kustAbs)
|
|
4825
|
+
if (kust === null) return false
|
|
4826
|
+
|
|
4827
|
+
const kustDir = dirname(kustAbs)
|
|
4828
|
+
const pathRefs = resourcePathRefsFromKustomizationObject(kust)
|
|
4829
|
+
const baseDirs = await k8sBaseDirsFromKustomizeResourcePathRefs(kustDir, pathRefs, rootNorm)
|
|
4830
|
+
if (baseDirs.length === 0) return false
|
|
4831
|
+
|
|
4832
|
+
const flags = await Promise.all(
|
|
4833
|
+
baseDirs.map(bd => kustomizeResourceTreeHpaPdbDeploymentFlags(join(bd, 'kustomization.yaml'), rootNorm))
|
|
4834
|
+
)
|
|
4835
|
+
|
|
4836
|
+
return flags.some(f => f.hasDeployment && (f.hasHpa || f.hasPdb))
|
|
4837
|
+
}
|
|
4838
|
+
|
|
4806
4839
|
/**
|
|
4807
4840
|
* Для прод kustomization.yaml вимагає patches, що перевизначають **`/spec/minReplicas`** і **`/spec/maxReplicas`**
|
|
4808
|
-
* на **HorizontalPodAutoscaler**, а також **`/spec/minAvailable`** на **PodDisruptionBudget**.
|
|
4809
|
-
*
|
|
4841
|
+
* на **HorizontalPodAutoscaler**, а також **`/spec/minAvailable`** на **PodDisruptionBudget**.
|
|
4842
|
+
*
|
|
4843
|
+
* Не застосовується до dev-like (base / dev / *-qa).
|
|
4844
|
+
*
|
|
4845
|
+
* Також **не застосовується**, якщо оверлей не наслідує base з Deployment + HPA/PDB (див. `prodOverlayNeedsHpaPdbOverrides`).
|
|
4810
4846
|
* @param {string} root корінь репозиторію
|
|
4811
4847
|
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
4812
4848
|
* @param {(msg: string) => void} fail callback при помилці
|
|
4813
4849
|
* @param {(msg: string) => void} passFn callback при успіху
|
|
4814
4850
|
*/
|
|
4815
4851
|
async function validateProdKustomizationOverrides(root, yamlFilesAbs, fail, passFn) {
|
|
4852
|
+
const rootNorm = resolve(root)
|
|
4816
4853
|
const kustFiles = yamlFilesAbs.filter(abs => basename(abs) === 'kustomization.yaml')
|
|
4817
4854
|
for (const kustAbs of kustFiles) {
|
|
4818
|
-
const rel = relative(
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
if (kust !== null) {
|
|
4823
|
-
checkProdOverridesInKustomization(kust, rel, fail, passFn)
|
|
4824
|
-
}
|
|
4825
|
-
}
|
|
4855
|
+
const rel = (relative(rootNorm, kustAbs) || kustAbs).replaceAll('\\', '/')
|
|
4856
|
+
if (!(await prodOverlayNeedsHpaPdbOverrides(rootNorm, kustAbs))) continue
|
|
4857
|
+
const kust = await readFirstYamlObject(kustAbs)
|
|
4858
|
+
if (kust !== null) checkProdOverridesInKustomization(kust, rel, fail, passFn)
|
|
4826
4859
|
}
|
|
4827
4860
|
}
|
|
4828
4861
|
|
package/scripts/check-php.mjs
CHANGED
package/scripts/check-text.mjs
CHANGED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Спільні утиліти для AST-сканерів JS/TS на oxc-parser:
|
|
3
|
+
* вибір мови за розширенням, переклад зміщення в номер рядка, стиснення сніпета,
|
|
4
|
+
* обхід AST з предками, парсинг програми з безпечним поверненням `null`,
|
|
5
|
+
* розпізнавання типових вузлів (функцій, `*.join(...)`),
|
|
6
|
+
* робота з `TemplateLiteral` (текст quasis, контекст SQL-списку).
|
|
7
|
+
*
|
|
8
|
+
* Використовується файлами `bun-sql-scan.mjs`, `mssql-pool-scan.mjs` та іншими сканерами
|
|
9
|
+
* для уникнення дублювання boilerplate.
|
|
10
|
+
*/
|
|
11
|
+
import { parseSync } from 'oxc-parser'
|
|
12
|
+
|
|
13
|
+
const SQL_LIST_CONTEXT_RE = /\b(in|values)\b\s*\(/iu
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Мова для Oxc за шляхом файлу (розширення).
|
|
17
|
+
* @param {string} filePath віртуальний або реальний шлях до файлу
|
|
18
|
+
* @returns {'js' | 'jsx' | 'ts' | 'tsx'} значення опції `lang` для `parseSync`
|
|
19
|
+
*/
|
|
20
|
+
export function langFromPath(filePath) {
|
|
21
|
+
const lower = filePath.toLowerCase()
|
|
22
|
+
if (lower.endsWith('.tsx')) return 'tsx'
|
|
23
|
+
if (lower.endsWith('.ts') || lower.endsWith('.mts') || lower.endsWith('.cts')) return 'ts'
|
|
24
|
+
if (lower.endsWith('.jsx')) return 'jsx'
|
|
25
|
+
return 'js'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Номер рядка (1-based) за зміщенням у буфері.
|
|
30
|
+
* @param {string} content повний текст файлу
|
|
31
|
+
* @param {number} offset байтове зміщення початку фрагмента
|
|
32
|
+
* @returns {number} номер рядка від 1
|
|
33
|
+
*/
|
|
34
|
+
export function offsetToLine(content, offset) {
|
|
35
|
+
let line = 1
|
|
36
|
+
const n = Math.min(offset, content.length)
|
|
37
|
+
for (let i = 0; i < n; i++) {
|
|
38
|
+
if (content.codePointAt(i) === 10) line++
|
|
39
|
+
}
|
|
40
|
+
return line
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Стискає пробіли для повідомлення про порушення.
|
|
45
|
+
* @param {string} s фрагмент коду
|
|
46
|
+
* @returns {string} скорочений однорядковий рядок
|
|
47
|
+
*/
|
|
48
|
+
export function normalizeSnippet(s) {
|
|
49
|
+
return s.replaceAll(/\s+/gu, ' ').trim().slice(0, 180)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Чи є вузол функцією.
|
|
54
|
+
* @param {unknown} node AST node
|
|
55
|
+
* @returns {boolean} true, якщо це будь-який вузол-функція
|
|
56
|
+
*/
|
|
57
|
+
export function isFunctionNode(node) {
|
|
58
|
+
return (
|
|
59
|
+
!!node &&
|
|
60
|
+
typeof node === 'object' &&
|
|
61
|
+
typeof node.type === 'string' &&
|
|
62
|
+
(node.type === 'FunctionDeclaration' ||
|
|
63
|
+
node.type === 'FunctionExpression' ||
|
|
64
|
+
node.type === 'ArrowFunctionExpression')
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Рекурсивний обхід AST з предками, щоб визначати контекст (всередині функції чи ні).
|
|
70
|
+
* @param {unknown} node поточний вузол
|
|
71
|
+
* @param {unknown[]} ancestors масив предків від кореня до parent
|
|
72
|
+
* @param {(n: Record<string, unknown>, ancestors: unknown[]) => void} visit відвідувач для вузлів з `type`
|
|
73
|
+
* @returns {void}
|
|
74
|
+
*/
|
|
75
|
+
export function walkAstWithAncestors(node, ancestors, visit) {
|
|
76
|
+
if (!node || typeof node !== 'object') return
|
|
77
|
+
if (Array.isArray(node)) {
|
|
78
|
+
for (const item of node) walkAstWithAncestors(item, ancestors, visit)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const rec = /** @type {Record<string, unknown>} */ (node)
|
|
83
|
+
if (typeof rec.type === 'string') {
|
|
84
|
+
visit(rec, ancestors)
|
|
85
|
+
ancestors = [...ancestors, rec]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const key of Object.keys(node)) {
|
|
89
|
+
if (key === 'parent') continue
|
|
90
|
+
const v = rec[key]
|
|
91
|
+
if (v && typeof v === 'object') {
|
|
92
|
+
walkAstWithAncestors(v, ancestors, visit)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Парсить файл і повертає `program` або null, якщо є синтаксичні помилки чи виняток.
|
|
99
|
+
* @param {string} content вихідний код
|
|
100
|
+
* @param {string} virtualPath шлях для вибору `lang` (також для діагностики)
|
|
101
|
+
* @returns {unknown | null} `result.program` або null, якщо парсинг не вдався
|
|
102
|
+
*/
|
|
103
|
+
export function parseProgramOrNull(content, virtualPath) {
|
|
104
|
+
const lang = langFromPath(virtualPath || 'scan.ts')
|
|
105
|
+
let result
|
|
106
|
+
try {
|
|
107
|
+
result = parseSync(virtualPath || 'scan.ts', content, { lang, sourceType: 'module' })
|
|
108
|
+
} catch {
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
if (result.errors?.length) return null
|
|
112
|
+
return result.program
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Чи це `.join(...)` виклик (типово для динамічних списків у SQL).
|
|
117
|
+
* @param {unknown} node AST node
|
|
118
|
+
* @returns {boolean} true, якщо це CallExpression `*.join(...)`
|
|
119
|
+
*/
|
|
120
|
+
export function isJoinCall(node) {
|
|
121
|
+
if (!node || node.type !== 'CallExpression') return false
|
|
122
|
+
const callee = node.callee
|
|
123
|
+
if (!callee || callee.type !== 'MemberExpression' || callee.computed) return false
|
|
124
|
+
const prop = callee.property
|
|
125
|
+
return !!prop && prop.type === 'Identifier' && prop.name === 'join'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Текст quasis у TemplateLiteral (без expressions).
|
|
130
|
+
* @param {unknown} template TemplateLiteral
|
|
131
|
+
* @returns {string} обʼєднаний raw-текст
|
|
132
|
+
*/
|
|
133
|
+
export function templateQuasisText(template) {
|
|
134
|
+
if (!template || template.type !== 'TemplateLiteral') return ''
|
|
135
|
+
const quasis = template.quasis
|
|
136
|
+
if (!Array.isArray(quasis) || quasis.length === 0) return ''
|
|
137
|
+
let out = ''
|
|
138
|
+
for (const q of quasis) {
|
|
139
|
+
if (!q || typeof q !== 'object') continue
|
|
140
|
+
const value = q.value
|
|
141
|
+
if (!value || typeof value !== 'object') continue
|
|
142
|
+
if (typeof value.raw === 'string') out += value.raw
|
|
143
|
+
}
|
|
144
|
+
return out
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Чи виглядає TemplateLiteral як SQL-контекст зі списком (IN/VALUES (...)).
|
|
149
|
+
* @param {unknown} template TemplateLiteral
|
|
150
|
+
* @returns {boolean} true, якщо текст містить `IN (` або `VALUES (`
|
|
151
|
+
*/
|
|
152
|
+
export function isSqlListContextTemplate(template) {
|
|
153
|
+
return SQL_LIST_CONTEXT_RE.test(templateQuasisText(template))
|
|
154
|
+
}
|