@nitra/cursor 1.13.31 → 1.13.38
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 +47 -1
- package/package.json +1 -1
- package/rules/changelog/fix/consistency/check.mjs +100 -85
- package/rules/ci4/ci4.mdc +7 -7
- package/rules/image-avif/image-avif.mdc +2 -2
- package/rules/js-lint/policy/vscode_extensions/template/extensions.json.snippet.json +1 -5
- package/rules/js-run/fix/runtime/check.mjs +3 -0
- package/rules/js-run/js-run.mdc +16 -1
- package/rules/js-run/policy/package_json/package_json.rego +17 -0
- package/rules/js-run/policy/package_json/template/package.json.deny.json +13 -1
- package/rules/k8s/fix/kubescape_exceptions/template/.kubescape-exceptions.json.snippet.json +21 -0
- package/rules/k8s/fix/manifests/check.mjs +775 -139
- package/rules/k8s/k8s.mdc +60 -6
- package/rules/k8s/lint/lint.mjs +29 -4
- package/rules/k8s/policy/base_kustomization/base_kustomization.rego +13 -6
- package/rules/k8s/policy/network_policy/network_policy.rego +158 -0
- package/rules/k8s/policy/network_policy/template/networkpolicy.snippet.yaml +32 -0
- package/rules/security/fix/trufflehog/check.mjs +3 -0
- package/rules/security/policy/package_json/template/package.json.snippet.json +5 -1
- package/rules/style-lint/style-lint.mdc +20 -1
- package/rules/text/policy/cspell/cspell.rego +1 -1
- package/rules/text/policy/markdownlint/markdownlint.rego +1 -1
- package/rules/text/policy/oxfmtrc/template/.oxfmtrc.json.snippet.json +1 -5
- package/rules/text/policy/vscode_extensions/template/extensions.json.snippet.json +1 -5
- package/rules/vue/vue.mdc +1 -0
- package/scripts/sync-claude-config.mjs +2 -2
- package/scripts/utils/check-mdc-template-refs.mjs +15 -5
- package/scripts/utils/inline-template-links.mjs +15 -8
- package/scripts/utils/package-manifest.mjs +24 -19
- package/scripts/utils/run-conftest-batch.mjs +22 -15
- package/scripts/utils/template.mjs +89 -21
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.34'
|
|
4
4
|
globs: "**/k8s/**/*.yaml"
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
@@ -35,6 +35,14 @@ alwaysApply: false
|
|
|
35
35
|
|
|
36
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)).
|
|
37
37
|
|
|
38
|
+
### Винятки kubescape: `.kubescape-exceptions.json`
|
|
39
|
+
|
|
40
|
+
Якщо в **корені проєкту** є файл **`.kubescape-exceptions.json`** — `lint-k8s` автоматично передає його в `kubescape scan` через **`--exceptions`** ([postureExceptionPolicy](https://github.com/kubescape/kubescape/blob/master/docs/exceptions.md)). Файл — JSON-масив об'єктів з полями `name`, `policyType: "postureExceptionPolicy"`, `actions` (`["alertOnly"]` — знижує fail до alert, не блокує lint), `resources` (resource designator) і `posturePolicies` (масив `controlID`).
|
|
41
|
+
|
|
42
|
+
Канонічний кейс — **C-0012** (`Applications credentials in configuration files`, High): control тригериться на **імʼя** env, що містить підрядок `secret`/`password`/`key`/`token`, а **не** на значення. Для `HASURA_GRAPHQL_JWT_SECRET` у ConfigMap значення — публічний JWT-конфіг (`jwk_url`, `issuer`), не credentials, але kubescape падає лише через імʼя. Точкове виключення для ConfigMap із цим env — канон: [.kubescape-exceptions.json.snippet.json](./fix/kubescape_exceptions/template/.kubescape-exceptions.json.snippet.json)
|
|
43
|
+
|
|
44
|
+
Підстав свою `attributes.name` (рядок або regex), якщо ConfigMap зветься інакше; виключай контрольно, а не глобально (не додавай винятки без `attributes.name`/`labels`, бо тоді C-0012 знімається для усіх ConfigMap-ів проєкту і реальні витоки credentials теж пройдуть).
|
|
45
|
+
|
|
38
46
|
У репозиторії пакета **`@nitra/cursor`** скрипт **`lint-k8s`** делегує до CLI **`n-cursor lint-k8s`** (реалізація — **`npm/rules/k8s/js/run.mjs`**). У інших проєктах достатньо встановити **`@nitra/cursor`** у `devDependencies` — бінарка **`n-cursor`** буде у **`node_modules/.bin/`**.
|
|
39
47
|
|
|
40
48
|
```json title="package.json"
|
|
@@ -110,7 +118,7 @@ resources:
|
|
|
110
118
|
memory: '128Mi'
|
|
111
119
|
```
|
|
112
120
|
|
|
113
|
-
**HPA і
|
|
121
|
+
**HPA, PDB і NetworkPolicy у base не тримаємо**: ні локальних `hpa.yaml` / `pdb.yaml` / `networkpolicy.yaml` поруч із workload-маніфестами, ні через `resources` / `components` / `bases`. Канон — sibling каталог **`components/`** (Kustomize Component) поруч з `base/` (див. розділ нижче).
|
|
114
122
|
|
|
115
123
|
### Поза base (оверлеї, окремі каталоги)
|
|
116
124
|
|
|
@@ -372,12 +380,15 @@ images:
|
|
|
372
380
|
|
|
373
381
|
## Deployment: `topologySpreadConstraints`, HPA / PDB через `components/`
|
|
374
382
|
|
|
375
|
-
Для **кожного** `kind: Deployment` у каталозі **`…/k8s/…/base/`** (у будь-якому файлі `.yaml`, наприклад **`deploy.yaml`**, **`deployment.yaml`**) сам Deployment має канонічні **`spec.template.spec.topologySpreadConstraints`**, а **HPA і PDB** живуть у **sibling каталозі** **`…/k8s/…/components/`** (Kustomize Component, фіксована назва каталогу — `components`). У `base/` локальні `hpa.yaml` і `pdb.yaml` **заборонені** (file-existence error). У дереві base-kustomize HPA / PDB також **не дозволені** через `resources` / `components` / `bases`.
|
|
383
|
+
Для **кожного** `kind: Deployment` у каталозі **`…/k8s/…/base/`** (у будь-якому файлі `.yaml`, наприклад **`deploy.yaml`**, **`deployment.yaml`**) сам Deployment має канонічні **`spec.template.spec.topologySpreadConstraints`**, а **HPA і PDB** живуть у **sibling каталозі** **`…/k8s/…/components/`** (Kustomize Component, фіксована назва каталогу — `components`). У `base/` локальні `hpa.yaml`, `networkpolicy.yaml` і `pdb.yaml` **заборонені** (file-existence error). У дереві base-kustomize HPA / PDB / NetworkPolicy також **не дозволені** через `resources` / `components` / `bases`.
|
|
384
|
+
|
|
385
|
+
Для **кожного** з **`Deployment`**, **`StatefulSet`**, **`DaemonSet`**, **`Job`**, **`CronJob`** під `k8s` обов'язковий **NetworkPolicy**: у **`…/k8s/…/base/`** — у **`components/networkpolicy.yaml`** (multi-doc, якщо workload-ів кілька); у **не-base** — **`networkpolicy.yaml`** поруч із маніфестом workload у тому ж каталозі. `metadata.name` NetworkPolicy **= `metadata.name`** workload; `spec.podSelector.matchLabels.app` **= мітка `app`** з `spec.selector.matchLabels` (для **CronJob** — з `spec.jobTemplate.spec.selector.matchLabels`). Відсутні документи **`check k8s`** створює автоматично.
|
|
376
386
|
|
|
377
387
|
**Канонічна структура `<pkg>/k8s/components/`** (sibling до `base/`):
|
|
378
388
|
|
|
379
|
-
- **`kustomization.yaml`** — `apiVersion: kustomize.config.k8s.io/v1alpha1`, `kind: Component`, `resources: [hpa.yaml, pdb.yaml]
|
|
389
|
+
- **`kustomization.yaml`** — `apiVersion: kustomize.config.k8s.io/v1alpha1`, `kind: Component`, `resources: [hpa.yaml, networkpolicy.yaml, pdb.yaml]` (відсортовано за алфавітом).
|
|
380
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`); **інші порти** — лише in-cluster через `to.namespaceSelector: {}` (трафік на `*.svc` / Pod-и в кластері; Postgres лише `*.svc`, без Cloud SQL). Заборонено `egress: [{}]`. Канон: [networkpolicy.snippet.yaml](./policy/network_policy/template/networkpolicy.snippet.yaml). Якщо документа для workload немає — **`check k8s`** дописує його автоматично.
|
|
381
392
|
- **`pdb.yaml`** — `policy/v1`, `PodDisruptionBudget`, **без** `metadata.namespace`, `spec.selector.matchLabels.app` **= `spec.selector.matchLabels.app`** Deployment, dev-like `minAvailable: 0`.
|
|
382
393
|
|
|
383
394
|
Інші назви каталогу (`scale/`, `hpa-component/`, `pdb-component/`) — fail.
|
|
@@ -386,13 +397,14 @@ images:
|
|
|
386
397
|
|
|
387
398
|
**`<pkg>/k8s/components/kustomization.yaml`** має `kind: Component` (не `kind: Kustomization`) — це **джерело** канонічних HPA/PDB для всіх overlays, а не overlay сам по собі. Прод-перезаписи (`/spec/minReplicas`, `/spec/maxReplicas`, `/spec/minAvailable`) живуть у `<env>/kustomization.yaml`, що підключає Component через `components:`. У самому Component patches не потрібні — він env-неутральний; **`check k8s`** не вимагає прод-патчів від `components/kustomization.yaml`.
|
|
388
399
|
|
|
389
|
-
У **не-base** оверлеях (без `components/`) поруч із `Deployment` лишається звична схема:
|
|
400
|
+
У **не-base** оверлеях (без `components/`) поруч із `Deployment` лишається звична схема: окремі **`hpa.yaml`**, **`networkpolicy.yaml`** і **`pdb.yaml`**, якщо такі потрібні для цього середовища. Для **StatefulSet**, **DaemonSet**, **Job**, **CronJob** у не-base — обов'язковий лише **`networkpolicy.yaml`** (HPA/PDB лишаються прив'язаними до **Deployment**, якщо є). **`check k8s`** звіряє прив'язку за іменами (і **дописує** відсутні NetworkPolicy-документи автоматично):
|
|
390
401
|
|
|
391
402
|
- **`hpa.yaml`** (поза **`…/base/`**) — `autoscaling/v2`, `HorizontalPodAutoscaler`, `spec.scaleTargetRef.name` **= `metadata.name`** Deployment.
|
|
403
|
+
- **`networkpolicy.yaml`** — той самий канон egress/ingress, що в `components/` (multi-doc, якщо у каталозі кілька workload-ів).
|
|
392
404
|
- **`pdb.yaml`** — `policy/v1`, `PodDisruptionBudget`, `spec.selector.matchLabels.app` **= `spec.selector.matchLabels.app`** Deployment.
|
|
393
405
|
- **`topologySpreadConstraints`** — запис з `maxSkew: 1`, `topologyKey: kubernetes.io/hostname`, `whenUnsatisfiable: ScheduleAnyway`, `labelSelector.matchLabels.app` рівне тій самій мітці `app`.
|
|
394
406
|
|
|
395
|
-
**Перевірка структури `components/`** (для кожного Deployment у `base/`): наявність каталогу, валідний `kustomization.yaml` як Component, `hpa.yaml` і `pdb.yaml` з відповідністю до Deployment-name / app-label. Алгоритм — функція `validateComponentsForBaseDeployment` у **`check-k8s.mjs`**.
|
|
407
|
+
**Перевірка структури `components/`** (для кожного Deployment у `base/`): наявність каталогу, валідний `kustomization.yaml` як Component, `hpa.yaml`, `networkpolicy.yaml` і `pdb.yaml` з відповідністю до Deployment-name / app-label. Алгоритм — функція `validateComponentsForBaseDeployment` у **`check-k8s.mjs`**.
|
|
396
408
|
|
|
397
409
|
**Локальні шляхи в `kustomization.yaml`:** кожен запис без `://` (remote) з `resources` / `bases` / `components` / `crds`, `patchesStrategicMerge`, `patches[].path`, `patchesJson6902[].path`, `configurations[]`, `replacements[].path` має вказувати на **існуючий** у репозиторії файл (`.yaml` / `.yml`) або **каталог**; биті посилання — помилка **`check k8s`**.
|
|
398
410
|
|
|
@@ -454,9 +466,51 @@ apiVersion: kustomize.config.k8s.io/v1alpha1
|
|
|
454
466
|
kind: Component
|
|
455
467
|
resources:
|
|
456
468
|
- hpa.yaml
|
|
469
|
+
- networkpolicy.yaml
|
|
457
470
|
- pdb.yaml
|
|
458
471
|
```
|
|
459
472
|
|
|
473
|
+
```yaml title="k8s/components/networkpolicy.yaml"
|
|
474
|
+
# yaml-language-server: $schema=https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.33.9-standalone-strict/networkpolicy-networking-k8s-io-v1.json
|
|
475
|
+
apiVersion: networking.k8s.io/v1
|
|
476
|
+
kind: NetworkPolicy
|
|
477
|
+
metadata:
|
|
478
|
+
name: backend-api
|
|
479
|
+
spec:
|
|
480
|
+
podSelector:
|
|
481
|
+
matchLabels:
|
|
482
|
+
app: backend-api
|
|
483
|
+
policyTypes:
|
|
484
|
+
- Ingress
|
|
485
|
+
- Egress
|
|
486
|
+
ingress:
|
|
487
|
+
- from:
|
|
488
|
+
- podSelector: {}
|
|
489
|
+
egress:
|
|
490
|
+
- to:
|
|
491
|
+
- namespaceSelector:
|
|
492
|
+
matchLabels:
|
|
493
|
+
kubernetes.io/metadata.name: kube-system
|
|
494
|
+
podSelector:
|
|
495
|
+
matchLabels:
|
|
496
|
+
k8s-app: kube-dns
|
|
497
|
+
ports:
|
|
498
|
+
- protocol: UDP
|
|
499
|
+
port: 53
|
|
500
|
+
- protocol: TCP
|
|
501
|
+
port: 53
|
|
502
|
+
- to:
|
|
503
|
+
- ipBlock:
|
|
504
|
+
cidr: 0.0.0.0/0
|
|
505
|
+
ports:
|
|
506
|
+
- protocol: TCP
|
|
507
|
+
port: 80
|
|
508
|
+
- protocol: TCP
|
|
509
|
+
port: 443
|
|
510
|
+
- to:
|
|
511
|
+
- namespaceSelector: {}
|
|
512
|
+
```
|
|
513
|
+
|
|
460
514
|
```yaml title="k8s/components/hpa.yaml"
|
|
461
515
|
# yaml-language-server: $schema=https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.33.9-standalone-strict/horizontalpodautoscaler-autoscaling-v2.json
|
|
462
516
|
apiVersion: autoscaling/v2
|
package/rules/k8s/lint/lint.mjs
CHANGED
|
@@ -13,13 +13,17 @@
|
|
|
13
13
|
* Kubescape не має аналога цього прапорця; орієнтир цільового кластера — та сама лінія релізу (див. k8s.mdc).
|
|
14
14
|
*/
|
|
15
15
|
import { spawnSync } from 'node:child_process'
|
|
16
|
-
import {
|
|
16
|
+
import { existsSync } from 'node:fs'
|
|
17
|
+
import { basename, dirname, join, relative } from 'node:path'
|
|
17
18
|
|
|
18
19
|
import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
|
|
19
20
|
import { loadCursorIgnorePaths } from '../../../scripts/utils/load-cursor-config.mjs'
|
|
20
21
|
import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
|
|
21
22
|
import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
22
23
|
|
|
24
|
+
/** Per-project kubescape exceptions file; підмішується через --exceptions, якщо існує в корені. */
|
|
25
|
+
const KUBESCAPE_EXCEPTIONS_FILE = '.kubescape-exceptions.json'
|
|
26
|
+
|
|
23
27
|
const PATH_SEPARATOR_RE = /[/\\]/u
|
|
24
28
|
const YAML_EXT_RE = /\.yaml$/iu
|
|
25
29
|
|
|
@@ -118,20 +122,41 @@ function runKubeconform(dirs) {
|
|
|
118
122
|
return r.status ?? 1
|
|
119
123
|
}
|
|
120
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Будує аргументи `--exceptions <file>` для kubescape, якщо в корені проєкту є
|
|
127
|
+
* `.kubescape-exceptions.json`. Інакше — порожній масив.
|
|
128
|
+
* @param {string} root корінь репозиторію
|
|
129
|
+
* @returns {string[]} `['--exceptions', '<abs-path>']` або `[]`
|
|
130
|
+
*/
|
|
131
|
+
export function buildKubescapeExceptionsArgs(root) {
|
|
132
|
+
const exceptionsPath = join(root, KUBESCAPE_EXCEPTIONS_FILE)
|
|
133
|
+
return existsSync(exceptionsPath) ? ['--exceptions', exceptionsPath] : []
|
|
134
|
+
}
|
|
135
|
+
|
|
121
136
|
/**
|
|
122
137
|
* Запускає kubescape scan для кожного каталогу окремо (узгоджено з прикладами CLI).
|
|
123
138
|
* Немає прапорця версії Kubernetes — за потреби додай `scan framework <ім’я>` під CIS/інші набори.
|
|
139
|
+
*
|
|
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`).
|
|
124
144
|
* @param {string[]} dirs абсолютні шляхи до `…/k8s`
|
|
145
|
+
* @param {string} root корінь репозиторію (для пошуку exceptions-файлу)
|
|
125
146
|
* @returns {number} 0 при успіху, інакше код останнього невдалого scan або 127, якщо kubescape відсутній у PATH
|
|
126
147
|
*/
|
|
127
|
-
function runKubescape(dirs) {
|
|
148
|
+
function runKubescape(dirs, root) {
|
|
149
|
+
const exceptionsArgs = buildKubescapeExceptionsArgs(root)
|
|
150
|
+
if (exceptionsArgs.length > 0) {
|
|
151
|
+
console.log(`run-k8s: kubescape exceptions — ${KUBESCAPE_EXCEPTIONS_FILE}`)
|
|
152
|
+
}
|
|
128
153
|
for (const d of dirs) {
|
|
129
154
|
const kubescapePath = resolveCmd('kubescape')
|
|
130
155
|
if (!kubescapePath) {
|
|
131
156
|
console.error('kubescape не знайдено в PATH. Встанови з https://github.com/kubescape/kubescape#readme')
|
|
132
157
|
return 127
|
|
133
158
|
}
|
|
134
|
-
const r = spawnSync(kubescapePath, ['scan', d, '--severity-threshold', 'high'], {
|
|
159
|
+
const r = spawnSync(kubescapePath, ['scan', d, '--severity-threshold', 'high', ...exceptionsArgs], {
|
|
135
160
|
stdio: 'inherit',
|
|
136
161
|
shell: false
|
|
137
162
|
})
|
|
@@ -165,7 +190,7 @@ export async function runLintK8s() {
|
|
|
165
190
|
const kc = runKubeconform(dirs)
|
|
166
191
|
if (kc !== 0) return kc
|
|
167
192
|
|
|
168
|
-
const ks = runKubescape(dirs)
|
|
193
|
+
const ks = runKubescape(dirs, root)
|
|
169
194
|
return ks
|
|
170
195
|
}
|
|
171
196
|
|
|
@@ -41,16 +41,16 @@ deny contains base_namespace_required_msg if {
|
|
|
41
41
|
# (із зануренням у вкладені kustomization.yaml) — JS-оркестратор
|
|
42
42
|
# `verifyK8sBaseKustomizeHasNoHpaPdb` у `check-k8s.mjs` (потребує fs-доступу). Цей
|
|
43
43
|
# rego-deny — defense-in-depth: спрацює навіть якщо JS-крок упаде з винятку раніше.
|
|
44
|
-
deny contains
|
|
44
|
+
deny contains hpa_pdb_np_in_base_resources_msg(r) if {
|
|
45
45
|
is_kustomization
|
|
46
46
|
some r in object.get(input, "resources", [])
|
|
47
47
|
is_string(r)
|
|
48
|
-
|
|
48
|
+
is_hpa_pdb_or_np_filename(r)
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
hpa_pdb_np_in_base_resources_msg(file) := sprintf(
|
|
52
52
|
concat("", [
|
|
53
|
-
"у base/kustomization.yaml `resources:` містить '%v' — HPA/PDB заборонені у base, ",
|
|
53
|
+
"у base/kustomization.yaml `resources:` містить '%v' — HPA/PDB/NetworkPolicy заборонені у base, ",
|
|
54
54
|
"перенесіть у sibling каталог components/ і підключайте з overlay (k8s.mdc)",
|
|
55
55
|
]),
|
|
56
56
|
[file],
|
|
@@ -61,8 +61,15 @@ is_kustomization if {
|
|
|
61
61
|
startswith(object.get(input, "apiVersion", ""), "kustomize.config.k8s.io/")
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
basename(p) in {
|
|
64
|
+
is_hpa_pdb_or_np_filename(p) if {
|
|
65
|
+
basename(p) in {
|
|
66
|
+
"hpa.yaml",
|
|
67
|
+
"pdb.yaml",
|
|
68
|
+
"networkpolicy.yaml",
|
|
69
|
+
"hpa.yml",
|
|
70
|
+
"pdb.yml",
|
|
71
|
+
"networkpolicy.yml",
|
|
72
|
+
}
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
basename(p) := parts[count(parts) - 1] if {
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Пер-документна структурна перевірка NetworkPolicy (k8s.mdc).
|
|
2
|
+
# Cross-file (metadata.name = workload, podSelector.app = мітка app) — JS
|
|
3
|
+
# (`networkPolicyManifestViolations`, `validateNetworkPolicyForWorkload`).
|
|
4
|
+
#
|
|
5
|
+
# Канон egress: kube-dns; TCP 80/443 на 0.0.0.0/0; інші порти — namespaceSelector: {}
|
|
6
|
+
# (in-cluster, зокрема *.svc). Заборонено egress: [{}] (allow-all).
|
|
7
|
+
#
|
|
8
|
+
# Запуск:
|
|
9
|
+
# conftest test path/to/networkpolicy.yaml -p npm/policy/k8s/network_policy \
|
|
10
|
+
# --namespace k8s.network_policy
|
|
11
|
+
package k8s.network_policy
|
|
12
|
+
|
|
13
|
+
import rego.v1
|
|
14
|
+
|
|
15
|
+
np_kind_template := "kind має бути NetworkPolicy (зараз: %v) (k8s.mdc)"
|
|
16
|
+
|
|
17
|
+
np_api_template := "apiVersion має бути networking.k8s.io/v1 (зараз: %v) (k8s.mdc)"
|
|
18
|
+
|
|
19
|
+
deny contains msg if {
|
|
20
|
+
is_np_doc
|
|
21
|
+
input.kind != "NetworkPolicy"
|
|
22
|
+
msg := sprintf(np_kind_template, [input.kind])
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
deny contains msg if {
|
|
26
|
+
is_np_doc
|
|
27
|
+
input.apiVersion != "networking.k8s.io/v1"
|
|
28
|
+
msg := sprintf(np_api_template, [input.apiVersion])
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
deny contains "spec відсутній або некоректний (NetworkPolicy; k8s.mdc)" if {
|
|
32
|
+
is_np_doc
|
|
33
|
+
not is_object(object.get(input, "spec", null))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
deny contains "spec.podSelector.matchLabels відсутній (NetworkPolicy; k8s.mdc)" if {
|
|
37
|
+
is_np_doc
|
|
38
|
+
spec := object.get(input, "spec", null)
|
|
39
|
+
is_object(spec)
|
|
40
|
+
selector := object.get(spec, "podSelector", null)
|
|
41
|
+
is_object(selector)
|
|
42
|
+
not is_object(object.get(selector, "matchLabels", null))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
deny contains "spec.policyTypes має містити Ingress і Egress (NetworkPolicy; k8s.mdc)" if {
|
|
46
|
+
is_np_doc
|
|
47
|
+
spec := object.get(input, "spec", null)
|
|
48
|
+
is_object(spec)
|
|
49
|
+
types := object.get(spec, "policyTypes", [])
|
|
50
|
+
not policy_types_has_ingress_and_egress(types)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
deny contains "spec.ingress має містити from.podSelector (NetworkPolicy; k8s.mdc)" if {
|
|
54
|
+
is_np_doc
|
|
55
|
+
spec := object.get(input, "spec", null)
|
|
56
|
+
is_object(spec)
|
|
57
|
+
not ingress_has_pod_selector_rule(spec)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
deny contains "spec.egress має бути непорожнім масивом (NetworkPolicy; k8s.mdc)" if {
|
|
61
|
+
is_np_doc
|
|
62
|
+
spec := object.get(input, "spec", null)
|
|
63
|
+
is_object(spec)
|
|
64
|
+
not is_non_empty_array(object.get(spec, "egress", null))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
deny contains "spec.egress: заборонено allow-all {} — канон k8s.mdc (80/443 назовні, інше — in-cluster)" if {
|
|
68
|
+
is_np_doc
|
|
69
|
+
spec := object.get(input, "spec", null)
|
|
70
|
+
is_object(spec)
|
|
71
|
+
egress_allows_all(object.get(spec, "egress", null))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
deny contains "spec.egress: потрібен ipBlock 0.0.0.0/0 з ports 80 і 443 (HTTP/HTTPS назовні; k8s.mdc)" if {
|
|
75
|
+
is_np_doc
|
|
76
|
+
spec := object.get(input, "spec", null)
|
|
77
|
+
is_object(spec)
|
|
78
|
+
not egress_has_internet_http_https(spec)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
deny contains "spec.egress: потрібен to.namespaceSelector: {} (інші порти лише in-cluster / *.svc; k8s.mdc)" if {
|
|
82
|
+
is_np_doc
|
|
83
|
+
spec := object.get(input, "spec", null)
|
|
84
|
+
is_object(spec)
|
|
85
|
+
not egress_has_cluster_namespace_selector(spec)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
is_np_doc if input.kind == "NetworkPolicy"
|
|
89
|
+
|
|
90
|
+
is_np_doc if startswith(object.get(input, "apiVersion", ""), "networking.k8s.io/")
|
|
91
|
+
|
|
92
|
+
policy_types_has_ingress_and_egress(types) if {
|
|
93
|
+
is_array(types)
|
|
94
|
+
"Ingress" in types
|
|
95
|
+
"Egress" in types
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
ingress_has_pod_selector_rule(spec) if {
|
|
99
|
+
ingress := object.get(spec, "ingress", null)
|
|
100
|
+
is_array(ingress)
|
|
101
|
+
some rule in ingress
|
|
102
|
+
is_object(rule)
|
|
103
|
+
from_list := object.get(rule, "from", null)
|
|
104
|
+
is_array(from_list)
|
|
105
|
+
some peer in from_list
|
|
106
|
+
is_object(peer)
|
|
107
|
+
object.get(peer, "podSelector", null) != null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
is_non_empty_array(x) if {
|
|
111
|
+
is_array(x)
|
|
112
|
+
count(x) > 0
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
egress_allows_all(egress) if {
|
|
116
|
+
is_array(egress)
|
|
117
|
+
some rule in egress
|
|
118
|
+
is_object(rule)
|
|
119
|
+
count(object.keys(rule)) == 0
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
egress_has_internet_http_https(spec) if {
|
|
123
|
+
egress := object.get(spec, "egress", null)
|
|
124
|
+
is_array(egress)
|
|
125
|
+
some rule in egress
|
|
126
|
+
is_object(rule)
|
|
127
|
+
to_list := object.get(rule, "to", null)
|
|
128
|
+
is_array(to_list)
|
|
129
|
+
some peer in to_list
|
|
130
|
+
is_object(peer)
|
|
131
|
+
ipb := object.get(peer, "ipBlock", null)
|
|
132
|
+
is_object(ipb)
|
|
133
|
+
object.get(ipb, "cidr", "") == "0.0.0.0/0"
|
|
134
|
+
ports := object.get(rule, "ports", null)
|
|
135
|
+
is_array(ports)
|
|
136
|
+
egress_ports_include(ports, 80)
|
|
137
|
+
egress_ports_include(ports, 443)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
egress_ports_include(ports, want) if {
|
|
141
|
+
some p in ports
|
|
142
|
+
is_object(p)
|
|
143
|
+
object.get(p, "port", null) == want
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
egress_has_cluster_namespace_selector(spec) if {
|
|
147
|
+
egress := object.get(spec, "egress", null)
|
|
148
|
+
is_array(egress)
|
|
149
|
+
some rule in egress
|
|
150
|
+
is_object(rule)
|
|
151
|
+
to_list := object.get(rule, "to", null)
|
|
152
|
+
is_array(to_list)
|
|
153
|
+
some peer in to_list
|
|
154
|
+
is_object(peer)
|
|
155
|
+
ns := object.get(peer, "namespaceSelector", null)
|
|
156
|
+
is_object(ns)
|
|
157
|
+
count(ns) == 0
|
|
158
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
spec:
|
|
2
|
+
podSelector:
|
|
3
|
+
matchLabels: {}
|
|
4
|
+
policyTypes:
|
|
5
|
+
- Ingress
|
|
6
|
+
- Egress
|
|
7
|
+
ingress:
|
|
8
|
+
- from:
|
|
9
|
+
- podSelector: {}
|
|
10
|
+
egress:
|
|
11
|
+
- to:
|
|
12
|
+
- namespaceSelector:
|
|
13
|
+
matchLabels:
|
|
14
|
+
kubernetes.io/metadata.name: kube-system
|
|
15
|
+
podSelector:
|
|
16
|
+
matchLabels:
|
|
17
|
+
k8s-app: kube-dns
|
|
18
|
+
ports:
|
|
19
|
+
- protocol: UDP
|
|
20
|
+
port: 53
|
|
21
|
+
- protocol: TCP
|
|
22
|
+
port: 53
|
|
23
|
+
- to:
|
|
24
|
+
- ipBlock:
|
|
25
|
+
cidr: 0.0.0.0/0
|
|
26
|
+
ports:
|
|
27
|
+
- protocol: TCP
|
|
28
|
+
port: 80
|
|
29
|
+
- protocol: TCP
|
|
30
|
+
port: 443
|
|
31
|
+
- to:
|
|
32
|
+
- namespaceSelector: {}
|
|
@@ -17,6 +17,9 @@ import { checkTextSubset } from '../../../../scripts/utils/template.mjs'
|
|
|
17
17
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
18
18
|
const SNIPPET_PATH = join(HERE, 'template', '.trufflehog-exclude.snippet.txt')
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* @returns {Promise<number>} exit-код перевірки
|
|
22
|
+
*/
|
|
20
23
|
export async function check() {
|
|
21
24
|
const reporter = createCheckReporter()
|
|
22
25
|
const { pass, fail } = reporter
|
|
@@ -1 +1,5 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"scripts": {
|
|
3
|
+
"lint-security": "trufflehog filesystem . --no-update --exclude-paths .trufflehog-exclude --results=verified,unknown --fail"
|
|
4
|
+
}
|
|
5
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Правила стилів CSS та SCSS
|
|
3
|
-
version: '1.
|
|
3
|
+
version: '1.4'
|
|
4
4
|
globs: "**/*.{css,scss,vue}"
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
@@ -12,6 +12,25 @@ alwaysApply: false
|
|
|
12
12
|
- **Запуск stylelint:** лише **`npx stylelint`**. Локально — через скрипт **`lint-style`** (`bun run lint-style`); у **GitHub Actions** у кроці **`run`** викликай `npx stylelint '**/*.{css,scss,vue}' --fix` напряму (не через **`bun run lint-style`**). Не використовуй **`bunx stylelint`**. Після змін запускай **`bun run lint-style`** і виправляй усе, що лишилось після auto-fix; за потреби — повний `bun run lint` (навичка **`/n-lint`**).
|
|
13
13
|
- **Не розширюй винятки:** не додавай зайві **`stylelint-disable`** без потреби; краще підлаштувати стилі під правила проєкту.
|
|
14
14
|
|
|
15
|
+
## Канон
|
|
16
|
+
|
|
17
|
+
### `package.json`
|
|
18
|
+
|
|
19
|
+
- `lint-style` (substring requirement): [package.json.contains.json](./policy/package_json/template/package.json.contains.json)
|
|
20
|
+
- `stylelint.extends`: [package.json.snippet.json](./policy/package_json/template/package.json.snippet.json)
|
|
21
|
+
|
|
22
|
+
### `.vscode/extensions.json`
|
|
23
|
+
|
|
24
|
+
- Канон `recommendations`: [extensions.json.snippet.json](./policy/vscode_extensions/template/extensions.json.snippet.json)
|
|
25
|
+
|
|
26
|
+
### `.vscode/settings.json`
|
|
27
|
+
|
|
28
|
+
- Вимкнення вбудованої CSS-валідації VS Code: [settings.json.snippet.json](./policy/vscode_settings/template/settings.json.snippet.json)
|
|
29
|
+
|
|
30
|
+
### CI: `.github/workflows/lint-style.yml`
|
|
31
|
+
|
|
32
|
+
- Канон: [lint-style.yml.snippet.yml](./policy/lint_style_yml/template/lint-style.yml.snippet.yml)
|
|
33
|
+
|
|
15
34
|
**`package.json`:**
|
|
16
35
|
|
|
17
36
|
```json title="package.json"
|
|
@@ -53,9 +53,9 @@ deny contains msg if {
|
|
|
53
53
|
# ── deny: import substrings forbidden ────────────────────────────────────
|
|
54
54
|
|
|
55
55
|
deny contains msg if {
|
|
56
|
-
some forbidden, reason in data.template.deny["import-substrings"]
|
|
57
56
|
imports := object.get(input, "import", [])
|
|
58
57
|
is_array(imports)
|
|
58
|
+
some forbidden, reason in data.template.deny["import-substrings"]
|
|
59
59
|
some imp in imports
|
|
60
60
|
is_string(imp)
|
|
61
61
|
contains(imp, forbidden)
|
|
@@ -47,7 +47,7 @@ deny contains msg if {
|
|
|
47
47
|
msg := sprintf(".markdownlint-cli2.jsonc: %s.%s.%s має бути %v (text.mdc)", [section, inner_key, leaf, expected])
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
# ── deny:
|
|
50
|
+
# ── deny: вкладеного обʼєкта взагалі немає ──────────────────────────────
|
|
51
51
|
|
|
52
52
|
deny contains msg if {
|
|
53
53
|
some section, expected_inner in data.template.snippet
|
package/rules/vue/vue.mdc
CHANGED
|
@@ -118,6 +118,7 @@ GlobalRegistrator.register()
|
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
`jsdom` не використовуй — happy-dom швидший і достатній для типових Vue-компонентних тестів.
|
|
121
|
+
|
|
121
122
|
- **E2E:** **Playwright** змістовні сценарії користувацьких потоків.
|
|
122
123
|
|
|
123
124
|
### Інструменти (узгоджено з Vite і цим правилом)
|
|
@@ -84,7 +84,7 @@ const ADR_NORMALIZE_STOP_HOOK_GROUP = Object.freeze({
|
|
|
84
84
|
/** Канонічний Cursor stop-hook для ADR capture. Cursor передає payload через stdin JSON. */
|
|
85
85
|
const CURSOR_ADR_STOP_HOOK = Object.freeze({
|
|
86
86
|
command: [
|
|
87
|
-
|
|
87
|
+
'bash -lc \'root="$PWD";',
|
|
88
88
|
`if [ ! -f "$root/${CURSOR_ADR_HOOK_COMMAND_MARKER}" ] && [ -f "$root/../${CURSOR_ADR_HOOK_COMMAND_MARKER}" ]; then root="$root/.."; fi;`,
|
|
89
89
|
`bash "$root/${CURSOR_ADR_HOOK_COMMAND_MARKER}"'`
|
|
90
90
|
].join(' '),
|
|
@@ -94,7 +94,7 @@ const CURSOR_ADR_STOP_HOOK = Object.freeze({
|
|
|
94
94
|
/** Канонічний Cursor stop-hook для ADR normalize. */
|
|
95
95
|
const CURSOR_ADR_NORMALIZE_STOP_HOOK = Object.freeze({
|
|
96
96
|
command: [
|
|
97
|
-
|
|
97
|
+
'bash -lc \'root="$PWD";',
|
|
98
98
|
`if [ ! -f "$root/${CURSOR_ADR_NORMALIZE_HOOK_COMMAND_MARKER}" ] && [ -f "$root/../${CURSOR_ADR_NORMALIZE_HOOK_COMMAND_MARKER}" ]; then root="$root/.."; fi;`,
|
|
99
99
|
`bash "$root/${CURSOR_ADR_NORMALIZE_HOOK_COMMAND_MARKER}"'`
|
|
100
100
|
].join(' '),
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Returns list of template/ files that are NOT referenced in <id>.mdc as
|
|
3
3
|
* markdown link targets. Paths returned are relative to ruleDir.
|
|
4
|
-
*
|
|
5
|
-
* @param {string} ruleDir absolute path to npm/rules/<id>/
|
|
6
|
-
* @param {string} ruleId basename (e.g. "security")
|
|
7
|
-
* @returns {Promise<string[]>}
|
|
8
4
|
*/
|
|
9
5
|
import { existsSync } from 'node:fs'
|
|
10
6
|
import { readdir, readFile, stat } from 'node:fs/promises'
|
|
11
7
|
import { join, relative } from 'node:path'
|
|
12
8
|
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} ruleDir абсолютний шлях до каталогу правила
|
|
11
|
+
* @returns {Promise<string[]>} абсолютні шляхи всіх файлів у template/
|
|
12
|
+
*/
|
|
13
13
|
async function walkTemplateDirs(ruleDir) {
|
|
14
14
|
const out = []
|
|
15
15
|
for (const kind of ['fix', 'policy']) {
|
|
@@ -18,13 +18,18 @@ async function walkTemplateDirs(ruleDir) {
|
|
|
18
18
|
for (const concern of await readdir(kindDir)) {
|
|
19
19
|
const tpl = join(kindDir, concern, 'template')
|
|
20
20
|
if (!existsSync(tpl)) continue
|
|
21
|
-
|
|
21
|
+
const tplStat = await stat(tpl)
|
|
22
|
+
if (!tplStat.isDirectory()) continue
|
|
22
23
|
out.push(...(await collectFiles(tpl)))
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
return out.map(p => relative(ruleDir, p))
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} dir каталог для обходу
|
|
31
|
+
* @returns {Promise<string[]>} абсолютні шляхи знайдених файлів
|
|
32
|
+
*/
|
|
28
33
|
async function collectFiles(dir) {
|
|
29
34
|
const out = []
|
|
30
35
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
@@ -35,6 +40,11 @@ async function collectFiles(dir) {
|
|
|
35
40
|
return out
|
|
36
41
|
}
|
|
37
42
|
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} ruleDir абсолютний шлях до npm/rules/<id>/
|
|
45
|
+
* @param {string} ruleId basename правила (напр. "security")
|
|
46
|
+
* @returns {Promise<string[]>} відносні шляхи template-файлів без посилань у .mdc
|
|
47
|
+
*/
|
|
38
48
|
export async function findMissingMdcRefs(ruleDir, ruleId) {
|
|
39
49
|
const mdcPath = join(ruleDir, `${ruleId}.mdc`)
|
|
40
50
|
if (!existsSync(mdcPath)) return []
|