@nitra/cursor 1.13.49 → 1.13.51
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,18 @@
|
|
|
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.51] - 2026-05-19
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- `lint-k8s`: `kubescape scan -` (stdin), доданий у 1.13.49 і збережений у 1.13.50, **не працює в kubescape v4.x** — `-` трактується як шлях до файлу й сканер виходить з `no resources found to scan` (fatal), тож `bun run lint` падав на `lint-k8s` навіть на чистих маніфестах. Прапорця `--input`/`--stdin` у CLI також немає. Тепер `runKubescapeManifest` пише зібраний kustomize-маніфест у тимчасовий файл під `os.tmpdir()` (через `fs.mkdtempSync`) і запускає **`kubescape scan <tmp-file>`**; тимчасова директорія прибирається у `finally`. Bump `k8s.mdc` `1.38` → `1.39`.
|
|
12
|
+
|
|
13
|
+
## [1.13.50] - 2026-05-19
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- `lint-k8s`: kubescape тепер збирає kustomize-маніфест через **вшиту в kubectl підкоманду** — `kubectl kustomize <dir> | kubescape scan -` (замість окремого бінарника `kustomize build <dir>`, доданого в 1.13.49). Причина: на машинах без окремого `kustomize` lint-k8s падав з `kustomize не знайдено в PATH`, тоді як `kubectl` — штатний інструмент з вшитим Kustomize (рендеринг локальний, доступ до кластера не потрібен). PATH-залежність зведена з пари `kubectl+kustomize` до одного `kubectl`; крок `Install kustomize` у GHA-шаблоні `lint-k8s.yml` прибрано (на github-hosted runner'ах kubectl уже доступний). Bump `k8s.mdc` `1.37` → `1.38`.
|
|
18
|
+
|
|
7
19
|
## [1.13.49] - 2026-05-19
|
|
8
20
|
|
|
9
21
|
### Changed
|
package/package.json
CHANGED
|
@@ -40,8 +40,10 @@ all_run_text := concat("\n", [run_text |
|
|
|
40
40
|
run_text := step_run_to_text(step)
|
|
41
41
|
])
|
|
42
42
|
|
|
43
|
+
# conftest парсить YAML 1.1, тож канонічний `on:` без лапок стає булевим ключем
|
|
44
|
+
# `true` (як у `ga.lint_ga`). Тому читаємо через `input["true"]`.
|
|
43
45
|
push_paths_set := {p |
|
|
44
|
-
some p in object.get(object.get(object.get(input, "
|
|
46
|
+
some p in object.get(object.get(object.get(input, "true", {}), "push", {}), "paths", [])
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
# ── deny: on.push.paths subset-of ──────────────────────────────────────
|
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.39'
|
|
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 і
|
|
32
|
+
**Залежності:** виконувані файли kubeconform, kubescape і kubectl у **PATH** (kustomize використовуємо як вшиту підкоманду **`kubectl kustomize`** — окремий бінарник `kustomize` не потрібен); не додавай їх у **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 — вхід через зібраний kustomize-маніфест:** для кожного dir-у з `kustomization.yaml` (`kind: Kustomization`; **`kind: Component`** пропускається — він не білдиться окремо) `lint-k8s` виконує **`kustomize
|
|
36
|
+
**kubescape — вхід через зібраний kustomize-маніфест:** для кожного dir-у з `kustomization.yaml` (`kind: Kustomization`; **`kind: Component`** пропускається — він не білдиться окремо) `lint-k8s` виконує **`kubectl kustomize <dir>`** і передає stdout у **`kubescape scan <tmp-file>`** з порогом **`--severity-threshold high`** (вбудована в kubectl підкоманда `kustomize` — окремий бінарник `kustomize` не потрібен; рендеринг локальний і не потребує доступу до кластера). Маніфест проходить через тимчасовий файл, бо **`kubescape scan` у v4.x не читає stdin** (`-` як шлях → `no resources found to scan`; прапорця `--input`/`--stdin` немає); тимчасова директорія створюється під `os.tmpdir()` і прибирається після скану. Це усуває 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,10 +99,8 @@ 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
|
-
|
|
103
|
-
|
|
104
|
-
curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
|
|
105
|
-
sudo mv kustomize /usr/local/bin/
|
|
102
|
+
# kustomize не встановлюємо окремо — використовуємо вбудовану підкоманду `kubectl kustomize`
|
|
103
|
+
# (kubectl уже доступний на github-hosted runner'ах: https://github.com/actions/runner-images).
|
|
106
104
|
|
|
107
105
|
- name: Lint K8s
|
|
108
106
|
run: bun run lint-k8s
|
package/rules/k8s/lint/lint.mjs
CHANGED
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* Kubescape не має аналога цього прапорця; орієнтир цільового кластера — та сама лінія релізу (див. k8s.mdc).
|
|
14
14
|
*/
|
|
15
15
|
import { spawnSync } from 'node:child_process'
|
|
16
|
-
import { existsSync } from 'node:fs'
|
|
16
|
+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
17
17
|
import { readFile } from 'node:fs/promises'
|
|
18
|
+
import { tmpdir } from 'node:os'
|
|
18
19
|
import { basename, dirname, join, relative } from 'node:path'
|
|
19
20
|
|
|
20
21
|
import { parse } from 'yaml'
|
|
@@ -178,14 +179,16 @@ export async function findKustomizationDirs(dir) {
|
|
|
178
179
|
}
|
|
179
180
|
|
|
180
181
|
/**
|
|
181
|
-
* Запускає `kustomize
|
|
182
|
-
* щоб помилки збірки (биті посилання, недозволені плагіни) було видно одразу.
|
|
183
|
-
*
|
|
182
|
+
* Запускає `kubectl kustomize <dir>` і повертає stdout як буфер. stderr інхеритимо в термінал,
|
|
183
|
+
* щоб помилки збірки (биті посилання, недозволені плагіни) було видно одразу. Використовуємо
|
|
184
|
+
* вшитий у kubectl kustomize замість окремого бінарника — kubectl є штатним інструментом і не
|
|
185
|
+
* потребує доступу до кластера для підкоманди `kustomize` (рендеринг локальний).
|
|
186
|
+
* @param {string} kubectlPath абсолютний шлях до бінарника kubectl
|
|
184
187
|
* @param {string} dir абсолютний шлях до каталогу з `kustomization.yaml`
|
|
185
188
|
* @returns {{ status: number, stdout: Buffer }} статус процесу і зібраний маніфест
|
|
186
189
|
*/
|
|
187
|
-
function runKustomizeBuild(
|
|
188
|
-
const r = spawnSync(
|
|
190
|
+
function runKustomizeBuild(kubectlPath, dir) {
|
|
191
|
+
const r = spawnSync(kubectlPath, ['kustomize', dir], {
|
|
189
192
|
stdio: ['ignore', 'pipe', 'inherit'],
|
|
190
193
|
shell: false
|
|
191
194
|
})
|
|
@@ -193,29 +196,41 @@ function runKustomizeBuild(kustomizePath, dir) {
|
|
|
193
196
|
}
|
|
194
197
|
|
|
195
198
|
/**
|
|
196
|
-
* Запускає `kubescape scan
|
|
199
|
+
* Запускає `kubescape scan <file>` для зібраного kustomize-маніфесту. stdout/stderr інхерит у термінал.
|
|
200
|
+
*
|
|
201
|
+
* Маніфест пишемо в тимчасовий файл, бо `kubescape scan` у v4.x **не читає stdin**: `-` як шлях
|
|
202
|
+
* не розпізнається (`no resources found to scan`), а прапорця типу `--input`/`--stdin` у CLI немає
|
|
203
|
+
* (`kubescape scan --help` показує лише шляхи й кластер). Тимчасова директорія створюється
|
|
204
|
+
* через `mkdtempSync` під `os.tmpdir()` і прибирається в `finally`.
|
|
197
205
|
* @param {string} kubescapePath абсолютний шлях до бінарника kubescape
|
|
198
206
|
* @param {Buffer} manifest зібраний kustomize-маніфест
|
|
199
207
|
* @param {string[]} exceptionsArgs `['--exceptions', '<file>']` або `[]`
|
|
200
208
|
* @returns {{ status: number, enoent: boolean }} статус процесу і прапор ENOENT
|
|
201
209
|
*/
|
|
202
|
-
function
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
+
function runKubescapeManifest(kubescapePath, manifest, exceptionsArgs) {
|
|
211
|
+
const dir = mkdtempSync(join(tmpdir(), 'nitra-cursor-k8s-'))
|
|
212
|
+
const file = join(dir, 'manifest.yaml')
|
|
213
|
+
try {
|
|
214
|
+
writeFileSync(file, manifest)
|
|
215
|
+
const r = spawnSync(kubescapePath, ['scan', file, '--severity-threshold', 'high', ...exceptionsArgs], {
|
|
216
|
+
stdio: 'inherit',
|
|
217
|
+
shell: false
|
|
218
|
+
})
|
|
219
|
+
const enoent = Boolean(r.error && 'code' in r.error && r.error.code === 'ENOENT')
|
|
220
|
+
return { status: r.status ?? 1, enoent }
|
|
221
|
+
} finally {
|
|
222
|
+
rmSync(dir, { recursive: true, force: true })
|
|
223
|
+
}
|
|
210
224
|
}
|
|
211
225
|
|
|
212
226
|
/**
|
|
213
227
|
* Запускає kubescape по зібраному kustomize-маніфесту для кожного `…/k8s`-кореня. Для кожного
|
|
214
|
-
* dir-у з `kustomization.yaml` (крім `kind: Component`) робимо `kustomize
|
|
215
|
-
* stdout у `kubescape scan
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
* `namespace`
|
|
228
|
+
* dir-у з `kustomization.yaml` (крім `kind: Component`) робимо `kubectl kustomize <dir>` і
|
|
229
|
+
* передаємо stdout у `kubescape scan <tmp-file>` через тимчасовий файл (kubescape v4.x не читає
|
|
230
|
+
* stdin — див. `runKubescapeManifest`). Це усуває false-positive C-0260 (`Missing network policy`)
|
|
231
|
+
* у випадках, коли NetworkPolicy живе у sibling `components/` без `metadata.namespace` (намспейс
|
|
232
|
+
* інжектить overlay через `kustomization.namespace`); сирий dir-скан не виконує kustomize й бачить
|
|
233
|
+
* порожній `namespace` у NetworkPolicy проти непорожнього у Deployment, через що `podSelector` не матчиться.
|
|
219
234
|
*
|
|
220
235
|
* Якщо в `…/k8s`-корені немає жодного білдабельного kustomization.yaml (проєкт без Kustomize) —
|
|
221
236
|
* fallback на старий dir-скан, щоб не блокувати чистий YAML-only набір маніфестів.
|
|
@@ -224,7 +239,7 @@ function runKubescapeStdin(kubescapePath, manifest, exceptionsArgs) {
|
|
|
224
239
|
* (точкові винятки control'ів, напр. C-0012 на ConfigMap з публічним JWT-конфігом; див. k8s.mdc).
|
|
225
240
|
* @param {string[]} dirs абсолютні шляхи до `…/k8s`
|
|
226
241
|
* @param {string} root корінь репозиторію (для пошуку exceptions-файлу)
|
|
227
|
-
* @returns {Promise<number>} 0 при успіху, інакше код невдалого процесу або 127, якщо kubescape/
|
|
242
|
+
* @returns {Promise<number>} 0 при успіху, інакше код невдалого процесу або 127, якщо kubescape/kubectl відсутні
|
|
228
243
|
*/
|
|
229
244
|
async function runKubescape(dirs, root) {
|
|
230
245
|
const exceptionsArgs = buildKubescapeExceptionsArgs(root)
|
|
@@ -236,7 +251,7 @@ async function runKubescape(dirs, root) {
|
|
|
236
251
|
console.error('kubescape не знайдено в PATH. Встанови з https://github.com/kubescape/kubescape#readme')
|
|
237
252
|
return 127
|
|
238
253
|
}
|
|
239
|
-
let
|
|
254
|
+
let kubectlPath = null
|
|
240
255
|
for (const d of dirs) {
|
|
241
256
|
const kdirs = await findKustomizationDirs(d)
|
|
242
257
|
if (kdirs.length === 0) {
|
|
@@ -252,18 +267,18 @@ async function runKubescape(dirs, root) {
|
|
|
252
267
|
if (r.status !== 0) return r.status ?? 1
|
|
253
268
|
continue
|
|
254
269
|
}
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
if (!
|
|
258
|
-
console.error('
|
|
270
|
+
if (kubectlPath === null) {
|
|
271
|
+
kubectlPath = resolveCmd('kubectl')
|
|
272
|
+
if (!kubectlPath) {
|
|
273
|
+
console.error('kubectl не знайдено в PATH. Встанови з https://kubernetes.io/docs/tasks/tools/#kubectl')
|
|
259
274
|
return 127
|
|
260
275
|
}
|
|
261
276
|
}
|
|
262
277
|
for (const kdir of kdirs) {
|
|
263
|
-
console.log(`run-k8s: kustomize
|
|
264
|
-
const build = runKustomizeBuild(
|
|
278
|
+
console.log(`run-k8s: kubectl kustomize ${kdir} | kubescape scan <tmp>`)
|
|
279
|
+
const build = runKustomizeBuild(kubectlPath, kdir)
|
|
265
280
|
if (build.status !== 0) return build.status
|
|
266
|
-
const ks =
|
|
281
|
+
const ks = runKubescapeManifest(kubescapePath, build.stdout, exceptionsArgs)
|
|
267
282
|
if (ks.enoent) {
|
|
268
283
|
console.error('kubescape не знайдено в PATH. Встанови з https://github.com/kubescape/kubescape#readme')
|
|
269
284
|
return 127
|
|
@@ -19,6 +19,10 @@ expected_runs_on := data.template.snippet.jobs.text["runs-on"]
|
|
|
19
19
|
|
|
20
20
|
expected_perms := data.template.snippet.jobs.text.permissions
|
|
21
21
|
|
|
22
|
+
# conftest парсить YAML 1.1, де канонічний `on:` без лапок стає булевим ключем
|
|
23
|
+
# `true` (як у `ga.lint_ga`). Тому читаємо on-блок через `input["true"]`.
|
|
24
|
+
gha_on := input["true"]
|
|
25
|
+
|
|
22
26
|
job := input.jobs.text
|
|
23
27
|
|
|
24
28
|
job_uses_set contains job.steps[_].uses
|
|
@@ -45,17 +49,17 @@ deny contains msg if {
|
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
deny contains msg if {
|
|
48
|
-
not branches_superset_of(
|
|
52
|
+
not branches_superset_of(gha_on.push.branches, expected_push_branches)
|
|
49
53
|
msg := "lint-text.yml: on.push.branches має містити dev і main (text.mdc)"
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
deny contains msg if {
|
|
53
|
-
not branches_superset_of(
|
|
57
|
+
not branches_superset_of(gha_on.pull_request.branches, expected_pr_branches)
|
|
54
58
|
msg := "lint-text.yml: on.pull_request.branches має містити dev і main (text.mdc)"
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
deny contains msg if {
|
|
58
|
-
not paths_superset_of(
|
|
62
|
+
not paths_superset_of(gha_on.push.paths, expected_push_paths)
|
|
59
63
|
msg := "lint-text.yml: on.push.paths має містити очікувані glob-и (text.mdc)"
|
|
60
64
|
}
|
|
61
65
|
|