@nitra/cursor 1.8.141 → 1.8.143
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/mdc/docker.mdc +3 -3
- package/mdc/js-mssql.mdc +107 -3
- package/mdc/vue.mdc +41 -0
- package/package.json +1 -1
- package/scripts/check-docker.mjs +42 -46
- package/scripts/check-js-mssql.mjs +22 -0
- package/scripts/check-vue.mjs +10 -0
- package/scripts/utils/mssql-pool-scan.mjs +149 -0
package/mdc/docker.mdc
CHANGED
|
@@ -9,7 +9,7 @@ alwaysApply: false
|
|
|
9
9
|
|
|
10
10
|
[hadolint](https://github.com/hadolint/hadolint) перевіряє Dockerfile на типові помилки та рекомендації (`FROM`, `RUN`, `COPY`, shell form тощо).
|
|
11
11
|
|
|
12
|
-
Для образів з Docker Hub — **`oven/bun`**, **`alpine`**, **`nginx`**, **`node`** — у **`FROM`** треба вказувати дзеркало GCR, а не pull напряму з Hub: **`mirror.gcr.io/oven/bun`**, **`mirror.gcr.io/library/alpine`**, **`mirror.gcr.io/
|
|
12
|
+
Для образів з Docker Hub — **`oven/bun`**, **`alpine`**, **`nginxinc/nginx-unprivileged`**, **`node`** — у **`FROM`** треба вказувати дзеркало GCR, а не pull напряму з Hub: **`mirror.gcr.io/oven/bun`**, **`mirror.gcr.io/library/alpine`**, **`mirror.gcr.io/nginxinc/nginx-unprivileged`**, **`mirror.gcr.io/library/node`**. Перевіряє **`check-docker.mjs`**, деталі в **`npm/scripts/utils/docker-mirror.mjs`**.
|
|
13
13
|
|
|
14
14
|
Також Dockerfile/Containerfile **має бути multistage build**: окремий build stage (залежності/компіляція) і окремий runtime stage. У фінальному stage дозволені лише мінімальні базові образи:
|
|
15
15
|
|
|
@@ -17,13 +17,13 @@ alwaysApply: false
|
|
|
17
17
|
- **ультра-легкі (glibc / одна статична збірка)**: `scratch` — тільки як `FROM scratch` (офіційний порожній базовий шар), коли весь **runtime** уже в `COPY --from=…`
|
|
18
18
|
- **glibc, Debian (slim)**: `mirror.gcr.io/library/debian:*` **лише** з тегом, у якому є `slim` (наприклад `bookworm-slim`, `trixie-slim`), а не `bookworm` без `slim`
|
|
19
19
|
- **виняток (інтерпретовані стеки)**: `mirror.gcr.io/library/php:*` або `mirror.gcr.io/library/python:*` — якщо сервіс має крутитися в офіційному runtime PHP чи Python, а не як один бінарник на Alpine; інакше лишай **alpine** у фінальному stage
|
|
20
|
-
- **frontend**: `mirror.gcr.io/
|
|
20
|
+
- **frontend**: `mirror.gcr.io/nginxinc/nginx-unprivileged:*` або `mirror.gcr.io/openresty/openresty:*`
|
|
21
21
|
|
|
22
22
|
Це стримує зайвий build tooling (Bun, **node_modules** зі збірки) у фінальному образі; для **alpine** / **nginx** / **openresty** у **runtime** лишаються лише відповідні вимоги, для **php** / **python** (виняток) — цільовий інтерпретований **stack**; **scratch** і **debian** з тегом **`*slim*`** — коли glibc і мінімальне оточення Debian важливіші за musl в **alpine**.
|
|
23
23
|
|
|
24
24
|
## Мінімальні образи
|
|
25
25
|
|
|
26
|
-
Якщо використовується nginx, то
|
|
26
|
+
Якщо використовується nginx, то **використовуй** `mirror.gcr.io/nginxinc/nginx-unprivileged:alpine-slim` (і не `latest` / `alpine`).
|
|
27
27
|
|
|
28
28
|
## компіляція
|
|
29
29
|
|
package/mdc/js-mssql.mdc
CHANGED
|
@@ -4,11 +4,18 @@ alwaysApply: true
|
|
|
4
4
|
version: '1.1'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
## Підтримувана версія SQL Server
|
|
8
|
+
|
|
9
|
+
Правило орієнтоване на **SQL Server 2019**.
|
|
10
|
+
|
|
11
|
+
## Версія пакета `mssql`
|
|
12
|
+
|
|
13
|
+
Якщо в проекті в будь-якому `package.json` в секції `dependencies` присутній пакет **`mssql`**, то його версія повинна бути не менше **12.5.0**.
|
|
9
14
|
|
|
10
15
|
Потрібно використовувати connection pool (sql.ConnectionPool) як singleton, а НЕ створювати підключення на кожен запит.
|
|
11
16
|
|
|
17
|
+
## Як виконувати запити (безпечно)
|
|
18
|
+
|
|
12
19
|
tagged template треба викликати на request-обʼєкті цього пулу:
|
|
13
20
|
```javascript
|
|
14
21
|
javascript// db.js
|
|
@@ -46,7 +53,10 @@ const result = await pool.request().query`
|
|
|
46
53
|
|
|
47
54
|
Ключове: pool.request().query\...`— бекті́ки післяquery`, без круглих дужок. Це той самий tagged template, тільки контекст — конкретний пул, а не глобальний.
|
|
48
55
|
|
|
49
|
-
Що НЕ робити
|
|
56
|
+
## Що НЕ робити
|
|
57
|
+
|
|
58
|
+
### Не робити `query(\`...\`)`
|
|
59
|
+
|
|
50
60
|
javascript// ❌ Це не tagged template — це конкатенація рядка перед викликом
|
|
51
61
|
```javascript
|
|
52
62
|
await pool.request().query(`SELECT * FROM users WHERE id = ${userId}`);
|
|
@@ -56,4 +66,98 @@ await pool.request().query(`SELECT * FROM users WHERE id = ${userId}`);
|
|
|
56
66
|
Різниця між query\...`іquery(`...`)` — критична. Перше безпечне, друге — діра.
|
|
57
67
|
Потрібно використовувати перше. Потрібно шукати в коді друге і заміняти на перше.
|
|
58
68
|
|
|
69
|
+
### Не шарити `Request` між запитами
|
|
70
|
+
|
|
71
|
+
Заборонено робити singleton `request` на рівні модуля на кшталт:
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
// ❌ НЕ МОЖНА
|
|
75
|
+
export const request = pool.request();
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`Request` має створюватися **щоразу заново**:
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
// ✅ МОЖНА
|
|
82
|
+
const request = pool.request();
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Це особливо важливо для **TVP** (табличні параметри) та будь-яких `.input(...)`.
|
|
86
|
+
|
|
87
|
+
## TVP дозволений і рекомендований (SQL Server 2019)
|
|
88
|
+
|
|
89
|
+
Для списків значень та пар ключів **найкращий** шлях по безпеці/продуктивності — **TVP** (table-valued parameters).
|
|
90
|
+
|
|
91
|
+
### 1) `IN (...)` для кодів → `JOIN` на TVP
|
|
92
|
+
|
|
93
|
+
Замість складання SQL-рядка:
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
// ❌ НЕ МОЖНА: динамічний список у SQL
|
|
97
|
+
await pool.request().query`
|
|
98
|
+
SELECT *
|
|
99
|
+
FROM promo.SomeTable t
|
|
100
|
+
WHERE t.ExternalCode IN (${codes.map(c => `'${c}'`).join(',')})
|
|
101
|
+
`;
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Роби TVP з 1 колонкою:
|
|
105
|
+
|
|
106
|
+
- створити `new sql.Table()`
|
|
107
|
+
- `columns.add('ExternalCode', sql.NVarChar(N))`
|
|
108
|
+
- `rows.add(code)` після `trim()` + валідації
|
|
109
|
+
- `request.input('codes', table)`
|
|
110
|
+
- в SQL: `JOIN @codes c ON c.ExternalCode = t.ExternalCode`
|
|
111
|
+
|
|
112
|
+
Плюси:
|
|
113
|
+
|
|
114
|
+
- 0% SQL injection
|
|
115
|
+
- текст SQL **не росте** від довжини масиву
|
|
116
|
+
- SQL Server часто оптимізує `JOIN` краще, ніж гігантський `IN (...)`
|
|
117
|
+
|
|
118
|
+
### 2) `DELETE ... VALUES (...)` / `MERGE ... VALUES (...)` → TVP з 2 колонками
|
|
119
|
+
|
|
120
|
+
Замість генерації списку рядків `(...),(...),...` у SQL (навіть якщо воно “через tagged template”):
|
|
121
|
+
|
|
122
|
+
- сформуй TVP-таблицю `@Pairs` з колонками:
|
|
123
|
+
- `PromoActivitiesId` (INT або BIGINT — як у схемі БД)
|
|
124
|
+
- `SupplierOutletId` (INT або BIGINT — як у схемі БД)
|
|
125
|
+
|
|
126
|
+
Delete:
|
|
127
|
+
|
|
128
|
+
```sql
|
|
129
|
+
DELETE abi
|
|
130
|
+
FROM promo.ActivitiesByOutlet abi
|
|
131
|
+
JOIN @Pairs p
|
|
132
|
+
ON p.PromoActivitiesId = abi.PromoActivitiesId
|
|
133
|
+
AND p.SupplierOutletId = abi.SupplierOutletId;
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Insert (без `MERGE`, простіше і безпечніше):
|
|
137
|
+
|
|
138
|
+
```sql
|
|
139
|
+
INSERT INTO promo.ActivitiesByOutlet (PromoActivitiesId, SupplierOutletId, Status)
|
|
140
|
+
SELECT p.PromoActivitiesId, p.SupplierOutletId, 2
|
|
141
|
+
FROM @Pairs p
|
|
142
|
+
WHERE NOT EXISTS (
|
|
143
|
+
SELECT 1
|
|
144
|
+
FROM promo.ActivitiesByOutlet abi
|
|
145
|
+
WHERE abi.PromoActivitiesId = p.PromoActivitiesId
|
|
146
|
+
AND abi.SupplierOutletId = p.SupplierOutletId
|
|
147
|
+
);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Плюси:
|
|
151
|
+
|
|
152
|
+
- не збираєш SQL-рядок із даних
|
|
153
|
+
- менше шансів упіймати edge-case `MERGE` (у SQL Server історично багато “сюрпризів”)
|
|
154
|
+
|
|
155
|
+
## Мінімальна валідація перед наповненням TVP
|
|
156
|
+
|
|
157
|
+
Навіть з TVP потрібно:
|
|
158
|
+
|
|
159
|
+
- `ExternalCode`: `typeof === 'string'`, `trim()`, довжина `<= N` (під схему), відкинути пусті.
|
|
160
|
+
- ліміт на кількість елементів (наприклад 5k/10k — залежить від вашого потоку).
|
|
161
|
+
- `supplierId`/ID: число/BigInt, валідне та скінченне.
|
|
162
|
+
|
|
59
163
|
Перевірка: `npx @nitra/cursor check js-mssql`.
|
package/mdc/vue.mdc
CHANGED
|
@@ -221,6 +221,47 @@ export default defineConfig({
|
|
|
221
221
|
|
|
222
222
|
Потрібно використовувати vite-plugin-vue-layouts-next для автоматичного імпортування layout компонентів.
|
|
223
223
|
|
|
224
|
+
## npm_lifecycle_event
|
|
225
|
+
|
|
226
|
+
у більшості проектів в файлі vite.config.js
|
|
227
|
+
є конструкція виду
|
|
228
|
+
|
|
229
|
+
switch (process.env.npm_lifecycle_event) {
|
|
230
|
+
case 'start-remote-tr': {
|
|
231
|
+
|
|
232
|
+
вона перестала працювати з новим Bun (і за цього не буде працювати bun start-remote-dev та інші)
|
|
233
|
+
|
|
234
|
+
і тепер її нада обрамити в функцію, наприклад
|
|
235
|
+
|
|
236
|
+
```javascript title="vite.config.js"
|
|
237
|
+
function getProxy(mode) {
|
|
238
|
+
const proxy = {}
|
|
239
|
+
|
|
240
|
+
switch (mode) {
|
|
241
|
+
case 'remote-tr': {
|
|
242
|
+
proxy['^/auth/.*'] = 'https://tr.efes.cloud'
|
|
243
|
+
proxy['/file-link/'] = 'https://tr.efes.cloud'
|
|
244
|
+
break
|
|
245
|
+
}
|
|
246
|
+
default: {
|
|
247
|
+
proxy['^/auth/.*'] = 'https://dev.efes.cloud'
|
|
248
|
+
proxy['/file-link/'] = 'https://dev.efes.cloud'
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return proxy
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
і викликати всередині
|
|
256
|
+
|
|
257
|
+
```javascript title="vite.config.js"
|
|
258
|
+
export default defineConfig(({ mode, command }) => {
|
|
259
|
+
...
|
|
260
|
+
server: {
|
|
261
|
+
proxy: getProxy(mode)
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
224
265
|
## Перевірка
|
|
225
266
|
|
|
226
267
|
`npx @nitra/cursor check vue` — перевіряє залежності, `vite.config`, а також обходить джерела Vue-пакета (`.vue`, `.ts`, `.js` тощо) на заборонені value-імпорти з модуля `vue`; дозволені лише type-only та side-effect `import 'vue'`. Імпорти аналізуються через **oxc-parser** (`module.staticImports`); для `.vue` вміст `<script>` витягується з SFC, далі той самий парсер (логіка в `npm/scripts/utils/vue-forbidden-imports.mjs`).
|
package/package.json
CHANGED
package/scripts/check-docker.mjs
CHANGED
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
* - backend: `mirror.gcr.io/library/alpine:*`, `scratch`, `mirror.gcr.io/library/debian:` з тегом, що
|
|
10
10
|
* містить `slim` (не повний `debian:bookworm`), за винятком PHP/Python — `mirror.gcr.io/library/php:*` або
|
|
11
11
|
* `mirror.gcr.io/library/python:*`
|
|
12
|
-
* - frontend: `mirror.gcr.io/nginxinc/nginx-unprivileged
|
|
13
|
-
* `mirror.gcr.io/openresty/openresty:*`
|
|
12
|
+
* - frontend: `mirror.gcr.io/nginxinc/nginx-unprivileged:*`, `mirror.gcr.io/openresty/openresty:*`
|
|
14
13
|
*
|
|
15
14
|
* Якщо в Dockerfile є крок `bun install` і це не frontend-образ (фінальний stage — alpine),
|
|
16
15
|
* то очікується компіляція в один бінарник через `bun build --compile` у build stage, а у
|
|
@@ -19,8 +18,8 @@
|
|
|
19
18
|
* Мета — щоб у фінальному образі не було build tooling (Bun/Node та залежностей), а лише
|
|
20
19
|
* дозволений runtime (alpine, scratch, debian slim, за потреби php/python, nginx або openresty).
|
|
21
20
|
*
|
|
22
|
-
* Для nginx-образів (`mirror.gcr.io/nginxinc/nginx-unprivileged
|
|
23
|
-
*
|
|
21
|
+
* Для nginx-образів (`mirror.gcr.io/nginxinc/nginx-unprivileged`) у будь-якому `FROM` очікується
|
|
22
|
+
* тег `alpine-slim` (docker.mdc: мінімальні образи), не `latest` /
|
|
24
23
|
* `alpine` / інші. `nginx-unprivileged` запускається від не-root користувача (uid=101) без явного
|
|
25
24
|
* `USER` у Dockerfile — перевірка non-root для нього пропускається.
|
|
26
25
|
*
|
|
@@ -42,7 +41,6 @@ const BUN_BUILD_COMPILE_RE = /\bbun\s+build\b[^\n]*\s--compile\b/iu
|
|
|
42
41
|
const BUN_WORD_RE = /\bbun\b/iu
|
|
43
42
|
const USER_LINE_RE = /^\s*USER\s+([^\s#]+)/iu
|
|
44
43
|
|
|
45
|
-
const NGINX_MIRROR_PREFIX = 'mirror.gcr.io/library/nginx'
|
|
46
44
|
const NGINX_UNPRIVILEGED_MIRROR_PREFIX = 'mirror.gcr.io/nginxinc/nginx-unprivileged'
|
|
47
45
|
|
|
48
46
|
/**
|
|
@@ -97,7 +95,6 @@ const RUNTIME_IMAGES = /** @type {const} */ ([
|
|
|
97
95
|
'mirror.gcr.io/library/alpine',
|
|
98
96
|
'mirror.gcr.io/library/php',
|
|
99
97
|
'mirror.gcr.io/library/python',
|
|
100
|
-
'mirror.gcr.io/library/nginx',
|
|
101
98
|
'mirror.gcr.io/nginxinc/nginx-unprivileged',
|
|
102
99
|
'mirror.gcr.io/openresty/openresty'
|
|
103
100
|
])
|
|
@@ -179,7 +176,6 @@ export function getMultistageAndRuntimeHint(fileContent) {
|
|
|
179
176
|
* Очікування:
|
|
180
177
|
* - у build stage є `bun build --compile`;
|
|
181
178
|
* - у фінальному stage немає викликів `bun` (залишків build tooling).
|
|
182
|
-
*
|
|
183
179
|
* @param {string} fileContent вміст Dockerfile/Containerfile
|
|
184
180
|
* @returns {string | null} повідомлення помилки або null
|
|
185
181
|
*/
|
|
@@ -194,7 +190,6 @@ export function getBunCompileHint(fileContent) {
|
|
|
194
190
|
const hasBunInstall = BUN_INSTALL_RE.test(fileContent)
|
|
195
191
|
const isFinalAlpine = lastLower.startsWith('mirror.gcr.io/library/alpine:')
|
|
196
192
|
const isFinalFrontend =
|
|
197
|
-
lastLower.startsWith('mirror.gcr.io/library/nginx:') ||
|
|
198
193
|
lastLower.startsWith(`${NGINX_UNPRIVILEGED_MIRROR_PREFIX}:`) ||
|
|
199
194
|
lastLower.startsWith('mirror.gcr.io/openresty/openresty:')
|
|
200
195
|
|
|
@@ -216,14 +211,13 @@ export function getBunCompileHint(fileContent) {
|
|
|
216
211
|
}
|
|
217
212
|
|
|
218
213
|
/**
|
|
219
|
-
* Перевіряє, що для nginx-образів (`mirror.gcr.io/nginxinc/nginx-unprivileged`
|
|
220
|
-
*
|
|
221
|
-
*
|
|
214
|
+
* Перевіряє, що для nginx-образів (`mirror.gcr.io/nginxinc/nginx-unprivileged`) у `FROM` вказано
|
|
215
|
+
* тег `alpine-slim` (docker.mdc).
|
|
222
216
|
* @param {string} fileContent вміст Dockerfile/Containerfile
|
|
223
217
|
* @returns {string | null} повідомлення помилки або null
|
|
224
218
|
*/
|
|
225
219
|
export function getNginxAlpineSlimTagHint(fileContent) {
|
|
226
|
-
const prefixes = [NGINX_UNPRIVILEGED_MIRROR_PREFIX
|
|
220
|
+
const prefixes = [NGINX_UNPRIVILEGED_MIRROR_PREFIX]
|
|
227
221
|
for (const { line, image } of parseFromStages(fileContent)) {
|
|
228
222
|
const noDigest = (image.split('@')[0] || '').trim()
|
|
229
223
|
const d = noDigest.toLowerCase()
|
|
@@ -247,7 +241,6 @@ export function getNginxAlpineSlimTagHint(fileContent) {
|
|
|
247
241
|
* Очікування:
|
|
248
242
|
* - у фінальному stage має бути інструкція `USER <name|uid>`;
|
|
249
243
|
* - користувач не має бути `root` і не має бути `0`.
|
|
250
|
-
*
|
|
251
244
|
* @param {string} fileContent вміст Dockerfile/Containerfile
|
|
252
245
|
* @returns {string | null} повідомлення помилки або null
|
|
253
246
|
*/
|
|
@@ -289,7 +282,7 @@ export function getNonRootRuntimeHint(fileContent) {
|
|
|
289
282
|
*/
|
|
290
283
|
export async function check() {
|
|
291
284
|
const reporter = createCheckReporter()
|
|
292
|
-
const { pass
|
|
285
|
+
const { pass } = reporter
|
|
293
286
|
|
|
294
287
|
const root = process.cwd()
|
|
295
288
|
const files = await findDockerfilePaths(root)
|
|
@@ -302,42 +295,45 @@ export async function check() {
|
|
|
302
295
|
pass(`Знайдено файлів для hadolint: ${files.length}`)
|
|
303
296
|
|
|
304
297
|
for (const abs of files) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const hint = getMirrorGcrHint(content)
|
|
308
|
-
if (hint) {
|
|
309
|
-
fail(`${rel} (mirror.gcr.io): ${hint}`)
|
|
310
|
-
}
|
|
298
|
+
await checkDockerfile(reporter, root, abs)
|
|
299
|
+
}
|
|
311
300
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
fail(`${rel} (multistage): ${multistageHint}`)
|
|
315
|
-
}
|
|
301
|
+
return reporter.getExitCode()
|
|
302
|
+
}
|
|
316
303
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
304
|
+
/**
|
|
305
|
+
* Перевіряє один Dockerfile/Containerfile: mirror.gcr.io, multistage/runtime, compile/non-root/nginx tag і hadolint.
|
|
306
|
+
* @param {ReturnType<typeof createCheckReporter>} reporter репортер перевірок
|
|
307
|
+
* @param {string} root корінь репозиторію
|
|
308
|
+
* @param {string} abs абсолютний шлях до Dockerfile/Containerfile
|
|
309
|
+
* @returns {Promise<void>}
|
|
310
|
+
*/
|
|
311
|
+
async function checkDockerfile(reporter, root, abs) {
|
|
312
|
+
const { pass, fail } = reporter
|
|
313
|
+
const rel = posixRel(root, abs) || basename(abs)
|
|
314
|
+
const content = await readFile(abs, 'utf8')
|
|
321
315
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
fail(`${rel} (non-root): ${nonRootHint}`)
|
|
325
|
-
}
|
|
316
|
+
const hint = getMirrorGcrHint(content)
|
|
317
|
+
if (hint) fail(`${rel} (mirror.gcr.io): ${hint}`)
|
|
326
318
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
fail(`${rel} (nginx tag): ${nginxSlimHint}`)
|
|
330
|
-
}
|
|
319
|
+
const multistageHint = getMultistageAndRuntimeHint(content)
|
|
320
|
+
if (multistageHint) fail(`${rel} (multistage): ${multistageHint}`)
|
|
331
321
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (ok) {
|
|
335
|
-
pass(`${rel} (${via})`)
|
|
336
|
-
} else {
|
|
337
|
-
const detail = tail ? `:\n${tail}` : ''
|
|
338
|
-
fail(`${rel} (${via})${detail}`)
|
|
339
|
-
}
|
|
340
|
-
}
|
|
322
|
+
const compileHint = getBunCompileHint(content)
|
|
323
|
+
if (compileHint) fail(`${rel} (compile): ${compileHint}`)
|
|
341
324
|
|
|
342
|
-
|
|
325
|
+
const nonRootHint = getNonRootRuntimeHint(content)
|
|
326
|
+
if (nonRootHint) fail(`${rel} (non-root): ${nonRootHint}`)
|
|
327
|
+
|
|
328
|
+
const nginxSlimHint = getNginxAlpineSlimTagHint(content)
|
|
329
|
+
if (nginxSlimHint) fail(`${rel} (nginx tag): ${nginxSlimHint}`)
|
|
330
|
+
|
|
331
|
+
const { ok, stdout, stderr, via } = lintDockerfileWithHadolint(root, abs)
|
|
332
|
+
const tail = (stdout + stderr).trim()
|
|
333
|
+
if (ok) {
|
|
334
|
+
pass(`${rel} (${via})`)
|
|
335
|
+
} else {
|
|
336
|
+
const detail = tail ? `:\n${tail}` : ''
|
|
337
|
+
fail(`${rel} (${via})${detail}`)
|
|
338
|
+
}
|
|
343
339
|
}
|
|
@@ -15,7 +15,9 @@ import { join, relative, sep } from 'node:path'
|
|
|
15
15
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
16
16
|
import {
|
|
17
17
|
findMssqlPerRequestConnectionInText,
|
|
18
|
+
findSharedMssqlRequestInText,
|
|
18
19
|
findUnsafeMssqlQueryTemplateCallInText,
|
|
20
|
+
findUnsafeMssqlDynamicSqlListInText,
|
|
19
21
|
isMssqlScanSourceFile
|
|
20
22
|
} from './utils/mssql-pool-scan.mjs'
|
|
21
23
|
import { walkDir } from './utils/walkDir.mjs'
|
|
@@ -172,7 +174,9 @@ export async function check() {
|
|
|
172
174
|
}
|
|
173
175
|
|
|
174
176
|
let violations = 0
|
|
177
|
+
let sharedRequestViolations = 0
|
|
175
178
|
let unsafeQueryCalls = 0
|
|
179
|
+
let unsafeDynamicSqlLists = 0
|
|
176
180
|
for (const absPath of sourcePaths) {
|
|
177
181
|
const rel = relative(repoRoot, absPath).split('\\').join('/')
|
|
178
182
|
const content = await readFile(absPath, 'utf8')
|
|
@@ -182,20 +186,38 @@ export async function check() {
|
|
|
182
186
|
`js-mssql: ${rel}:${v.line} — не створюй new sql.ConnectionPool(...) на кожен запит; використовуй singleton sql.ConnectionPool: ${v.snippet}`
|
|
183
187
|
)
|
|
184
188
|
}
|
|
189
|
+
for (const v of findSharedMssqlRequestInText(content, rel)) {
|
|
190
|
+
sharedRequestViolations++
|
|
191
|
+
fail(
|
|
192
|
+
`js-mssql: ${rel}:${v.line} — заборонено шарити Request (наприклад export const request = pool.request()); створюй pool.request() щоразу заново (js-mssql.mdc): ${v.snippet}`
|
|
193
|
+
)
|
|
194
|
+
}
|
|
185
195
|
for (const v of findUnsafeMssqlQueryTemplateCallInText(content, rel)) {
|
|
186
196
|
unsafeQueryCalls++
|
|
187
197
|
fail(
|
|
188
198
|
`js-mssql: ${rel}:${v.line} — заборонено query(\`...\`): це не tagged template; використовуй pool.request().query\`...\` (js-mssql.mdc): ${v.snippet}`
|
|
189
199
|
)
|
|
190
200
|
}
|
|
201
|
+
for (const v of findUnsafeMssqlDynamicSqlListInText(content, rel)) {
|
|
202
|
+
unsafeDynamicSqlLists++
|
|
203
|
+
fail(
|
|
204
|
+
`js-mssql: ${rel}:${v.line} — заборонено підставляти у SQL динамічні списки через .join(',') (типово IN (...) / VALUES (...)); використовуй TVP (sql.Table) + JOIN/INSERT (js-mssql.mdc): ${v.snippet}`
|
|
205
|
+
)
|
|
206
|
+
}
|
|
191
207
|
}
|
|
192
208
|
|
|
193
209
|
if (violations === 0) {
|
|
194
210
|
pass('js-mssql: немає створення new sql.ConnectionPool(...) всередині функцій (singleton pool)')
|
|
195
211
|
}
|
|
212
|
+
if (sharedRequestViolations === 0) {
|
|
213
|
+
pass('js-mssql: немає shared Request (export const request = pool.request())')
|
|
214
|
+
}
|
|
196
215
|
if (unsafeQueryCalls === 0) {
|
|
197
216
|
pass('js-mssql: немає небезпечних викликів query(`...`) (потрібно query`...`)')
|
|
198
217
|
}
|
|
218
|
+
if (unsafeDynamicSqlLists === 0) {
|
|
219
|
+
pass("js-mssql: немає небезпечних динамічних SQL-списків через .join(',') у IN/VALUES")
|
|
220
|
+
}
|
|
199
221
|
}
|
|
200
222
|
|
|
201
223
|
return reporter.getExitCode()
|
package/scripts/check-vue.mjs
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* Версії Vite та плагінів, vue-macros, auto-import, layouts, вміст `vite.config`;
|
|
5
5
|
* у репозиторії — рекомендацію розширення Vue.volar.
|
|
6
6
|
*
|
|
7
|
+
* У `vite.config.*` заборонено використовувати `process.env.npm_lifecycle_event` (Bun не підставляє його як npm),
|
|
8
|
+
* натомість використовуй `mode` з `defineConfig(({ mode }) => ...)`.
|
|
9
|
+
*
|
|
7
10
|
* Заборонені явні value-імпорти з `vue` у джерелах пакета — сканування `.vue`/`.ts`/`.js` тощо
|
|
8
11
|
* через **oxc-parser** (`module.staticImports`; див. `utils/vue-forbidden-imports.mjs`); дозволені лише type-only та side-effect `import 'vue'`.
|
|
9
12
|
*/
|
|
@@ -115,6 +118,13 @@ async function checkViteConfig(rootDir, prefix, passFn, fail) {
|
|
|
115
118
|
fail(`${prefix}${err}`)
|
|
116
119
|
}
|
|
117
120
|
}
|
|
121
|
+
|
|
122
|
+
if (content.includes('process.env.npm_lifecycle_event')) {
|
|
123
|
+
fail(
|
|
124
|
+
`${prefix}${viteConfig} використовує process.env.npm_lifecycle_event — у Bun це не працює. ` +
|
|
125
|
+
`Перенеси логіку на mode (defineConfig(({ mode }) => ...)) і передавай mode в helper-функції.`
|
|
126
|
+
)
|
|
127
|
+
}
|
|
118
128
|
}
|
|
119
129
|
|
|
120
130
|
/**
|
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
* виклик з інтерполяцією рядка, який може призвести до SQL injection. Натомість має
|
|
8
8
|
* використовуватись tagged template `query\`...\`` (див. js-mssql.mdc).
|
|
9
9
|
*
|
|
10
|
+
* Додатково знаходить:
|
|
11
|
+
* - shared `Request` (наприклад `export const request = pool.request()`), який не можна
|
|
12
|
+
* повторно використовувати між запитами.
|
|
13
|
+
* - небезпечні “динамічні списки” в SQL, коли в TemplateLiteral/TaggedTemplateExpression
|
|
14
|
+
* підставляють рядки, зібрані через `.join(',')` (типово для `IN (...)` або `VALUES (...)`).
|
|
15
|
+
*
|
|
10
16
|
* Семантика береться з **oxc-parser** по AST, щоб не покладатися на regex.
|
|
11
17
|
* Якщо файл не парситься або містить синтаксичні помилки — повертаємо порожній
|
|
12
18
|
* результат (спочатку треба полагодити синтаксис, потім перезапустити перевірку).
|
|
@@ -14,6 +20,7 @@
|
|
|
14
20
|
import { parseSync } from 'oxc-parser'
|
|
15
21
|
|
|
16
22
|
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/
|
|
23
|
+
const SQL_LIST_CONTEXT_RE = /\b(in|values)\b\s*\(/iu
|
|
17
24
|
|
|
18
25
|
/**
|
|
19
26
|
* Мова для Oxc за шляхом файлу (розширення).
|
|
@@ -134,6 +141,62 @@ function isUnsafeQueryCallWithTemplateLiteral(node) {
|
|
|
134
141
|
return !!first && typeof first === 'object' && first.type === 'TemplateLiteral'
|
|
135
142
|
}
|
|
136
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Чи це `something.request()` (наприклад `pool.request()`), яку не можна шарити між запитами.
|
|
146
|
+
* @param {unknown} node AST node
|
|
147
|
+
* @returns {boolean} true, якщо це CallExpression з `.request()`
|
|
148
|
+
*/
|
|
149
|
+
function isRequestFactoryCall(node) {
|
|
150
|
+
if (!node || node.type !== 'CallExpression') return false
|
|
151
|
+
const callee = node.callee
|
|
152
|
+
if (!callee || callee.type !== 'MemberExpression') return false
|
|
153
|
+
if (callee.computed) return false
|
|
154
|
+
const prop = callee.property
|
|
155
|
+
return !!prop && prop.type === 'Identifier' && prop.name === 'request'
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Чи це `.join(...)` виклик (часто використовується для динамічних списків в SQL).
|
|
160
|
+
* @param {unknown} node AST node
|
|
161
|
+
* @returns {boolean} true, якщо це CallExpression `*.join(...)`
|
|
162
|
+
*/
|
|
163
|
+
function isJoinCall(node) {
|
|
164
|
+
if (!node || node.type !== 'CallExpression') return false
|
|
165
|
+
const callee = node.callee
|
|
166
|
+
if (!callee || callee.type !== 'MemberExpression') return false
|
|
167
|
+
if (callee.computed) return false
|
|
168
|
+
const prop = callee.property
|
|
169
|
+
return !!prop && prop.type === 'Identifier' && prop.name === 'join'
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Повертає текст quasis у TemplateLiteral (без expressions).
|
|
174
|
+
* @param {unknown} template TemplateLiteral
|
|
175
|
+
* @returns {string} обʼєднаний текст
|
|
176
|
+
*/
|
|
177
|
+
function templateQuasisText(template) {
|
|
178
|
+
if (!template || template.type !== 'TemplateLiteral') return ''
|
|
179
|
+
const quasis = template.quasis
|
|
180
|
+
if (!Array.isArray(quasis) || quasis.length === 0) return ''
|
|
181
|
+
let out = ''
|
|
182
|
+
for (const q of quasis) {
|
|
183
|
+
if (!q || typeof q !== 'object') continue
|
|
184
|
+
const value = q.value
|
|
185
|
+
if (!value || typeof value !== 'object') continue
|
|
186
|
+
if (typeof value.raw === 'string') out += value.raw
|
|
187
|
+
}
|
|
188
|
+
return out
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Чи виглядає TemplateLiteral як SQL-контекст зі списком (IN/VALUES (...)).
|
|
193
|
+
* @param {unknown} template TemplateLiteral
|
|
194
|
+
* @returns {boolean} true, якщо текст містить `IN (` або `VALUES (`
|
|
195
|
+
*/
|
|
196
|
+
function isSqlListContextTemplate(template) {
|
|
197
|
+
return SQL_LIST_CONTEXT_RE.test(templateQuasisText(template))
|
|
198
|
+
}
|
|
199
|
+
|
|
137
200
|
/**
|
|
138
201
|
* Знаходить створення `ConnectionPool` всередині функцій.
|
|
139
202
|
* @param {string} content вихідний код
|
|
@@ -197,6 +260,92 @@ export function findUnsafeMssqlQueryTemplateCallInText(content, virtualPath = 's
|
|
|
197
260
|
return out
|
|
198
261
|
}
|
|
199
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Знаходить shared Request (`export const request = pool.request()` та подібні), які не можна
|
|
265
|
+
* повторно використовувати між запитами.
|
|
266
|
+
* @param {string} content вихідний код
|
|
267
|
+
* @param {string} [virtualPath] шлях для вибору `lang`
|
|
268
|
+
* @returns {{ line: number, snippet: string }[]} список порушень
|
|
269
|
+
*/
|
|
270
|
+
export function findSharedMssqlRequestInText(content, virtualPath = 'scan.ts') {
|
|
271
|
+
const lang = langFromPath(virtualPath || 'scan.ts')
|
|
272
|
+
let result
|
|
273
|
+
try {
|
|
274
|
+
result = parseSync(virtualPath, content, { lang, sourceType: 'module' })
|
|
275
|
+
} catch {
|
|
276
|
+
return []
|
|
277
|
+
}
|
|
278
|
+
if (result.errors?.length) return []
|
|
279
|
+
|
|
280
|
+
/** @type {{ line: number, snippet: string }[]} */
|
|
281
|
+
const out = []
|
|
282
|
+
walkAstWithAncestors(result.program, [], node => {
|
|
283
|
+
if (node.type !== 'VariableDeclarator') return
|
|
284
|
+
const id = node.id
|
|
285
|
+
const init = node.init
|
|
286
|
+
if (!id || id.type !== 'Identifier') return
|
|
287
|
+
if (id.name !== 'request') return
|
|
288
|
+
if (!init || typeof init !== 'object') return
|
|
289
|
+
if (!isRequestFactoryCall(init)) return
|
|
290
|
+
|
|
291
|
+
out.push({
|
|
292
|
+
line: offsetToLine(content, node.start),
|
|
293
|
+
snippet: normalizeSnippet(content.slice(node.start, node.end))
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
return out
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Знаходить небезпечні динамічні списки в SQL, коли у TemplateLiteral/TaggedTemplateExpression
|
|
301
|
+
* підставляють рядки, зібрані через `.join(...)` у контексті `IN (...)` або `VALUES (...)`.
|
|
302
|
+
*
|
|
303
|
+
* Цей патерн небезпечний навіть якщо зовні використовується tagged template, бо в запит
|
|
304
|
+
* потрапляє “готовий шматок SQL”, а не параметризовані значення.
|
|
305
|
+
*
|
|
306
|
+
* @param {string} content вихідний код
|
|
307
|
+
* @param {string} [virtualPath] шлях для вибору `lang`
|
|
308
|
+
* @returns {{ line: number, snippet: string }[]} список порушень
|
|
309
|
+
*/
|
|
310
|
+
export function findUnsafeMssqlDynamicSqlListInText(content, virtualPath = 'scan.ts') {
|
|
311
|
+
const lang = langFromPath(virtualPath || 'scan.ts')
|
|
312
|
+
let result
|
|
313
|
+
try {
|
|
314
|
+
result = parseSync(virtualPath, content, { lang, sourceType: 'module' })
|
|
315
|
+
} catch {
|
|
316
|
+
return []
|
|
317
|
+
}
|
|
318
|
+
if (result.errors?.length) return []
|
|
319
|
+
|
|
320
|
+
/** @type {{ line: number, snippet: string }[]} */
|
|
321
|
+
const out = []
|
|
322
|
+
walkAstWithAncestors(result.program, [], node => {
|
|
323
|
+
/** @type {unknown} */
|
|
324
|
+
let template = null
|
|
325
|
+
|
|
326
|
+
if (node.type === 'TemplateLiteral') {
|
|
327
|
+
template = node
|
|
328
|
+
} else if (node.type === 'TaggedTemplateExpression') {
|
|
329
|
+
template = node.quasi
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!template || typeof template !== 'object' || template.type !== 'TemplateLiteral') return
|
|
333
|
+
if (!isSqlListContextTemplate(template)) return
|
|
334
|
+
const expressions = template.expressions
|
|
335
|
+
if (!Array.isArray(expressions) || expressions.length === 0) return
|
|
336
|
+
|
|
337
|
+
const hasJoin = expressions.some(expr => isJoinCall(expr))
|
|
338
|
+
if (!hasJoin) return
|
|
339
|
+
|
|
340
|
+
out.push({
|
|
341
|
+
line: offsetToLine(content, template.start),
|
|
342
|
+
snippet: normalizeSnippet(content.slice(template.start, template.end))
|
|
343
|
+
})
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
return out
|
|
347
|
+
}
|
|
348
|
+
|
|
200
349
|
/**
|
|
201
350
|
* Чи сканувати цей файл за розширенням (JS/TS-сім'я).
|
|
202
351
|
* @param {string} relativePathPosix відносний шлях (posix)
|