@nitra/cursor 1.13.48 → 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 CHANGED
@@ -4,6 +4,12 @@
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
+
7
13
  ## [1.13.48] - 2026-05-19
8
14
 
9
15
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.13.48",
3
+ "version": "1.13.49",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
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.36'
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 і kubescape у **PATH**; не додавай їх у **devDependencies**.
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:** типово **`kubescape scan <каталог-k8s>`**; поріг серйозності підлаштуй під проєкт (наприклад **`--severity-threshold high`**). Перший запуск може завантажувати артефакти — у 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)).
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
  ```
@@ -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
- * Запускає kubescape scan для кожного каталогу окремо (узгоджено з прикладами CLI).
138
- * Немає прапорця версії Kubernetes за потреби додай `scan framework <ім’я>` під CIS/інші набори.
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
- * Якщо в корені проєкту є `.kubescape-exceptions.json` підмішується через `--exceptions <file>`.
141
- * Файл потрібен для точкових винятків control'ів kubescape (напр. C-0012 на ConfigMap, що містить
142
- * публічний JWT-конфіг типу `HASURA_GRAPHQL_JWT_SECRET={"jwk_url": "https://…"}` control тригериться
143
- * на ім'я env, а не на значення; див. приклад у `k8s.mdc`).
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 при успіху, інакше код останнього невдалого scan або 127, якщо kubescape відсутній у PATH
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 kubescapePath = resolveCmd('kubescape')
155
- if (!kubescapePath) {
156
- console.error('kubescape не знайдено в PATH. Встанови з https://github.com/kubescape/kubescape#readme')
157
- return 127
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
- const r = spawnSync(kubescapePath, ['scan', d, '--severity-threshold', 'high', ...exceptionsArgs], {
160
- stdio: 'inherit',
161
- shell: false
162
- })
163
- if (r.error && 'code' in r.error && r.error.code === 'ENOENT') {
164
- console.error('kubescape не знайдено в PATH. Встанови з https://github.com/kubescape/kubescape#readme')
165
- return 127
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