@nitra/cursor 1.13.45 → 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 +19 -1
- package/package.json +1 -1
- 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,6 +4,24 @@
|
|
|
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
|
+
|
|
7
25
|
## [1.13.45] - 2026-05-19
|
|
8
26
|
|
|
9
27
|
### Fixed
|
|
@@ -15,7 +33,7 @@
|
|
|
15
33
|
|
|
16
34
|
### Added
|
|
17
35
|
|
|
18
|
-
- `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`.
|
|
19
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`.
|
|
20
38
|
|
|
21
39
|
## [1.13.43] - 2026-05-18
|
package/package.json
CHANGED
|
@@ -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
|