@nitra/cursor 1.13.45 → 1.13.49
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 +25 -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 +28 -4
- package/rules/k8s/lint/lint.mjs +126 -21
- 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,30 @@
|
|
|
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.49] - 2026-05-19
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `lint-k8s`: kubescape тепер сканує **зібраний kustomize-маніфест** через stdin (`kustomize build <dir> | kubescape scan -`) для кожного dir-у з `kustomization.yaml` під `…/k8s` (Kustomize Components — `kind: Component` — пропускаються, вони не білдяться окремо). Це усуває false-positive **C-0260** (`Missing network policy`) у каноні з sibling `components/networkpolicy.yaml` без `metadata.namespace`: сирий dir-скан не виконував kustomize, бачив порожній namespace у NetworkPolicy проти непорожнього у Deployment з `base/`, через що `podSelector` не матчився. Якщо `kustomization.yaml` під коренем `…/k8s` немає — fallback на старий dir-скан. Нова PATH-залежність — `kustomize` (додано крок у GHA-шаблоні `lint-k8s.yml`). Bump `k8s.mdc` `1.36` → `1.37`.
|
|
12
|
+
|
|
13
|
+
## [1.13.48] - 2026-05-19
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- `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).
|
|
18
|
+
|
|
19
|
+
## [1.13.47] - 2026-05-19
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- `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`.
|
|
24
|
+
|
|
25
|
+
## [1.13.46] - 2026-05-19
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- `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`.
|
|
30
|
+
|
|
7
31
|
## [1.13.45] - 2026-05-19
|
|
8
32
|
|
|
9
33
|
### Fixed
|
|
@@ -15,7 +39,7 @@
|
|
|
15
39
|
|
|
16
40
|
### Added
|
|
17
41
|
|
|
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`.
|
|
42
|
+
- `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
43
|
- `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
44
|
|
|
21
45
|
## [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.37'
|
|
4
4
|
globs: "**/k8s/**/*.yaml"
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
@@ -29,11 +29,11 @@ alwaysApply: false
|
|
|
29
29
|
|
|
30
30
|
Окремо від modeline `$schema` у редакторі варто ганяти CLI-лінтери (**kubeconform** і **kubescape**) по тих самих дерев’ях **`…/k8s`**.
|
|
31
31
|
|
|
32
|
-
**Залежності:** виконувані файли kubeconform і
|
|
32
|
+
**Залежності:** виконувані файли kubeconform, kubescape і kustomize у **PATH**; не додавай їх у **devDependencies**.
|
|
33
33
|
|
|
34
34
|
**Версія Kubernetes для kubeconform** має відповідати PIN yannh у цьому правилі та в **`check-k8s.mjs`** (зараз **`-kubernetes-version 1.33.9`** — semver без префікса `v`, еквівалент релізу **v1.33.9**; набір схем **`v1.33.9-standalone-strict`**). Для CRD додатково підключай реєстр [datreeio/CRDs-catalog](https://github.com/datreeio/CRDs-catalog) другим **`-schema-location`**, як у [прикладах kubeconform](https://github.com/yannh/kubeconform#readme). За потреби **`-ignore-missing-schemas`**, якщо частина CRD ще без публічної схеми.
|
|
35
35
|
|
|
36
|
-
**kubescape
|
|
36
|
+
**kubescape — вхід через зібраний kustomize-маніфест:** для кожного dir-у з `kustomization.yaml` (`kind: Kustomization`; **`kind: Component`** пропускається — він не білдиться окремо) `lint-k8s` виконує **`kustomize build <dir> | kubescape scan -`** з порогом **`--severity-threshold high`**. Це усуває false-positive **C-0260** (`Missing network policy`) у каноні з sibling **`components/networkpolicy.yaml`** без `metadata.namespace`: сирий dir-скан не виконує kustomize-збірку й бачить порожній namespace у NetworkPolicy проти непорожнього у Deployment з `base/`, тож `podSelector` не матчиться. Якщо в дереві **`…/k8s`** немає жодного `kustomization.yaml` (проєкт без Kustomize) — fallback на старий dir-скан **`kubescape scan <каталог-k8s>`**. Перший запуск kubescape може завантажувати артефакти — у CI потрібна мережа або [offline](https://github.com/kubescape/kubescape#readme). На відміну від kubeconform, у **kubescape scan** немає прапорця **`-kubernetes-version`**: перевірка йде за **framework/control** (NSA, MITRE, CIS тощо), а не проти OpenAPI-схеми конкретного релізу Kubernetes. **Орієнтир** для репозиторію той самий, що й для kubeconform — кластер **v1.33.9** (див. **`-kubernetes-version 1.33.9`** вище); для CIS і подібних наближень обирай актуальний framework під політику команди (**`kubescape list frameworks`**, див. [CLI reference](https://github.com/kubescape/kubescape/blob/master/docs/cli-reference.md)).
|
|
37
37
|
|
|
38
38
|
### Винятки kubescape: `.kubescape-exceptions.json`
|
|
39
39
|
|
|
@@ -99,6 +99,11 @@ jobs:
|
|
|
99
99
|
curl -s https://raw.githubusercontent.com/kubescape/kubescape/master/install.sh | /bin/bash
|
|
100
100
|
echo "$HOME/.kubescape/bin" >> $GITHUB_PATH
|
|
101
101
|
|
|
102
|
+
- name: Install kustomize
|
|
103
|
+
run: |
|
|
104
|
+
curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
|
|
105
|
+
sudo mv kustomize /usr/local/bin/
|
|
106
|
+
|
|
102
107
|
- name: Lint K8s
|
|
103
108
|
run: bun run lint-k8s
|
|
104
109
|
```
|
|
@@ -388,7 +393,7 @@ images:
|
|
|
388
393
|
|
|
389
394
|
- **`kustomization.yaml`** — `apiVersion: kustomize.config.k8s.io/v1alpha1`, `kind: Component`, `resources: [hpa.yaml, networkpolicy.yaml, pdb.yaml]` (відсортовано за алфавітом).
|
|
390
395
|
- **`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`);
|
|
396
|
+
- **`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
397
|
- **`pdb.yaml`** — `policy/v1`, `PodDisruptionBudget`, **без** `metadata.namespace`, `spec.selector.matchLabels.app` **= `spec.selector.matchLabels.app`** Deployment, dev-like `minAvailable: 0`.
|
|
393
398
|
|
|
394
399
|
Інші назви каталогу (`scale/`, `hpa-component/`, `pdb-component/`) — fail.
|
|
@@ -509,6 +514,25 @@ spec:
|
|
|
509
514
|
port: 443
|
|
510
515
|
- to:
|
|
511
516
|
- namespaceSelector: {}
|
|
517
|
+
ports:
|
|
518
|
+
- protocol: TCP
|
|
519
|
+
port: 80
|
|
520
|
+
- protocol: TCP
|
|
521
|
+
port: 443
|
|
522
|
+
- protocol: TCP
|
|
523
|
+
port: 5432
|
|
524
|
+
- protocol: TCP
|
|
525
|
+
port: 3306
|
|
526
|
+
- protocol: TCP
|
|
527
|
+
port: 1433
|
|
528
|
+
- protocol: TCP
|
|
529
|
+
port: 6379
|
|
530
|
+
- protocol: TCP
|
|
531
|
+
port: 8080
|
|
532
|
+
- protocol: TCP
|
|
533
|
+
port: 4317
|
|
534
|
+
- protocol: TCP
|
|
535
|
+
port: 4318
|
|
512
536
|
```
|
|
513
537
|
|
|
514
538
|
```yaml title="k8s/components/hpa.yaml"
|
package/rules/k8s/lint/lint.mjs
CHANGED
|
@@ -14,8 +14,11 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { spawnSync } from 'node:child_process'
|
|
16
16
|
import { existsSync } from 'node:fs'
|
|
17
|
+
import { readFile } from 'node:fs/promises'
|
|
17
18
|
import { basename, dirname, join, relative } from 'node:path'
|
|
18
19
|
|
|
20
|
+
import { parse } from 'yaml'
|
|
21
|
+
|
|
19
22
|
import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
|
|
20
23
|
import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
|
|
21
24
|
import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
|
|
@@ -24,6 +27,9 @@ import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
|
24
27
|
/** Per-project kubescape exceptions file; підмішується через --exceptions, якщо існує в корені. */
|
|
25
28
|
const KUBESCAPE_EXCEPTIONS_FILE = '.kubescape-exceptions.json'
|
|
26
29
|
|
|
30
|
+
/** Назва kustomization-файлу (під `k8s` дозволено лише `.yaml`, див. k8s.mdc). */
|
|
31
|
+
const KUSTOMIZATION_FILE = 'kustomization.yaml'
|
|
32
|
+
|
|
27
33
|
const PATH_SEPARATOR_RE = /[/\\]/u
|
|
28
34
|
const YAML_EXT_RE = /\.yaml$/iu
|
|
29
35
|
|
|
@@ -134,37 +140,136 @@ export function buildKubescapeExceptionsArgs(root) {
|
|
|
134
140
|
}
|
|
135
141
|
|
|
136
142
|
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
143
|
+
* Знаходить каталоги-«точки входу» Kustomize під `dir` — ті, що містять `kustomization.yaml`,
|
|
144
|
+
* чий перший YAML-документ має `kind: Kustomization` (або без `kind` — типово Kustomization).
|
|
145
|
+
* Kustomize Components (`kind: Component`) пропускаються: вони не білдяться окремо,
|
|
146
|
+
* а підключаються через `components:` із overlay (див. k8s.mdc, секція «Kustomize: структура каталогів»).
|
|
139
147
|
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
148
|
+
* Семантика збігається з реальною поведінкою `kustomize build <dir>`: воно зчитує саме
|
|
149
|
+
* `kustomization.yaml`, тож пошук іде за назвою файлу (без `.yml`-варіанту — заборонено каноном).
|
|
150
|
+
* @param {string} dir абсолютний шлях до `…/k8s`
|
|
151
|
+
* @returns {Promise<string[]>} відсортовані абсолютні шляхи до dir-ів з білдабельним `kustomization.yaml`
|
|
152
|
+
*/
|
|
153
|
+
export async function findKustomizationDirs(dir) {
|
|
154
|
+
/** @type {string[]} */
|
|
155
|
+
const candidates = []
|
|
156
|
+
await walkDir(dir, p => {
|
|
157
|
+
if (basename(p) === KUSTOMIZATION_FILE) candidates.push(p)
|
|
158
|
+
})
|
|
159
|
+
/** @type {Set<string>} */
|
|
160
|
+
const result = new Set()
|
|
161
|
+
for (const p of candidates) {
|
|
162
|
+
let text
|
|
163
|
+
try {
|
|
164
|
+
text = await readFile(p, 'utf8')
|
|
165
|
+
} catch {
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
let doc
|
|
169
|
+
try {
|
|
170
|
+
doc = parse(text)
|
|
171
|
+
} catch {
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
if (doc && typeof doc === 'object' && doc.kind === 'Component') continue
|
|
175
|
+
result.add(dirname(p))
|
|
176
|
+
}
|
|
177
|
+
return [...result].toSorted((a, b) => a.localeCompare(b))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Запускає `kustomize build <dir>` і повертає stdout як буфер. stderr інхеритимо в термінал,
|
|
182
|
+
* щоб помилки збірки (биті посилання, недозволені плагіни) було видно одразу.
|
|
183
|
+
* @param {string} kustomizePath абсолютний шлях до бінарника kustomize
|
|
184
|
+
* @param {string} dir абсолютний шлях до каталогу з `kustomization.yaml`
|
|
185
|
+
* @returns {{ status: number, stdout: Buffer }} статус процесу і зібраний маніфест
|
|
186
|
+
*/
|
|
187
|
+
function runKustomizeBuild(kustomizePath, dir) {
|
|
188
|
+
const r = spawnSync(kustomizePath, ['build', dir], {
|
|
189
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
190
|
+
shell: false
|
|
191
|
+
})
|
|
192
|
+
return { status: r.status ?? 1, stdout: r.stdout ?? Buffer.alloc(0) }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Запускає `kubescape scan -` зі stdin (буфер `manifest`). stdout/stderr інхерит у термінал.
|
|
197
|
+
* @param {string} kubescapePath абсолютний шлях до бінарника kubescape
|
|
198
|
+
* @param {Buffer} manifest зібраний kustomize-маніфест
|
|
199
|
+
* @param {string[]} exceptionsArgs `['--exceptions', '<file>']` або `[]`
|
|
200
|
+
* @returns {{ status: number, enoent: boolean }} статус процесу і прапор ENOENT
|
|
201
|
+
*/
|
|
202
|
+
function runKubescapeStdin(kubescapePath, manifest, exceptionsArgs) {
|
|
203
|
+
const r = spawnSync(kubescapePath, ['scan', '-', '--severity-threshold', 'high', ...exceptionsArgs], {
|
|
204
|
+
input: manifest,
|
|
205
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
206
|
+
shell: false
|
|
207
|
+
})
|
|
208
|
+
const enoent = Boolean(r.error && 'code' in r.error && r.error.code === 'ENOENT')
|
|
209
|
+
return { status: r.status ?? 1, enoent }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Запускає kubescape по зібраному kustomize-маніфесту для кожного `…/k8s`-кореня. Для кожного
|
|
214
|
+
* dir-у з `kustomization.yaml` (крім `kind: Component`) робимо `kustomize build <dir>` і піпимо
|
|
215
|
+
* stdout у `kubescape scan -`. Це усуває false-positive C-0260 (`Missing network policy`) у випадках,
|
|
216
|
+
* коли NetworkPolicy живе у sibling `components/` без `metadata.namespace` (намспейс інжектить
|
|
217
|
+
* overlay через `kustomization.namespace`); сирий dir-скан не виконує kustomize й бачить порожній
|
|
218
|
+
* `namespace` у NetworkPolicy проти непорожнього у Deployment, через що `podSelector` не матчиться.
|
|
219
|
+
*
|
|
220
|
+
* Якщо в `…/k8s`-корені немає жодного білдабельного kustomization.yaml (проєкт без Kustomize) —
|
|
221
|
+
* fallback на старий dir-скан, щоб не блокувати чистий YAML-only набір маніфестів.
|
|
222
|
+
*
|
|
223
|
+
* Якщо в корені репо є `.kubescape-exceptions.json` — підмішується через `--exceptions <file>`
|
|
224
|
+
* (точкові винятки control'ів, напр. C-0012 на ConfigMap з публічним JWT-конфігом; див. k8s.mdc).
|
|
144
225
|
* @param {string[]} dirs абсолютні шляхи до `…/k8s`
|
|
145
226
|
* @param {string} root корінь репозиторію (для пошуку exceptions-файлу)
|
|
146
|
-
* @returns {number} 0 при успіху, інакше код
|
|
227
|
+
* @returns {Promise<number>} 0 при успіху, інакше код невдалого процесу або 127, якщо kubescape/kustomize відсутні
|
|
147
228
|
*/
|
|
148
|
-
function runKubescape(dirs, root) {
|
|
229
|
+
async function runKubescape(dirs, root) {
|
|
149
230
|
const exceptionsArgs = buildKubescapeExceptionsArgs(root)
|
|
150
231
|
if (exceptionsArgs.length > 0) {
|
|
151
232
|
console.log(`run-k8s: kubescape exceptions — ${KUBESCAPE_EXCEPTIONS_FILE}`)
|
|
152
233
|
}
|
|
234
|
+
const kubescapePath = resolveCmd('kubescape')
|
|
235
|
+
if (!kubescapePath) {
|
|
236
|
+
console.error('kubescape не знайдено в PATH. Встанови з https://github.com/kubescape/kubescape#readme')
|
|
237
|
+
return 127
|
|
238
|
+
}
|
|
239
|
+
let kustomizePath = null
|
|
153
240
|
for (const d of dirs) {
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
156
|
-
console.
|
|
157
|
-
|
|
241
|
+
const kdirs = await findKustomizationDirs(d)
|
|
242
|
+
if (kdirs.length === 0) {
|
|
243
|
+
console.log(`run-k8s: kubescape scan ${d} (без kustomization — сирий dir-скан)`)
|
|
244
|
+
const r = spawnSync(kubescapePath, ['scan', d, '--severity-threshold', 'high', ...exceptionsArgs], {
|
|
245
|
+
stdio: 'inherit',
|
|
246
|
+
shell: false
|
|
247
|
+
})
|
|
248
|
+
if (r.error && 'code' in r.error && r.error.code === 'ENOENT') {
|
|
249
|
+
console.error('kubescape не знайдено в PATH. Встанови з https://github.com/kubescape/kubescape#readme')
|
|
250
|
+
return 127
|
|
251
|
+
}
|
|
252
|
+
if (r.status !== 0) return r.status ?? 1
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
if (kustomizePath === null) {
|
|
256
|
+
kustomizePath = resolveCmd('kustomize')
|
|
257
|
+
if (!kustomizePath) {
|
|
258
|
+
console.error('kustomize не знайдено в PATH. Встанови з https://kubectl.docs.kubernetes.io/installation/kustomize/')
|
|
259
|
+
return 127
|
|
260
|
+
}
|
|
158
261
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
262
|
+
for (const kdir of kdirs) {
|
|
263
|
+
console.log(`run-k8s: kustomize build ${kdir} | kubescape scan -`)
|
|
264
|
+
const build = runKustomizeBuild(kustomizePath, kdir)
|
|
265
|
+
if (build.status !== 0) return build.status
|
|
266
|
+
const ks = runKubescapeStdin(kubescapePath, build.stdout, exceptionsArgs)
|
|
267
|
+
if (ks.enoent) {
|
|
268
|
+
console.error('kubescape не знайдено в PATH. Встанови з https://github.com/kubescape/kubescape#readme')
|
|
269
|
+
return 127
|
|
270
|
+
}
|
|
271
|
+
if (ks.status !== 0) return ks.status
|
|
166
272
|
}
|
|
167
|
-
if (r.status !== 0) return r.status ?? 1
|
|
168
273
|
}
|
|
169
274
|
return 0
|
|
170
275
|
}
|
|
@@ -190,7 +295,7 @@ export async function runLintK8s() {
|
|
|
190
295
|
const kc = runKubeconform(dirs)
|
|
191
296
|
if (kc !== 0) return kc
|
|
192
297
|
|
|
193
|
-
const ks = runKubescape(dirs, root)
|
|
298
|
+
const ks = await runKubescape(dirs, root)
|
|
194
299
|
return ks
|
|
195
300
|
}
|
|
196
301
|
|
|
@@ -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
|