@nitra/cursor 1.13.34 → 1.13.40
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 +41 -1
- package/bin/n-cursor.js +4 -0
- package/package.json +1 -1
- package/rules/changelog/fix/consistency/check.mjs +100 -85
- package/rules/ci4/ci4.mdc +7 -7
- package/rules/ga/lint/lint.mjs +23 -3
- package/rules/ga/policy/lint_ga/lint_ga.rego +6 -0
- package/rules/ga/policy/lint_ga/template/lint-ga.yml.snippet.yml +6 -0
- 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/manifests/check.mjs +775 -139
- package/rules/k8s/k8s.mdc +52 -6
- 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/text/fix/formatting/check.mjs +1 -1
- package/rules/text/lint/lint.mjs +113 -5
- package/rules/text/policy/cspell/cspell.rego +1 -1
- package/rules/text/policy/lint_text/lint_text.rego +100 -0
- package/rules/text/policy/lint_text/target.json +4 -0
- package/rules/text/policy/lint_text/template/lint-text.yml.snippet.yml +61 -0
- 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/text/text.mdc +3 -57
- 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
|
---
|
|
@@ -118,7 +118,7 @@ resources:
|
|
|
118
118
|
memory: '128Mi'
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
-
**HPA і
|
|
121
|
+
**HPA, PDB і NetworkPolicy у base не тримаємо**: ні локальних `hpa.yaml` / `pdb.yaml` / `networkpolicy.yaml` поруч із workload-маніфестами, ні через `resources` / `components` / `bases`. Канон — sibling каталог **`components/`** (Kustomize Component) поруч з `base/` (див. розділ нижче).
|
|
122
122
|
|
|
123
123
|
### Поза base (оверлеї, окремі каталоги)
|
|
124
124
|
|
|
@@ -380,12 +380,15 @@ images:
|
|
|
380
380
|
|
|
381
381
|
## Deployment: `topologySpreadConstraints`, HPA / PDB через `components/`
|
|
382
382
|
|
|
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` і `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`** створює автоматично.
|
|
384
386
|
|
|
385
387
|
**Канонічна структура `<pkg>/k8s/components/`** (sibling до `base/`):
|
|
386
388
|
|
|
387
|
-
- **`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]` (відсортовано за алфавітом).
|
|
388
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`** дописує його автоматично.
|
|
389
392
|
- **`pdb.yaml`** — `policy/v1`, `PodDisruptionBudget`, **без** `metadata.namespace`, `spec.selector.matchLabels.app` **= `spec.selector.matchLabels.app`** Deployment, dev-like `minAvailable: 0`.
|
|
390
393
|
|
|
391
394
|
Інші назви каталогу (`scale/`, `hpa-component/`, `pdb-component/`) — fail.
|
|
@@ -394,13 +397,14 @@ images:
|
|
|
394
397
|
|
|
395
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`.
|
|
396
399
|
|
|
397
|
-
У **не-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-документи автоматично):
|
|
398
401
|
|
|
399
402
|
- **`hpa.yaml`** (поза **`…/base/`**) — `autoscaling/v2`, `HorizontalPodAutoscaler`, `spec.scaleTargetRef.name` **= `metadata.name`** Deployment.
|
|
403
|
+
- **`networkpolicy.yaml`** — той самий канон egress/ingress, що в `components/` (multi-doc, якщо у каталозі кілька workload-ів).
|
|
400
404
|
- **`pdb.yaml`** — `policy/v1`, `PodDisruptionBudget`, `spec.selector.matchLabels.app` **= `spec.selector.matchLabels.app`** Deployment.
|
|
401
405
|
- **`topologySpreadConstraints`** — запис з `maxSkew: 1`, `topologyKey: kubernetes.io/hostname`, `whenUnsatisfiable: ScheduleAnyway`, `labelSelector.matchLabels.app` рівне тій самій мітці `app`.
|
|
402
406
|
|
|
403
|
-
**Перевірка структури `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`**.
|
|
404
408
|
|
|
405
409
|
**Локальні шляхи в `kustomization.yaml`:** кожен запис без `://` (remote) з `resources` / `bases` / `components` / `crds`, `patchesStrategicMerge`, `patches[].path`, `patchesJson6902[].path`, `configurations[]`, `replacements[].path` має вказувати на **існуючий** у репозиторії файл (`.yaml` / `.yml`) або **каталог**; биті посилання — помилка **`check k8s`**.
|
|
406
410
|
|
|
@@ -462,9 +466,51 @@ apiVersion: kustomize.config.k8s.io/v1alpha1
|
|
|
462
466
|
kind: Component
|
|
463
467
|
resources:
|
|
464
468
|
- hpa.yaml
|
|
469
|
+
- networkpolicy.yaml
|
|
465
470
|
- pdb.yaml
|
|
466
471
|
```
|
|
467
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
|
+
|
|
468
514
|
```yaml title="k8s/components/hpa.yaml"
|
|
469
515
|
# yaml-language-server: $schema=https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.33.9-standalone-strict/horizontalpodautoscaler-autoscaling-v2.json
|
|
470
516
|
apiVersion: autoscaling/v2
|
|
@@ -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
|
+
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* `npm/mdc/text.mdc` (markdown-текст, не JSON/YAML);
|
|
13
13
|
* - складна валідація скрипта `lint-text` (cspell, markdownlint, v8r у трьох
|
|
14
14
|
* варіантах, run-shellcheck-text.mjs, обовʼязкові glob-и);
|
|
15
|
-
* - workflow `lint-text.yml` має крок `bun run lint-text
|
|
15
|
+
* - workflow `lint-text.yml` має крок `bun run lint-text` (структура — rego `text.lint_text`).
|
|
16
16
|
*
|
|
17
17
|
* **Що покрила Rego** (`npx \@nitra/cursor check`):
|
|
18
18
|
* - `npm/policy/text/oxfmtrc/` — обовʼязкові ключі `.oxfmtrc.json` і канонічні
|
package/rules/text/lint/lint.mjs
CHANGED
|
@@ -1,26 +1,134 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI-обгортка над канонічним `lint-text` (text.mdc):
|
|
2
|
+
* CLI-обгортка над канонічним `lint-text` (text.mdc): preflight на `shellcheck`, `patch`
|
|
3
|
+
* (для авто-фіксу shellcheck) і `dotenv-linter`; далі послідовно
|
|
3
4
|
* 1) `cspell .` — перевірка правопису з `@nitra/cspell-dict`;
|
|
4
5
|
* 2) `runShellcheckText()` — авто-фікс і фінальна перевірка `*.sh` через `shellcheck`;
|
|
5
6
|
* 3) `runDotenvLinter()` — авто-фікс і фінальна перевірка `.env*` через `dotenv-linter`;
|
|
6
7
|
* 4) `bunx markdownlint-cli2 --fix "**\/*.md" "**\/*.mdc"` — авто-фікс Markdown;
|
|
7
|
-
* 5) `runV8rWithGlobs()` — schema-валідація json/json5/yaml/yml/toml через v8r
|
|
8
|
+
* 5) `runV8rWithGlobs()` — schema-валідація json/json5/yaml/yml/toml через v8r.
|
|
9
|
+
*
|
|
10
|
+
* Без preflight локальний прогін може пройти cspell/markdownlint, а CI на ubuntu-latest
|
|
11
|
+
* (де shellcheck передвстановлений, але dotenv-linter — ні) падає на кроці dotenv-linter
|
|
12
|
+
* з неінформативним повідомленням. Preflight збирає всі відсутні бінарники до першого кроку.
|
|
8
13
|
*
|
|
9
14
|
* Перший ненульовий код з ланцюжка повертається як код виходу; наступні кроки не запускаються.
|
|
10
15
|
* Експортовано як `runLintTextCli` — використовується з `bin/n-cursor.js` як підкоманда `lint-text`.
|
|
11
16
|
*/
|
|
17
|
+
import { platform } from 'node:process'
|
|
18
|
+
|
|
12
19
|
import { runLintStep } from '../../../scripts/utils/run-lint-step.mjs'
|
|
20
|
+
import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
|
|
13
21
|
import { runDotenvLinter } from './run-dotenv-linter.mjs'
|
|
14
22
|
import { runShellcheckText } from './run-shellcheck.mjs'
|
|
15
23
|
import { runV8rWithGlobs } from './run-v8r.mjs'
|
|
16
24
|
|
|
17
25
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
26
|
+
* Опис залежності preflight-ом.
|
|
27
|
+
* @typedef {object} PreflightDep
|
|
28
|
+
* @property {string} bin ім'я виконуваного файлу
|
|
29
|
+
* @property {string[]} [winBins] альтернативні імена на Windows
|
|
30
|
+
* @property {string} explanation наслідки відсутності
|
|
31
|
+
* @property {string[]} install команди встановлення
|
|
32
|
+
* @property {string} successMsg повідомлення на pass-шлях
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** @type {PreflightDep} */
|
|
36
|
+
const SHELLCHECK_PREFLIGHT = {
|
|
37
|
+
bin: 'shellcheck',
|
|
38
|
+
winBins: ['shellcheck.exe'],
|
|
39
|
+
explanation: [
|
|
40
|
+
'Без нього `runShellcheckText()` пропускає перевірку tracked `*.sh` — локально lint-text',
|
|
41
|
+
'може бути зеленим, а CI (shellcheck + patch) падає на тих самих скриптах.'
|
|
42
|
+
].join('\n '),
|
|
43
|
+
install: [
|
|
44
|
+
'macOS: brew install shellcheck',
|
|
45
|
+
'Debian/Ubuntu: sudo apt-get install -y shellcheck',
|
|
46
|
+
'Arch: sudo pacman -S shellcheck'
|
|
47
|
+
],
|
|
48
|
+
successMsg: '✅ shellcheck знайдено в PATH — lint-text перевірить *.sh'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @type {PreflightDep} */
|
|
52
|
+
const PATCH_PREFLIGHT = {
|
|
53
|
+
bin: 'patch',
|
|
54
|
+
explanation: [
|
|
55
|
+
'Без `patch` не застосуються авто-виправлення shellcheck (`shellcheck -f diff` + `patch -p1`).'
|
|
56
|
+
].join('\n '),
|
|
57
|
+
install: [
|
|
58
|
+
'macOS: зазвичай уже є в системі',
|
|
59
|
+
'Debian/Ubuntu: sudo apt-get install -y patch'
|
|
60
|
+
],
|
|
61
|
+
successMsg: '✅ patch знайдено в PATH — shellcheck auto-fix працюватиме'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @type {PreflightDep} */
|
|
65
|
+
const DOTENV_LINTER_PREFLIGHT = {
|
|
66
|
+
bin: 'dotenv-linter',
|
|
67
|
+
winBins: ['dotenv-linter.exe'],
|
|
68
|
+
explanation: [
|
|
69
|
+
'Без нього не виконається крок `.env*` у lint-text — локально cspell/markdownlint',
|
|
70
|
+
'пройдуть, а CI без Install dotenv-linter впаде неінформативно.'
|
|
71
|
+
].join('\n '),
|
|
72
|
+
install: [
|
|
73
|
+
'macOS: brew install dotenv-linter',
|
|
74
|
+
'Linux: curl -sSfL https://git.io/JLbXn | sh -s -- -b /usr/local/bin',
|
|
75
|
+
'cargo: cargo install dotenv-linter'
|
|
76
|
+
],
|
|
77
|
+
successMsg: '✅ dotenv-linter знайдено в PATH — lint-text перевірить .env*'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {PreflightDep} dep
|
|
82
|
+
* @returns {string | null}
|
|
83
|
+
*/
|
|
84
|
+
function resolvePreflightBin(dep) {
|
|
85
|
+
if (platform === 'win32' && dep.winBins) {
|
|
86
|
+
for (const name of dep.winBins) {
|
|
87
|
+
const r = resolveCmd(name)
|
|
88
|
+
if (r) return r
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return resolveCmd(dep.bin)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {PreflightDep} dep
|
|
96
|
+
* @returns {void}
|
|
97
|
+
*/
|
|
98
|
+
function printPreflightMissingMessage(dep) {
|
|
99
|
+
console.error(`❌ ${dep.bin} не знайдено в PATH.`)
|
|
100
|
+
console.error(` ${dep.explanation}`)
|
|
101
|
+
console.error(' Встанови:')
|
|
102
|
+
for (const line of dep.install) {
|
|
103
|
+
console.error(` ${line}`)
|
|
104
|
+
}
|
|
105
|
+
console.error(' Деталі: text.mdc → секція про lint-text.')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {PreflightDep} dep
|
|
110
|
+
* @returns {boolean}
|
|
111
|
+
*/
|
|
112
|
+
function preflight(dep) {
|
|
113
|
+
if (resolvePreflightBin(dep)) {
|
|
114
|
+
console.log(dep.successMsg)
|
|
115
|
+
return true
|
|
116
|
+
}
|
|
117
|
+
printPreflightMissingMessage(dep)
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Виконує канонічний `lint-text` з preflight і ланцюжком cspell → shellcheck → dotenv → markdownlint → v8r.
|
|
21
123
|
* @returns {number} 0 — все OK, інакше — код першого кроку, що впав
|
|
22
124
|
*/
|
|
23
125
|
export function runLintTextCli() {
|
|
126
|
+
let preflightOk = true
|
|
127
|
+
for (const dep of [SHELLCHECK_PREFLIGHT, PATCH_PREFLIGHT, DOTENV_LINTER_PREFLIGHT]) {
|
|
128
|
+
if (!preflight(dep)) preflightOk = false
|
|
129
|
+
}
|
|
130
|
+
if (!preflightOk) return 1
|
|
131
|
+
|
|
24
132
|
const cspellCode = runLintStep('cspell', 'npx', ['cspell', '.'])
|
|
25
133
|
if (cspellCode !== 0) return cspellCode
|
|
26
134
|
|
|
@@ -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)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Перевірка `.github/workflows/lint-text.yml` (text.mdc).
|
|
2
|
+
#
|
|
3
|
+
# Канон надходить через --data: { "template": { "snippet": ... } }
|
|
4
|
+
# Структура --data сформована з template/lint-text.yml.snippet.yml.
|
|
5
|
+
# Універсальні workflow-перевірки (checkout, permissions) — у `ga.workflow_common`.
|
|
6
|
+
package text.lint_text
|
|
7
|
+
|
|
8
|
+
import rego.v1
|
|
9
|
+
|
|
10
|
+
expected_name := data.template.snippet.name
|
|
11
|
+
|
|
12
|
+
expected_push_branches := {b | some b in data.template.snippet.on.push.branches}
|
|
13
|
+
|
|
14
|
+
expected_pr_branches := {b | some b in data.template.snippet.on.pull_request.branches}
|
|
15
|
+
|
|
16
|
+
expected_push_paths := {p | some p in data.template.snippet.on.push.paths}
|
|
17
|
+
|
|
18
|
+
expected_runs_on := data.template.snippet.jobs.text["runs-on"]
|
|
19
|
+
|
|
20
|
+
expected_perms := data.template.snippet.jobs.text.permissions
|
|
21
|
+
|
|
22
|
+
job := input.jobs.text
|
|
23
|
+
|
|
24
|
+
job_uses_set contains job.steps[_].uses
|
|
25
|
+
|
|
26
|
+
job_run_blob := concat("\n", [run |
|
|
27
|
+
run := job.steps[_].run
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
expected_uses_set contains u if {
|
|
31
|
+
some step in data.template.snippet.jobs.text.steps
|
|
32
|
+
u := object.get(step, "uses", "")
|
|
33
|
+
u != ""
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
expected_run_substrings contains r if {
|
|
37
|
+
some step in data.template.snippet.jobs.text.steps
|
|
38
|
+
r := object.get(step, "run", "")
|
|
39
|
+
r != ""
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
deny contains msg if {
|
|
43
|
+
input.name != expected_name
|
|
44
|
+
msg := sprintf("lint-text.yml: name має бути %q (text.mdc)", [expected_name])
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
deny contains msg if {
|
|
48
|
+
not branches_superset_of(input.on.push.branches, expected_push_branches)
|
|
49
|
+
msg := "lint-text.yml: on.push.branches має містити dev і main (text.mdc)"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
deny contains msg if {
|
|
53
|
+
not branches_superset_of(input.on.pull_request.branches, expected_pr_branches)
|
|
54
|
+
msg := "lint-text.yml: on.pull_request.branches має містити dev і main (text.mdc)"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
deny contains msg if {
|
|
58
|
+
not paths_superset_of(input.on.push.paths, expected_push_paths)
|
|
59
|
+
msg := "lint-text.yml: on.push.paths має містити очікувані glob-и (text.mdc)"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
deny contains msg if {
|
|
63
|
+
not job
|
|
64
|
+
msg := "lint-text.yml: jobs.text відсутній (text.mdc)"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
deny contains msg if {
|
|
68
|
+
job["runs-on"] != expected_runs_on
|
|
69
|
+
msg := sprintf("lint-text.yml: runs-on має бути %s (text.mdc)", [expected_runs_on])
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
deny contains msg if {
|
|
73
|
+
job.permissions.contents != expected_perms.contents
|
|
74
|
+
msg := sprintf("lint-text.yml: permissions.contents має бути %s (text.mdc)", [expected_perms.contents])
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
deny contains msg if {
|
|
78
|
+
count(job.steps) == 0
|
|
79
|
+
msg := "lint-text.yml: jobs.text.steps відсутні (text.mdc)"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
deny contains msg if {
|
|
83
|
+
some required_use in expected_uses_set
|
|
84
|
+
not required_use in job_uses_set
|
|
85
|
+
msg := sprintf("lint-text.yml: має бути uses: %s (text.mdc)", [required_use])
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
deny contains msg if {
|
|
89
|
+
some required_run in expected_run_substrings
|
|
90
|
+
not contains(job_run_blob, required_run)
|
|
91
|
+
msg := sprintf("lint-text.yml: жоден крок run не містить %q (text.mdc)", [required_run])
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
branches_superset_of(actual, expected) if {
|
|
95
|
+
expected & {b | some b in actual} == expected
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
paths_superset_of(actual, expected) if {
|
|
99
|
+
expected & {p | some p in actual} == expected
|
|
100
|
+
}
|