@nitra/cursor 1.13.44 → 1.13.48
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 +26 -1
- package/package.json +1 -1
- package/rules/image-avif/fix/avif_generation/check.mjs +58 -22
- package/rules/image-avif/image-avif.mdc +6 -5
- package/rules/js-bun-db/js-bun-db.mdc +3 -1
- package/rules/js-bun-redis/js-bun-redis.mdc +3 -1
- package/rules/k8s/fix/manifests/check.mjs +86 -1
- package/rules/k8s/k8s.mdc +21 -2
- package/rules/k8s/policy/network_policy/network_policy.rego +23 -0
- package/rules/k8s/policy/network_policy/template/networkpolicy.snippet.yaml +19 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,11 +4,36 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
+
## [1.13.48] - 2026-05-19
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `k8s.network_policy`: канонічний egress NetworkPolicy більше **не дозволяє** `to.namespaceSelector: {}` без `ports:` (catch-all). У шаблоні `networkpolicy.snippet.yaml`, генераторі `buildNetworkPolicyYaml` і rego-policy `network_policy.rego` тепер in-cluster rule має явний список TCP-портів: `80, 443, 5432, 3306, 1433, 6379, 8080, 4317, 4318`. Додатково: `fix k8s` під час прогону знаходить існуючі `networkpolicy.yaml` з legacy catch-all egress і **перезаписує** їх через `buildNetworkPolicyYaml` (повний rebuild за `metadata.name` + `app`-міткою). JS-валідатор `networkPolicyManifestViolations` не змінюється (порти enforce-ить rego). Bump `k8s.mdc` `1.35` → `1.36`. Спец: [docs/superpowers/specs/2026-05-19-networkpolicy-egress-explicit-ports-design.md](../../docs/superpowers/specs/2026-05-19-networkpolicy-egress-explicit-ports-design.md).
|
|
12
|
+
|
|
13
|
+
## [1.13.47] - 2026-05-19
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- `js-bun-db`, `js-bun-redis` rules: додано markdown-посилання на `policy/package_json/template/package.json.deny.json` у канонічних `<id>.mdc` — `findMissingMdcRefs` (викликається з `run-rule.mjs`) падав, бо канонічні `.mdc` не містили `[package.json.deny.json](./policy/package_json/template/package.json.deny.json)`. Bump rule versions: `js-bun-db.mdc` `1.7` → `1.8`, `js-bun-redis.mdc` `1.1` → `1.2`.
|
|
18
|
+
|
|
19
|
+
## [1.13.46] - 2026-05-19
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- `image-avif` rule: двопрохідний rewrite у `check image-avif` — спочатку pre-scan `.vue`/`.html` на raster-посилання (`VUE_RASTER_IMPORT_RE` + `VUE_RASTER_STATIC_SRC_RE`), і лише якщо є хоча б одне — запускається `npx @nitra/minify-image --avif`, rewrite та cleanup AVIF-сиріт. Якщо raster-посилань нема — вихід `0` без жодного side-effect. Bump `image-avif.mdc` `1.3` → `1.4`.
|
|
24
|
+
|
|
25
|
+
## [1.13.45] - 2026-05-19
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- `inlineTemplateLinks` tests: оновлено очікувані рядки для фікстури `__fixtures__/inline-template/fix/foo/template/snippet.json` (перейшла на форматований варіант `{ "key": "val" }` ще в 1.13.38) та для інтеграційного тесту `security.mdc` (snippet `package.json` тепер multi-line після lint-проходу). Без зміни рантайм-логіки.
|
|
30
|
+
- `check-ga` тестова фікстура `setupCanonicalGaProject`: додано крок `Install conftest` у `.github/workflows/lint-ga.yml`, без якого `ga.lint_ga` rego-полісі забороняє workflow і `check()` повертав 1.
|
|
31
|
+
|
|
7
32
|
## [1.13.44] - 2026-05-18
|
|
8
33
|
|
|
9
34
|
### Added
|
|
10
35
|
|
|
11
|
-
- `abie` rule: новий policy-концерн `abie.package_json_docs` — у кореневому `package.json` `devDependencies` має містити `@nitra/abie-docs` (presence-only, версію не фіксуємо). Реалізація: `npm/rules/abie/policy/package_json_docs/` (target.json + .rego + _test.rego). Bump `abie.mdc` `1.20` → `1.21`.
|
|
36
|
+
- `abie` rule: новий policy-концерн `abie.package_json_docs` — у кореневому `package.json` `devDependencies` має містити `@nitra/abie-docs` (presence-only, версію не фіксуємо). Реалізація: `npm/rules/abie/policy/package_json_docs/` (target.json + .rego + \_test.rego). Bump `abie.mdc` `1.20` → `1.21`.
|
|
12
37
|
- `efes` rule: перший policy-концерн `efes.package_json_docs` — у кореневому `package.json` `devDependencies` має містити `@nitra/efes-docs` (узгоджено з `graphql.mdc`, де схема береться з `node_modules/@nitra/efes-docs/schema/maya.graphql`). Реалізація: `npm/rules/efes/policy/package_json_docs/`. Bump `efes.mdc` `1.0` → `1.1`.
|
|
13
38
|
|
|
14
39
|
## [1.13.43] - 2026-05-18
|
package/package.json
CHANGED
|
@@ -3,11 +3,16 @@
|
|
|
3
3
|
* ув'язування `.avif`-двійників з посиланнями у `.vue`/`.html`.
|
|
4
4
|
*
|
|
5
5
|
* Дії під час `check image-avif`:
|
|
6
|
-
* 1.
|
|
7
|
-
*
|
|
6
|
+
* 1. **Pre-scan**: знайти в `.vue`/`.html` хоча б одне raster-посилання, яке потенційно
|
|
7
|
+
* можна переписати на AVIF-двійник (через `import x from '...png'` або
|
|
8
|
+
* `<img src="...png" />`). Пакети з opt-out `disable-avif: true` пропускаються.
|
|
9
|
+
* Якщо жодного raster-посилання не знайдено → exit 0 одразу (`npx --avif` не запускаємо,
|
|
10
|
+
* rewrite/cleanup-пасс теж пропускаємо — нічого не змінилось би).
|
|
11
|
+
* 2. `npx \@nitra/minify-image --src=. --write --avif` — генерує AVIF-двійники.
|
|
12
|
+
* 3. У кожному workspace-пакеті переписує raster-посилання у `.vue`/`.html` на `.avif`
|
|
8
13
|
* (де AVIF-двійник реально існує на диску). Pakety з `"\@nitra/minify-image": {
|
|
9
14
|
* "disable-avif": true }` у `package.json` пропускаються.
|
|
10
|
-
*
|
|
15
|
+
* 4. Прибирає AVIF-сироти — `<name>.<ext>.avif`, на які не лишилось жодного посилання
|
|
11
16
|
* у `.vue`/`.html` репозиторію, видаляються (умова правила: «AVIF лишається лише
|
|
12
17
|
* там, де заміна вдалася»).
|
|
13
18
|
*
|
|
@@ -69,7 +74,6 @@ const VUE_RASTER_STATIC_SRC_RE = /(?<![:\-_.])\bsrc\s*=\s*['"]([^'"\s]+\.(?:png|
|
|
|
69
74
|
* є сиротами і підлягають видаленню.
|
|
70
75
|
*/
|
|
71
76
|
const VUE_AVIF_REF_RE = /['"]([^'"\s]+\.(?:png|jpe?g|gif)\.avif)['"]/giu
|
|
72
|
-
const RASTER_IMAGE_EXT_RE = /\.(?:png|jpe?g|gif)$/iu
|
|
73
77
|
|
|
74
78
|
/**
|
|
75
79
|
* Чи у `package.json` пакета вимкнено avif-перевірку Vue-імпортів.
|
|
@@ -279,24 +283,51 @@ async function checkVueAvifImports(ignorePaths, usedAvifAbs, stats, pass, fail)
|
|
|
279
283
|
}
|
|
280
284
|
|
|
281
285
|
/**
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
+
* Pre-scan: чи є в `.vue`/`.html` хоча б одне raster-посилання, яке потенційно треба
|
|
287
|
+
* переписати на AVIF-двійник (через `import x from '...png'` або `<img src="...png" />`).
|
|
288
|
+
*
|
|
289
|
+
* Якщо false — весь подальший етап `image-avif` пропускаємо: ні `npx --avif`, ні rewrite,
|
|
290
|
+
* ні cleanup-сиріт не дали б ніяких змін. Сенс — не запускати дорогий `npx @nitra/minify-image`
|
|
291
|
+
* у проєктах, де AVIF не вживається (а опційно і не плануються).
|
|
292
|
+
*
|
|
293
|
+
* Скан робиться тими самими regexp-ами, що й основний rewrite-пасс (`VUE_RASTER_IMPORT_RE`
|
|
294
|
+
* + `VUE_RASTER_STATIC_SRC_RE`), і ходить лише по `.vue`/`.html` у workspace-пакетах, що НЕ
|
|
295
|
+
* мають opt-out `"@nitra/minify-image": { "disable-avif": true }` (інакше їхні шаблони ми
|
|
296
|
+
* все одно не сканували б, тож вони не мають провокувати запуск AVIF-етапу).
|
|
286
297
|
* @param {string[]} ignorePaths абсолютні шляхи каталогів, повністю виключених з обходу
|
|
287
|
-
* @returns {Promise<boolean>} `true`, якщо знайдено принаймні
|
|
298
|
+
* @returns {Promise<boolean>} `true`, якщо знайдено принаймні одне raster-посилання
|
|
288
299
|
*/
|
|
289
|
-
async function
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
+
async function hasAnyVueRasterReference(ignorePaths) {
|
|
301
|
+
const roots = await getMonorepoPackageRootDirs()
|
|
302
|
+
const absRootsByRel = new Map(roots.map(r => [r, join(process.cwd(), r)]))
|
|
303
|
+
for (const root of roots) {
|
|
304
|
+
const pkgPath = join(root, 'package.json')
|
|
305
|
+
if (existsSync(pkgPath)) {
|
|
306
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
307
|
+
if (packageHasAvifDisabled(pkg)) continue
|
|
308
|
+
}
|
|
309
|
+
const absRoot = absRootsByRel.get(root) ?? join(process.cwd(), root)
|
|
310
|
+
const otherRootsAbs = roots.filter(r => r !== root && r !== '.').map(r => absRootsByRel.get(r) ?? '')
|
|
311
|
+
/** @type {string[]} */
|
|
312
|
+
const targetFiles = []
|
|
313
|
+
await walkDir(
|
|
314
|
+
absRoot,
|
|
315
|
+
absPath => {
|
|
316
|
+
if (!absPath.endsWith('.vue') && !absPath.endsWith('.html')) return
|
|
317
|
+
if (otherRootsAbs.some(other => absPath.startsWith(`${other}/`))) return
|
|
318
|
+
targetFiles.push(absPath)
|
|
319
|
+
},
|
|
320
|
+
ignorePaths
|
|
321
|
+
)
|
|
322
|
+
for (const absPath of targetFiles) {
|
|
323
|
+
const content = await readFile(absPath, 'utf8')
|
|
324
|
+
VUE_RASTER_IMPORT_RE.lastIndex = 0
|
|
325
|
+
if (VUE_RASTER_IMPORT_RE.test(content)) return true
|
|
326
|
+
VUE_RASTER_STATIC_SRC_RE.lastIndex = 0
|
|
327
|
+
if (VUE_RASTER_STATIC_SRC_RE.test(content)) return true
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return false
|
|
300
331
|
}
|
|
301
332
|
|
|
302
333
|
/**
|
|
@@ -381,10 +412,15 @@ export async function check() {
|
|
|
381
412
|
|
|
382
413
|
const ignorePaths = await loadCursorIgnorePaths(process.cwd())
|
|
383
414
|
|
|
384
|
-
if (await
|
|
385
|
-
|
|
415
|
+
if (!(await hasAnyVueRasterReference(ignorePaths))) {
|
|
416
|
+
pass(
|
|
417
|
+
'image-avif: у .vue/.html немає raster-посилань для переписування — AVIF-генерація і cleanup пропущені'
|
|
418
|
+
)
|
|
419
|
+
return reporter.getExitCode()
|
|
386
420
|
}
|
|
387
421
|
|
|
422
|
+
runAvifGeneration()
|
|
423
|
+
|
|
388
424
|
/** @type {Set<string>} */
|
|
389
425
|
const usedAvifAbs = new Set()
|
|
390
426
|
/** @type {RewriteStats} */
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: AVIF-двійники для raster-зображень з ув'язуванням у .vue/.html
|
|
3
|
-
version: '1.
|
|
3
|
+
version: '1.4'
|
|
4
4
|
globs: "**/*.{png,jpg,jpeg,gif,avif,vue,html}"
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
AVIF-двійники (`<name>.<ext>.avif`) генерує **виключно** `npx @nitra/cursor check image-avif` — у `lint-image` прапорець `--avif` заборонений (це валідує правило `image-compress`). Перевірка робить
|
|
8
|
+
AVIF-двійники (`<name>.<ext>.avif`) генерує **виключно** `npx @nitra/cursor check image-avif` — у `lint-image` прапорець `--avif` заборонений (це валідує правило `image-compress`). Перевірка робить чотири кроки в порядку:
|
|
9
9
|
|
|
10
|
-
1.
|
|
11
|
-
2.
|
|
10
|
+
1. **Pre-scan**: шукає у `.vue`/`.html` хоча б одне raster-посилання, яке потенційно треба переписати на AVIF-двійник (`import x from '...png'` або `<img src="...png" />`). Пакети з opt-out `disable-avif: true` пропускаються. **Якщо жодного raster-посилання не знайдено — `check image-avif` завершується успіхом одразу: ні `npx --avif`, ні rewrite, ні cleanup-сиріт не виконуються** (нічого було б змінювати). Так уникаємо дорогого `npx @nitra/minify-image` у проєктах, де AVIF не вживається.
|
|
11
|
+
2. Запускає `npx @nitra/minify-image --src=. --write --avif` (≥ **3.3.1**) — генерує `<name>.<ext>.avif` поряд з кожним PNG/JPEG/GIF. CLI порівнює sha1 кожного raster-сорсу зі збереженим у `.n-minify-image.tsv` і перезаписує `<source>.avif` при зміні оригіналу.
|
|
12
|
+
3. Сканує `.vue` (а також `.html`) файли в кожному workspace-пакеті (root + workspaces) і автоматично переписує raster-посилання на AVIF-двійник у двох формах:
|
|
12
13
|
- **Імпорт-пов'язані** — `import x from '...png|jpg|jpeg|gif'` (далі `:src="x"` у шаблоні);
|
|
13
14
|
- **Прямі статичні** — `<img src="...png" />` у `<template>` (Vite перетворює такий шлях на asset-імпорт на етапі збірки, тож вимога та сама).
|
|
14
|
-
|
|
15
|
+
4. Видаляє AVIF-сироти: ходить по всіх `<...>.avif` у репозиторії; якщо на двійник не лишилось жодного посилання у `.vue`/`.html` — файл видаляється. **AVIF на диску лишається лише там, де заміна реально відбулась** — тому невикористані оригінали не накопичують `.avif`-«хвости». **Зауваж**: cleanup виконується ЛИШЕ якщо pre-scan на кроці 1 знайшов хоча б одне raster-посилання — інакше осиротілі `.avif` залишаються недоторканими (видаляться вже наступним прогоном, коли в `.vue`/`.html` зʼявиться raster для конвертації).
|
|
15
16
|
|
|
16
17
|
```vue title="App.vue (після check image-avif)"
|
|
17
18
|
<script setup>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
description: Використання pg / mysql2 / Bun SQL у Node.js та Bun
|
|
3
3
|
globs: "**/package.json,**/src/conn/**"
|
|
4
4
|
alwaysApply: false
|
|
5
|
-
version: '1.
|
|
5
|
+
version: '1.8'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
## Підтримувані версії баз даних
|
|
@@ -17,6 +17,8 @@ PostgreSQL 18+, MariaDB 10.6+ (сумісний з MySQL-протоколом,
|
|
|
17
17
|
- Видалити з коду: усі `import` / `require` цих пакетів та власні обгортки над ними.
|
|
18
18
|
- Замінити на `import { sql, SQL } from 'bun'` — Bun має вбудований клієнт із пулом, prepared statements та tagged templates.
|
|
19
19
|
|
|
20
|
+
Канон заборонених `dependencies` (`pg`, `pg-format`, `mysql2`): [package.json.deny.json](./policy/package_json/template/package.json.deny.json).
|
|
21
|
+
|
|
20
22
|
`pg-format` — це ручне форматування SQL через escape (`format('... %L ...', value)`); такі рядки легко поламати неправильним типом, locale-залежним escape або забутим `%L`. Tagged template Bun SQL параметризує значення нативно (`sql\`... ${value} ...\``) і не лишає простору для injection — окремий «форматер» не потрібен.
|
|
21
23
|
|
|
22
24
|
## `pg-format`: повне видалення, без шимів
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
description: Використання Redis/Valkey з Bun
|
|
3
3
|
globs: "**/package.json,**/src/conn/**"
|
|
4
4
|
alwaysApply: false
|
|
5
|
-
version: '1.
|
|
5
|
+
version: '1.2'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
## Підтримувані версії redis
|
|
@@ -16,3 +16,5 @@ Redis 7.2+
|
|
|
16
16
|
- Видалити з `dependencies`: `ioredis`, `node-redis`.
|
|
17
17
|
- Видалити з коду: усі `import` / `require` цих пакетів та власні обгортки над ними.
|
|
18
18
|
- Замінити на `import { redis } from 'bun'`
|
|
19
|
+
|
|
20
|
+
Канон заборонених `dependencies` (`ioredis`, `node-redis`, `redis`, `@redis/*`): [package.json.deny.json](./policy/package_json/template/package.json.deny.json).
|
|
@@ -4252,9 +4252,16 @@ export function pdbManifestViolations(manifest, expectedAppLabel, isDevLike) {
|
|
|
4252
4252
|
return errs
|
|
4253
4253
|
}
|
|
4254
4254
|
|
|
4255
|
+
/**
|
|
4256
|
+
* Канонічний список in-cluster TCP-портів у `to: [{namespaceSelector: {}}]` rule (k8s.mdc).
|
|
4257
|
+
* Зовнішній доступ (80/443 → 0.0.0.0/0) і kube-dns (53 UDP/TCP) — окремі rule вище.
|
|
4258
|
+
* Catch-all (`namespaceSelector: {}` без `ports:`) — заборонено.
|
|
4259
|
+
*/
|
|
4260
|
+
const NETWORK_POLICY_IN_CLUSTER_DEFAULT_PORTS = [80, 443, 5432, 3306, 1433, 6379, 8080, 4317, 4318]
|
|
4261
|
+
|
|
4255
4262
|
/**
|
|
4256
4263
|
* Канонічний блок `spec.egress` NetworkPolicy (k8s.mdc): kube-dns; TCP 80/443 на 0.0.0.0/0;
|
|
4257
|
-
*
|
|
4264
|
+
* in-cluster `namespaceSelector: {}` зі списком `NETWORK_POLICY_IN_CLUSTER_DEFAULT_PORTS`.
|
|
4258
4265
|
*/
|
|
4259
4266
|
const NETWORK_POLICY_EGRESS_YAML = ` egress:
|
|
4260
4267
|
- to:
|
|
@@ -4279,6 +4286,8 @@ const NETWORK_POLICY_EGRESS_YAML = ` egress:
|
|
|
4279
4286
|
port: 443
|
|
4280
4287
|
- to:
|
|
4281
4288
|
- namespaceSelector: {}
|
|
4289
|
+
ports:
|
|
4290
|
+
${NETWORK_POLICY_IN_CLUSTER_DEFAULT_PORTS.map(p => ` - protocol: TCP\n port: ${p}`).join('\n')}
|
|
4282
4291
|
`
|
|
4283
4292
|
|
|
4284
4293
|
/**
|
|
@@ -6406,6 +6415,76 @@ async function appendNetworkPolicyDocuments(npAbs, toAdd, npRel, passFn) {
|
|
|
6406
6415
|
}
|
|
6407
6416
|
}
|
|
6408
6417
|
|
|
6418
|
+
/**
|
|
6419
|
+
* Перевіряє, чи `spec.egress` містить in-cluster rule з порожнім namespaceSelector БЕЗ ports
|
|
6420
|
+
* (legacy catch-all — заборонено новим каноном k8s.mdc).
|
|
6421
|
+
* @param {unknown} doc розпарсений NetworkPolicy-документ
|
|
6422
|
+
* @returns {boolean} true якщо doc має legacy catch-all rule
|
|
6423
|
+
*/
|
|
6424
|
+
function networkPolicyHasLegacyCatchAllEgress(doc) {
|
|
6425
|
+
if (doc === null || typeof doc !== 'object' || Array.isArray(doc)) return false
|
|
6426
|
+
const spec = /** @type {Record<string, unknown>} */ (doc).spec
|
|
6427
|
+
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) return false
|
|
6428
|
+
const egress = /** @type {Record<string, unknown>} */ (spec).egress
|
|
6429
|
+
if (!Array.isArray(egress)) return false
|
|
6430
|
+
for (const rule of egress) {
|
|
6431
|
+
if (rule === null || typeof rule !== 'object' || Array.isArray(rule)) continue
|
|
6432
|
+
const ruleRec = /** @type {Record<string, unknown>} */ (rule)
|
|
6433
|
+
const to = ruleRec.to
|
|
6434
|
+
if (!Array.isArray(to)) continue
|
|
6435
|
+
const hasEmptyNsPeer = to.some(peer => {
|
|
6436
|
+
if (peer === null || typeof peer !== 'object' || Array.isArray(peer)) return false
|
|
6437
|
+
const ns = /** @type {Record<string, unknown>} */ (peer).namespaceSelector
|
|
6438
|
+
return ns !== null && typeof ns === 'object' && !Array.isArray(ns) && Object.keys(ns).length === 0
|
|
6439
|
+
})
|
|
6440
|
+
if (!hasEmptyNsPeer) continue
|
|
6441
|
+
const ports = ruleRec.ports
|
|
6442
|
+
if (!Array.isArray(ports) || ports.length === 0) return true
|
|
6443
|
+
}
|
|
6444
|
+
return false
|
|
6445
|
+
}
|
|
6446
|
+
|
|
6447
|
+
/**
|
|
6448
|
+
* Migrate legacy `networkpolicy.yaml`: якщо хоч один документ має catch-all in-cluster egress —
|
|
6449
|
+
* перезаписати **всі** документи у файлі через `buildNetworkPolicyYaml(name, appLabel)`. Деталі — k8s.mdc.
|
|
6450
|
+
* @param {string} npAbs абсолютний шлях до networkpolicy.yaml
|
|
6451
|
+
* @returns {Promise<boolean>} true якщо файл переписаний
|
|
6452
|
+
*/
|
|
6453
|
+
export async function regenerateLegacyNetworkPolicyDocsInFile(npAbs) {
|
|
6454
|
+
if (!existsSync(npAbs)) return false
|
|
6455
|
+
const docs = await readAllDocsByKindFromFile(npAbs, 'NetworkPolicy')
|
|
6456
|
+
if (docs.length === 0) return false
|
|
6457
|
+
const needsMigration = docs.some(d => networkPolicyHasLegacyCatchAllEgress(d))
|
|
6458
|
+
if (!needsMigration) return false
|
|
6459
|
+
/**
|
|
6460
|
+
@type {Array<{ name: string, appLabel: string }>}
|
|
6461
|
+
*/
|
|
6462
|
+
const specs = []
|
|
6463
|
+
for (const doc of docs) {
|
|
6464
|
+
const name = manifestMetadataName(doc)
|
|
6465
|
+
const spec = /** @type {Record<string, unknown>} */ (doc).spec
|
|
6466
|
+
let appLabel = ''
|
|
6467
|
+
if (spec !== null && typeof spec === 'object' && !Array.isArray(spec)) {
|
|
6468
|
+
const podSelector = /** @type {Record<string, unknown>} */ (spec).podSelector
|
|
6469
|
+
if (podSelector !== null && typeof podSelector === 'object' && !Array.isArray(podSelector)) {
|
|
6470
|
+
const matchLabels = /** @type {Record<string, unknown>} */ (podSelector).matchLabels
|
|
6471
|
+
if (matchLabels !== null && typeof matchLabels === 'object' && !Array.isArray(matchLabels)) {
|
|
6472
|
+
const a = /** @type {Record<string, unknown>} */ (matchLabels).app
|
|
6473
|
+
if (typeof a === 'string') appLabel = a
|
|
6474
|
+
}
|
|
6475
|
+
}
|
|
6476
|
+
}
|
|
6477
|
+
if (typeof name === 'string' && name !== '' && appLabel !== '') specs.push({ name, appLabel })
|
|
6478
|
+
}
|
|
6479
|
+
if (specs.length === 0) return false
|
|
6480
|
+
const blocks = specs.map(({ name, appLabel }, i) => {
|
|
6481
|
+
const block = buildNetworkPolicyYaml(name, appLabel)
|
|
6482
|
+
return i === 0 ? block.trimEnd() : stripYamlLanguageServerModeline(block).trimEnd()
|
|
6483
|
+
})
|
|
6484
|
+
await writeFile(npAbs, `${blocks.join('\n---\n')}\n`, 'utf8')
|
|
6485
|
+
return true
|
|
6486
|
+
}
|
|
6487
|
+
|
|
6409
6488
|
/**
|
|
6410
6489
|
* Створює відсутні NetworkPolicy для workload-ів у каталозі (base → `components/`, інакше — поруч).
|
|
6411
6490
|
* @param {string} dir абсолютний каталог workload-маніфесту
|
|
@@ -6420,6 +6499,12 @@ async function ensureNetworkPoliciesForWorkloadsInDir(dir, workloads, rootNorm,
|
|
|
6420
6499
|
const isBase = isK8sYamlUnderBaseDirectory(`${relDir}/probe.yaml`)
|
|
6421
6500
|
const npAbs = isBase ? join(dir, '..', COMPONENTS_DIR, NETWORK_POLICY_FILENAME) : join(dir, NETWORK_POLICY_FILENAME)
|
|
6422
6501
|
const npRel = (relative(rootNorm, npAbs) || npAbs).replaceAll('\\', '/')
|
|
6502
|
+
if (existsSync(npAbs)) {
|
|
6503
|
+
const migrated = await regenerateLegacyNetworkPolicyDocsInFile(npAbs)
|
|
6504
|
+
if (migrated) {
|
|
6505
|
+
passFn(`${npRel}: міграція legacy catch-all egress → канон з явними in-cluster портами (k8s.mdc)`)
|
|
6506
|
+
}
|
|
6507
|
+
}
|
|
6423
6508
|
const existing = await existingNetworkPolicyNames(npAbs)
|
|
6424
6509
|
/**
|
|
6425
6510
|
@type {Array<{ name: string, appLabel: string, kind: string }>}
|
package/rules/k8s/k8s.mdc
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: K8s YAML — $schema (yaml-language-server); lint-k8s (kubeconform, kubescape); check-k8s
|
|
3
|
-
version: '1.
|
|
3
|
+
version: '1.36'
|
|
4
4
|
globs: "**/k8s/**/*.yaml"
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
@@ -388,7 +388,7 @@ images:
|
|
|
388
388
|
|
|
389
389
|
- **`kustomization.yaml`** — `apiVersion: kustomize.config.k8s.io/v1alpha1`, `kind: Component`, `resources: [hpa.yaml, networkpolicy.yaml, pdb.yaml]` (відсортовано за алфавітом).
|
|
390
390
|
- **`hpa.yaml`** — `autoscaling/v2`, `HorizontalPodAutoscaler`, **без** `metadata.namespace` (namespace задає kustomization-споживач), `spec.scaleTargetRef.name` **= `metadata.name`** Deployment з base, dev-like значення `minReplicas: 1`, `maxReplicas: 1`.
|
|
391
|
-
- **`networkpolicy.yaml`** — `networking.k8s.io/v1`, `NetworkPolicy`, **без** `metadata.namespace`; один або кілька документів (`---`) — по одному на кожен workload (**Deployment** / **StatefulSet** / **DaemonSet** / **Job** / **CronJob**) у sibling `base/`; `metadata.name` **= `metadata.name`** workload, `spec.podSelector.matchLabels.app` **= мітка `app`** workload. **Ingress:** `from.podSelector: {}` (інші Pod у namespace). **Egress (усі workload-и):** kube-dns (UDP/TCP 53); **TCP 80 і 443** на `0.0.0.0/0` (HTTP/HTTPS назовні, включно з metadata `169.254.169.254:80`);
|
|
391
|
+
- **`networkpolicy.yaml`** — `networking.k8s.io/v1`, `NetworkPolicy`, **без** `metadata.namespace`; один або кілька документів (`---`) — по одному на кожен workload (**Deployment** / **StatefulSet** / **DaemonSet** / **Job** / **CronJob**) у sibling `base/`; `metadata.name` **= `metadata.name`** workload, `spec.podSelector.matchLabels.app` **= мітка `app`** workload. **Ingress:** `from.podSelector: {}` (інші Pod у namespace). **Egress (усі workload-и):** kube-dns (UDP/TCP 53); **TCP 80 і 443** на `0.0.0.0/0` (HTTP/HTTPS назовні, включно з metadata `169.254.169.254:80`); **in-cluster** — `to.namespaceSelector: {}` з **явним списком TCP-портів** (`80, 443, 5432, 3306, 1433, 6379, 8080, 4317, 4318`; трафік на `*.svc` / Pod-и в кластері). Заборонено: `egress: [{}]`; `to.namespaceSelector: {}` без `ports:` (catch-all). Додаткові in-cluster порти можна додати вручну у `ports:` цього rule. Канон: [networkpolicy.snippet.yaml](./policy/network_policy/template/networkpolicy.snippet.yaml). Якщо документа для workload немає — **`check k8s`** дописує його автоматично.
|
|
392
392
|
- **`pdb.yaml`** — `policy/v1`, `PodDisruptionBudget`, **без** `metadata.namespace`, `spec.selector.matchLabels.app` **= `spec.selector.matchLabels.app`** Deployment, dev-like `minAvailable: 0`.
|
|
393
393
|
|
|
394
394
|
Інші назви каталогу (`scale/`, `hpa-component/`, `pdb-component/`) — fail.
|
|
@@ -509,6 +509,25 @@ spec:
|
|
|
509
509
|
port: 443
|
|
510
510
|
- to:
|
|
511
511
|
- namespaceSelector: {}
|
|
512
|
+
ports:
|
|
513
|
+
- protocol: TCP
|
|
514
|
+
port: 80
|
|
515
|
+
- protocol: TCP
|
|
516
|
+
port: 443
|
|
517
|
+
- protocol: TCP
|
|
518
|
+
port: 5432
|
|
519
|
+
- protocol: TCP
|
|
520
|
+
port: 3306
|
|
521
|
+
- protocol: TCP
|
|
522
|
+
port: 1433
|
|
523
|
+
- protocol: TCP
|
|
524
|
+
port: 6379
|
|
525
|
+
- protocol: TCP
|
|
526
|
+
port: 8080
|
|
527
|
+
- protocol: TCP
|
|
528
|
+
port: 4317
|
|
529
|
+
- protocol: TCP
|
|
530
|
+
port: 4318
|
|
512
531
|
```
|
|
513
532
|
|
|
514
533
|
```yaml title="k8s/components/hpa.yaml"
|
|
@@ -85,6 +85,13 @@ deny contains "spec.egress: потрібен to.namespaceSelector: {} (інші
|
|
|
85
85
|
not egress_has_cluster_namespace_selector(spec)
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
deny contains "spec.egress: to.namespaceSelector: {} мусить мати непорожні ports — catch-all заборонено (k8s.mdc)" if {
|
|
89
|
+
is_np_doc
|
|
90
|
+
spec := object.get(input, "spec", null)
|
|
91
|
+
is_object(spec)
|
|
92
|
+
cluster_egress_rule_without_ports(spec)
|
|
93
|
+
}
|
|
94
|
+
|
|
88
95
|
is_np_doc if input.kind == "NetworkPolicy"
|
|
89
96
|
|
|
90
97
|
is_np_doc if startswith(object.get(input, "apiVersion", ""), "networking.k8s.io/")
|
|
@@ -156,3 +163,19 @@ egress_has_cluster_namespace_selector(spec) if {
|
|
|
156
163
|
is_object(ns)
|
|
157
164
|
count(ns) == 0
|
|
158
165
|
}
|
|
166
|
+
|
|
167
|
+
cluster_egress_rule_without_ports(spec) if {
|
|
168
|
+
egress := object.get(spec, "egress", null)
|
|
169
|
+
is_array(egress)
|
|
170
|
+
some rule in egress
|
|
171
|
+
is_object(rule)
|
|
172
|
+
to_list := object.get(rule, "to", null)
|
|
173
|
+
is_array(to_list)
|
|
174
|
+
some peer in to_list
|
|
175
|
+
is_object(peer)
|
|
176
|
+
ns := object.get(peer, "namespaceSelector", null)
|
|
177
|
+
is_object(ns)
|
|
178
|
+
count(ns) == 0
|
|
179
|
+
ports := object.get(rule, "ports", [])
|
|
180
|
+
count(ports) == 0
|
|
181
|
+
}
|
|
@@ -30,3 +30,22 @@ spec:
|
|
|
30
30
|
port: 443
|
|
31
31
|
- to:
|
|
32
32
|
- namespaceSelector: {}
|
|
33
|
+
ports:
|
|
34
|
+
- protocol: TCP
|
|
35
|
+
port: 80
|
|
36
|
+
- protocol: TCP
|
|
37
|
+
port: 443
|
|
38
|
+
- protocol: TCP
|
|
39
|
+
port: 5432
|
|
40
|
+
- protocol: TCP
|
|
41
|
+
port: 3306
|
|
42
|
+
- protocol: TCP
|
|
43
|
+
port: 1433
|
|
44
|
+
- protocol: TCP
|
|
45
|
+
port: 6379
|
|
46
|
+
- protocol: TCP
|
|
47
|
+
port: 8080
|
|
48
|
+
- protocol: TCP
|
|
49
|
+
port: 4317
|
|
50
|
+
- protocol: TCP
|
|
51
|
+
port: 4318
|