@intentius/chant-lexicon-k8s 0.0.12

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.
Files changed (123) hide show
  1. package/dist/integrity.json +32 -0
  2. package/dist/manifest.json +8 -0
  3. package/dist/meta.json +1413 -0
  4. package/dist/rules/hardcoded-namespace.ts +56 -0
  5. package/dist/rules/k8s-helpers.ts +149 -0
  6. package/dist/rules/wk8005.ts +59 -0
  7. package/dist/rules/wk8006.ts +68 -0
  8. package/dist/rules/wk8041.ts +73 -0
  9. package/dist/rules/wk8042.ts +48 -0
  10. package/dist/rules/wk8101.ts +65 -0
  11. package/dist/rules/wk8102.ts +42 -0
  12. package/dist/rules/wk8103.ts +45 -0
  13. package/dist/rules/wk8104.ts +69 -0
  14. package/dist/rules/wk8105.ts +45 -0
  15. package/dist/rules/wk8201.ts +55 -0
  16. package/dist/rules/wk8202.ts +46 -0
  17. package/dist/rules/wk8203.ts +46 -0
  18. package/dist/rules/wk8204.ts +54 -0
  19. package/dist/rules/wk8205.ts +56 -0
  20. package/dist/rules/wk8207.ts +45 -0
  21. package/dist/rules/wk8208.ts +45 -0
  22. package/dist/rules/wk8209.ts +45 -0
  23. package/dist/rules/wk8301.ts +51 -0
  24. package/dist/rules/wk8302.ts +46 -0
  25. package/dist/rules/wk8303.ts +96 -0
  26. package/dist/skills/chant-k8s.md +433 -0
  27. package/dist/types/index.d.ts +2934 -0
  28. package/package.json +30 -0
  29. package/src/actions/actions.test.ts +83 -0
  30. package/src/actions/apps.ts +23 -0
  31. package/src/actions/batch.ts +9 -0
  32. package/src/actions/core.ts +62 -0
  33. package/src/actions/index.ts +50 -0
  34. package/src/actions/networking.ts +15 -0
  35. package/src/actions/rbac.ts +13 -0
  36. package/src/codegen/docs-cli.ts +3 -0
  37. package/src/codegen/docs.ts +1147 -0
  38. package/src/codegen/generate-cli.ts +41 -0
  39. package/src/codegen/generate-lexicon.ts +69 -0
  40. package/src/codegen/generate-typescript.ts +97 -0
  41. package/src/codegen/generate.ts +144 -0
  42. package/src/codegen/naming.test.ts +63 -0
  43. package/src/codegen/naming.ts +187 -0
  44. package/src/codegen/package.ts +56 -0
  45. package/src/codegen/patches.ts +108 -0
  46. package/src/codegen/snapshot.test.ts +95 -0
  47. package/src/codegen/typecheck.test.ts +24 -0
  48. package/src/codegen/typecheck.ts +4 -0
  49. package/src/codegen/versions.ts +43 -0
  50. package/src/composites/autoscaled-service.ts +236 -0
  51. package/src/composites/composites.test.ts +1109 -0
  52. package/src/composites/cron-workload.ts +167 -0
  53. package/src/composites/index.ts +14 -0
  54. package/src/composites/namespace-env.ts +163 -0
  55. package/src/composites/node-agent.ts +224 -0
  56. package/src/composites/stateful-app.ts +134 -0
  57. package/src/composites/web-app.ts +180 -0
  58. package/src/composites/worker-pool.ts +230 -0
  59. package/src/coverage.test.ts +27 -0
  60. package/src/coverage.ts +35 -0
  61. package/src/crd/loader.ts +112 -0
  62. package/src/crd/parser.test.ts +217 -0
  63. package/src/crd/parser.ts +279 -0
  64. package/src/crd/types.ts +54 -0
  65. package/src/default-labels.test.ts +111 -0
  66. package/src/default-labels.ts +122 -0
  67. package/src/generated/index.d.ts +2934 -0
  68. package/src/generated/index.ts +203 -0
  69. package/src/generated/lexicon-k8s.json +1413 -0
  70. package/src/generated/runtime.ts +4 -0
  71. package/src/import/generator.test.ts +121 -0
  72. package/src/import/generator.ts +285 -0
  73. package/src/import/parser.test.ts +156 -0
  74. package/src/import/parser.ts +204 -0
  75. package/src/import/roundtrip.test.ts +86 -0
  76. package/src/index.ts +38 -0
  77. package/src/lint/post-synth/k8s-helpers.test.ts +219 -0
  78. package/src/lint/post-synth/k8s-helpers.ts +149 -0
  79. package/src/lint/post-synth/post-synth.test.ts +969 -0
  80. package/src/lint/post-synth/wk8005.ts +59 -0
  81. package/src/lint/post-synth/wk8006.ts +68 -0
  82. package/src/lint/post-synth/wk8041.ts +73 -0
  83. package/src/lint/post-synth/wk8042.ts +48 -0
  84. package/src/lint/post-synth/wk8101.ts +65 -0
  85. package/src/lint/post-synth/wk8102.ts +42 -0
  86. package/src/lint/post-synth/wk8103.ts +45 -0
  87. package/src/lint/post-synth/wk8104.ts +69 -0
  88. package/src/lint/post-synth/wk8105.ts +45 -0
  89. package/src/lint/post-synth/wk8201.ts +55 -0
  90. package/src/lint/post-synth/wk8202.ts +46 -0
  91. package/src/lint/post-synth/wk8203.ts +46 -0
  92. package/src/lint/post-synth/wk8204.ts +54 -0
  93. package/src/lint/post-synth/wk8205.ts +56 -0
  94. package/src/lint/post-synth/wk8207.ts +45 -0
  95. package/src/lint/post-synth/wk8208.ts +45 -0
  96. package/src/lint/post-synth/wk8209.ts +45 -0
  97. package/src/lint/post-synth/wk8301.ts +51 -0
  98. package/src/lint/post-synth/wk8302.ts +46 -0
  99. package/src/lint/post-synth/wk8303.ts +96 -0
  100. package/src/lint/rules/hardcoded-namespace.ts +56 -0
  101. package/src/lint/rules/rules.test.ts +69 -0
  102. package/src/lsp/completions.test.ts +64 -0
  103. package/src/lsp/completions.ts +20 -0
  104. package/src/lsp/hover.test.ts +69 -0
  105. package/src/lsp/hover.ts +68 -0
  106. package/src/package-cli.ts +28 -0
  107. package/src/plugin.test.ts +209 -0
  108. package/src/plugin.ts +915 -0
  109. package/src/serializer.test.ts +275 -0
  110. package/src/serializer.ts +278 -0
  111. package/src/spec/fetch.test.ts +24 -0
  112. package/src/spec/fetch.ts +68 -0
  113. package/src/spec/parse.test.ts +102 -0
  114. package/src/spec/parse.ts +477 -0
  115. package/src/testdata/manifests/configmap.yaml +7 -0
  116. package/src/testdata/manifests/deployment.yaml +22 -0
  117. package/src/testdata/manifests/full-app.yaml +61 -0
  118. package/src/testdata/manifests/secret.yaml +7 -0
  119. package/src/testdata/manifests/service.yaml +15 -0
  120. package/src/validate-cli.ts +21 -0
  121. package/src/validate.test.ts +29 -0
  122. package/src/validate.ts +46 -0
  123. package/src/variables.ts +36 -0
@@ -0,0 +1,969 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
3
+
4
+ // Import all checks
5
+ import { wk8005 } from "./wk8005";
6
+ import { wk8006 } from "./wk8006";
7
+ import { wk8041 } from "./wk8041";
8
+ import { wk8042 } from "./wk8042";
9
+ import { wk8101 } from "./wk8101";
10
+ import { wk8102 } from "./wk8102";
11
+ import { wk8103 } from "./wk8103";
12
+ import { wk8104 } from "./wk8104";
13
+ import { wk8105 } from "./wk8105";
14
+ import { wk8201 } from "./wk8201";
15
+ import { wk8202 } from "./wk8202";
16
+ import { wk8203 } from "./wk8203";
17
+ import { wk8204 } from "./wk8204";
18
+ import { wk8205 } from "./wk8205";
19
+ import { wk8207 } from "./wk8207";
20
+ import { wk8208 } from "./wk8208";
21
+ import { wk8209 } from "./wk8209";
22
+ import { wk8301 } from "./wk8301";
23
+ import { wk8302 } from "./wk8302";
24
+ import { wk8303 } from "./wk8303";
25
+
26
+ function makeCtx(yaml: string): PostSynthContext {
27
+ return {
28
+ outputs: new Map([["k8s", yaml]]),
29
+ entities: new Map(),
30
+ buildResult: {
31
+ outputs: new Map([["k8s", yaml]]),
32
+ entities: new Map(),
33
+ warnings: [],
34
+ errors: [],
35
+ sourceFileCount: 1,
36
+ },
37
+ };
38
+ }
39
+
40
+ // ── WK8005: Secrets in env ──────────────────────────────────────────
41
+ // Note: Tests with nested container properties (env, resources, securityContext,
42
+ // ports, probes) use JSON format because the core parseYAML line-based parser
43
+ // cannot handle deeply nested properties inside YAML array items.
44
+
45
+ describe("WK8005: Hardcoded secrets in env", () => {
46
+ test("flags hardcoded password in env", () => {
47
+ const ctx = makeCtx(JSON.stringify({
48
+ apiVersion: "apps/v1",
49
+ kind: "Deployment",
50
+ metadata: { name: "app" },
51
+ spec: {
52
+ template: {
53
+ spec: {
54
+ containers: [
55
+ { name: "app", image: "app:1.0", env: [{ name: "DB_PASSWORD", value: "secret123" }] },
56
+ ],
57
+ },
58
+ },
59
+ },
60
+ }));
61
+ const diags = wk8005.check(ctx);
62
+ expect(diags.length).toBeGreaterThanOrEqual(1);
63
+ expect(diags[0].checkId).toBe("WK8005");
64
+ });
65
+
66
+ test("passes when env uses secretKeyRef", () => {
67
+ const ctx = makeCtx(JSON.stringify({
68
+ apiVersion: "apps/v1",
69
+ kind: "Deployment",
70
+ metadata: { name: "app" },
71
+ spec: {
72
+ template: {
73
+ spec: {
74
+ containers: [
75
+ { name: "app", image: "app:1.0", env: [{ name: "DB_PASSWORD", valueFrom: { secretKeyRef: { name: "db-secret", key: "password" } } }] },
76
+ ],
77
+ },
78
+ },
79
+ },
80
+ }));
81
+ const diags = wk8005.check(ctx);
82
+ expect(diags.length).toBe(0);
83
+ });
84
+
85
+ test("passes when env var name is not sensitive", () => {
86
+ const ctx = makeCtx(JSON.stringify({
87
+ apiVersion: "apps/v1",
88
+ kind: "Deployment",
89
+ metadata: { name: "app" },
90
+ spec: {
91
+ template: {
92
+ spec: {
93
+ containers: [
94
+ { name: "app", image: "app:1.0", env: [{ name: "LOG_LEVEL", value: "info" }] },
95
+ ],
96
+ },
97
+ },
98
+ },
99
+ }));
100
+ const diags = wk8005.check(ctx);
101
+ expect(diags.length).toBe(0);
102
+ });
103
+ });
104
+
105
+ // ── WK8006: Latest tag ──────────────────────────────────────────────
106
+
107
+ describe("WK8006: Latest/untagged images", () => {
108
+ test("flags image with :latest tag", () => {
109
+ const ctx = makeCtx(`
110
+ apiVersion: apps/v1
111
+ kind: Deployment
112
+ metadata:
113
+ name: app
114
+ spec:
115
+ template:
116
+ spec:
117
+ containers:
118
+ - name: app
119
+ image: nginx:latest
120
+ `);
121
+ const diags = wk8006.check(ctx);
122
+ expect(diags.length).toBe(1);
123
+ expect(diags[0].checkId).toBe("WK8006");
124
+ });
125
+
126
+ test("flags untagged image", () => {
127
+ const ctx = makeCtx(`
128
+ apiVersion: apps/v1
129
+ kind: Deployment
130
+ metadata:
131
+ name: app
132
+ spec:
133
+ template:
134
+ spec:
135
+ containers:
136
+ - name: app
137
+ image: nginx
138
+ `);
139
+ const diags = wk8006.check(ctx);
140
+ expect(diags.length).toBe(1);
141
+ });
142
+
143
+ test("passes for explicitly tagged image", () => {
144
+ const ctx = makeCtx(`
145
+ apiVersion: apps/v1
146
+ kind: Deployment
147
+ metadata:
148
+ name: app
149
+ spec:
150
+ template:
151
+ spec:
152
+ containers:
153
+ - name: app
154
+ image: nginx:1.25
155
+ `);
156
+ const diags = wk8006.check(ctx);
157
+ expect(diags.length).toBe(0);
158
+ });
159
+ });
160
+
161
+ // ── WK8041: API keys ────────────────────────────────────────────────
162
+
163
+ describe("WK8041: API keys in env", () => {
164
+ test("flags Stripe key pattern", () => {
165
+ const ctx = makeCtx(JSON.stringify({
166
+ apiVersion: "apps/v1",
167
+ kind: "Deployment",
168
+ metadata: { name: "app" },
169
+ spec: {
170
+ template: {
171
+ spec: {
172
+ containers: [
173
+ { name: "app", image: "app:1.0", env: [{ name: "STRIPE_KEY", value: "sk_live_abc123def456" }] },
174
+ ],
175
+ },
176
+ },
177
+ },
178
+ }));
179
+ const diags = wk8041.check(ctx);
180
+ expect(diags.length).toBeGreaterThanOrEqual(1);
181
+ expect(diags[0].checkId).toBe("WK8041");
182
+ });
183
+
184
+ test("passes for normal values", () => {
185
+ const ctx = makeCtx(JSON.stringify({
186
+ apiVersion: "apps/v1",
187
+ kind: "Deployment",
188
+ metadata: { name: "app" },
189
+ spec: {
190
+ template: {
191
+ spec: {
192
+ containers: [
193
+ { name: "app", image: "app:1.0", env: [{ name: "APP_MODE", value: "production" }] },
194
+ ],
195
+ },
196
+ },
197
+ },
198
+ }));
199
+ const diags = wk8041.check(ctx);
200
+ expect(diags.length).toBe(0);
201
+ });
202
+ });
203
+
204
+ // ── WK8042: Private keys ───────────────────────────────────────────
205
+
206
+ describe("WK8042: Private keys in manifests", () => {
207
+ test("flags private key in ConfigMap", () => {
208
+ const ctx = makeCtx(JSON.stringify({
209
+ apiVersion: "v1",
210
+ kind: "ConfigMap",
211
+ metadata: { name: "config" },
212
+ data: {
213
+ "cert.pem": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA...\n-----END RSA PRIVATE KEY-----",
214
+ },
215
+ }));
216
+ const diags = wk8042.check(ctx);
217
+ expect(diags.length).toBeGreaterThanOrEqual(1);
218
+ expect(diags[0].checkId).toBe("WK8042");
219
+ });
220
+
221
+ test("passes for normal ConfigMap data", () => {
222
+ const ctx = makeCtx(JSON.stringify({
223
+ apiVersion: "v1",
224
+ kind: "ConfigMap",
225
+ metadata: { name: "config" },
226
+ data: { "config.json": '{"key": "value"}' },
227
+ }));
228
+ const diags = wk8042.check(ctx);
229
+ expect(diags.length).toBe(0);
230
+ });
231
+ });
232
+
233
+ // ── WK8101: Selector mismatch ──────────────────────────────────────
234
+
235
+ describe("WK8101: Selector must match template labels", () => {
236
+ test("flags when matchLabels != template labels", () => {
237
+ const ctx = makeCtx(`
238
+ apiVersion: apps/v1
239
+ kind: Deployment
240
+ metadata:
241
+ name: app
242
+ spec:
243
+ selector:
244
+ matchLabels:
245
+ app: my-app
246
+ template:
247
+ metadata:
248
+ labels:
249
+ app: different-app
250
+ spec:
251
+ containers:
252
+ - name: app
253
+ image: app:1.0
254
+ `);
255
+ const diags = wk8101.check(ctx);
256
+ expect(diags.length).toBeGreaterThanOrEqual(1);
257
+ expect(diags[0].checkId).toBe("WK8101");
258
+ });
259
+
260
+ test("passes when selector matches template labels", () => {
261
+ const ctx = makeCtx(`
262
+ apiVersion: apps/v1
263
+ kind: Deployment
264
+ metadata:
265
+ name: app
266
+ spec:
267
+ selector:
268
+ matchLabels:
269
+ app: my-app
270
+ template:
271
+ metadata:
272
+ labels:
273
+ app: my-app
274
+ spec:
275
+ containers:
276
+ - name: app
277
+ image: app:1.0
278
+ `);
279
+ const diags = wk8101.check(ctx);
280
+ expect(diags.length).toBe(0);
281
+ });
282
+ });
283
+
284
+ // ── WK8102: Missing labels ─────────────────────────────────────────
285
+
286
+ describe("WK8102: Missing metadata.labels", () => {
287
+ test("flags resource without labels", () => {
288
+ const ctx = makeCtx(`
289
+ apiVersion: apps/v1
290
+ kind: Deployment
291
+ metadata:
292
+ name: app
293
+ spec:
294
+ template:
295
+ spec:
296
+ containers:
297
+ - name: app
298
+ image: app:1.0
299
+ `);
300
+ const diags = wk8102.check(ctx);
301
+ expect(diags.length).toBeGreaterThanOrEqual(1);
302
+ expect(diags[0].checkId).toBe("WK8102");
303
+ });
304
+
305
+ test("passes with labels present", () => {
306
+ const ctx = makeCtx(`
307
+ apiVersion: apps/v1
308
+ kind: Deployment
309
+ metadata:
310
+ name: app
311
+ labels:
312
+ app: my-app
313
+ spec:
314
+ template:
315
+ spec:
316
+ containers:
317
+ - name: app
318
+ image: app:1.0
319
+ `);
320
+ const diags = wk8102.check(ctx);
321
+ expect(diags.length).toBe(0);
322
+ });
323
+ });
324
+
325
+ // ── WK8103: Container name ─────────────────────────────────────────
326
+
327
+ describe("WK8103: Container missing name", () => {
328
+ test("flags container without name", () => {
329
+ const ctx = makeCtx(`
330
+ apiVersion: apps/v1
331
+ kind: Deployment
332
+ metadata:
333
+ name: app
334
+ spec:
335
+ template:
336
+ spec:
337
+ containers:
338
+ - image: app:1.0
339
+ `);
340
+ const diags = wk8103.check(ctx);
341
+ expect(diags.length).toBeGreaterThanOrEqual(1);
342
+ expect(diags[0].checkId).toBe("WK8103");
343
+ });
344
+
345
+ test("passes with container name", () => {
346
+ const ctx = makeCtx(`
347
+ apiVersion: apps/v1
348
+ kind: Deployment
349
+ metadata:
350
+ name: app
351
+ spec:
352
+ template:
353
+ spec:
354
+ containers:
355
+ - name: app
356
+ image: app:1.0
357
+ `);
358
+ const diags = wk8103.check(ctx);
359
+ expect(diags.length).toBe(0);
360
+ });
361
+ });
362
+
363
+ // ── WK8104: Named ports ────────────────────────────────────────────
364
+
365
+ describe("WK8104: Unnamed container ports", () => {
366
+ test("flags unnamed ports", () => {
367
+ const ctx = makeCtx(JSON.stringify({
368
+ apiVersion: "apps/v1",
369
+ kind: "Deployment",
370
+ metadata: { name: "app" },
371
+ spec: {
372
+ template: {
373
+ spec: {
374
+ containers: [
375
+ { name: "app", image: "app:1.0", ports: [{ containerPort: 8080 }] },
376
+ ],
377
+ },
378
+ },
379
+ },
380
+ }));
381
+ const diags = wk8104.check(ctx);
382
+ expect(diags.length).toBeGreaterThanOrEqual(1);
383
+ expect(diags[0].checkId).toBe("WK8104");
384
+ });
385
+
386
+ test("passes with named ports", () => {
387
+ const ctx = makeCtx(JSON.stringify({
388
+ apiVersion: "apps/v1",
389
+ kind: "Deployment",
390
+ metadata: { name: "app" },
391
+ spec: {
392
+ template: {
393
+ spec: {
394
+ containers: [
395
+ { name: "app", image: "app:1.0", ports: [{ containerPort: 8080, name: "http" }] },
396
+ ],
397
+ },
398
+ },
399
+ },
400
+ }));
401
+ const diags = wk8104.check(ctx);
402
+ expect(diags.length).toBe(0);
403
+ });
404
+ });
405
+
406
+ // ── WK8105: imagePullPolicy ────────────────────────────────────────
407
+
408
+ describe("WK8105: Missing imagePullPolicy", () => {
409
+ test("flags missing imagePullPolicy", () => {
410
+ const ctx = makeCtx(JSON.stringify({
411
+ apiVersion: "apps/v1",
412
+ kind: "Deployment",
413
+ metadata: { name: "app" },
414
+ spec: {
415
+ template: {
416
+ spec: {
417
+ containers: [{ name: "app", image: "app:1.0" }],
418
+ },
419
+ },
420
+ },
421
+ }));
422
+ const diags = wk8105.check(ctx);
423
+ expect(diags.length).toBeGreaterThanOrEqual(1);
424
+ expect(diags[0].checkId).toBe("WK8105");
425
+ });
426
+
427
+ test("passes with explicit imagePullPolicy", () => {
428
+ const ctx = makeCtx(JSON.stringify({
429
+ apiVersion: "apps/v1",
430
+ kind: "Deployment",
431
+ metadata: { name: "app" },
432
+ spec: {
433
+ template: {
434
+ spec: {
435
+ containers: [{ name: "app", image: "app:1.0", imagePullPolicy: "IfNotPresent" }],
436
+ },
437
+ },
438
+ },
439
+ }));
440
+ const diags = wk8105.check(ctx);
441
+ expect(diags.length).toBe(0);
442
+ });
443
+ });
444
+
445
+ // ── WK8201: Resource limits ────────────────────────────────────────
446
+
447
+ describe("WK8201: Resource limits required", () => {
448
+ test("flags container without resource limits", () => {
449
+ const ctx = makeCtx(JSON.stringify({
450
+ apiVersion: "apps/v1",
451
+ kind: "Deployment",
452
+ metadata: { name: "app" },
453
+ spec: {
454
+ template: {
455
+ spec: {
456
+ containers: [{ name: "app", image: "app:1.0" }],
457
+ },
458
+ },
459
+ },
460
+ }));
461
+ const diags = wk8201.check(ctx);
462
+ expect(diags.length).toBeGreaterThanOrEqual(1);
463
+ expect(diags[0].checkId).toBe("WK8201");
464
+ });
465
+
466
+ test("passes with resource limits", () => {
467
+ const ctx = makeCtx(JSON.stringify({
468
+ apiVersion: "apps/v1",
469
+ kind: "Deployment",
470
+ metadata: { name: "app" },
471
+ spec: {
472
+ template: {
473
+ spec: {
474
+ containers: [
475
+ { name: "app", image: "app:1.0", resources: { limits: { cpu: "500m", memory: "256Mi" } } },
476
+ ],
477
+ },
478
+ },
479
+ },
480
+ }));
481
+ const diags = wk8201.check(ctx);
482
+ expect(diags.length).toBe(0);
483
+ });
484
+ });
485
+
486
+ // ── WK8202: Privileged ─────────────────────────────────────────────
487
+
488
+ describe("WK8202: Privileged containers", () => {
489
+ test("flags privileged: true", () => {
490
+ const ctx = makeCtx(JSON.stringify({
491
+ apiVersion: "apps/v1",
492
+ kind: "Deployment",
493
+ metadata: { name: "app" },
494
+ spec: {
495
+ template: {
496
+ spec: {
497
+ containers: [
498
+ { name: "app", image: "app:1.0", securityContext: { privileged: true } },
499
+ ],
500
+ },
501
+ },
502
+ },
503
+ }));
504
+ const diags = wk8202.check(ctx);
505
+ expect(diags.length).toBe(1);
506
+ expect(diags[0].checkId).toBe("WK8202");
507
+ });
508
+
509
+ test("passes with privileged: false", () => {
510
+ const ctx = makeCtx(JSON.stringify({
511
+ apiVersion: "apps/v1",
512
+ kind: "Deployment",
513
+ metadata: { name: "app" },
514
+ spec: {
515
+ template: {
516
+ spec: {
517
+ containers: [
518
+ { name: "app", image: "app:1.0", securityContext: { privileged: false } },
519
+ ],
520
+ },
521
+ },
522
+ },
523
+ }));
524
+ const diags = wk8202.check(ctx);
525
+ expect(diags.length).toBe(0);
526
+ });
527
+ });
528
+
529
+ // ── WK8203: readOnlyRootFilesystem ─────────────────────────────────
530
+
531
+ describe("WK8203: readOnlyRootFilesystem", () => {
532
+ test("flags missing readOnlyRootFilesystem", () => {
533
+ const ctx = makeCtx(JSON.stringify({
534
+ apiVersion: "apps/v1",
535
+ kind: "Deployment",
536
+ metadata: { name: "app" },
537
+ spec: {
538
+ template: {
539
+ spec: {
540
+ containers: [
541
+ { name: "app", image: "app:1.0", securityContext: {} },
542
+ ],
543
+ },
544
+ },
545
+ },
546
+ }));
547
+ const diags = wk8203.check(ctx);
548
+ expect(diags.length).toBeGreaterThanOrEqual(1);
549
+ expect(diags[0].checkId).toBe("WK8203");
550
+ });
551
+
552
+ test("passes with readOnlyRootFilesystem: true", () => {
553
+ const ctx = makeCtx(JSON.stringify({
554
+ apiVersion: "apps/v1",
555
+ kind: "Deployment",
556
+ metadata: { name: "app" },
557
+ spec: {
558
+ template: {
559
+ spec: {
560
+ containers: [
561
+ { name: "app", image: "app:1.0", securityContext: { readOnlyRootFilesystem: true } },
562
+ ],
563
+ },
564
+ },
565
+ },
566
+ }));
567
+ const diags = wk8203.check(ctx);
568
+ expect(diags.length).toBe(0);
569
+ });
570
+ });
571
+
572
+ // ── WK8204: runAsNonRoot ───────────────────────────────────────────
573
+
574
+ describe("WK8204: runAsNonRoot", () => {
575
+ test("flags missing runAsNonRoot", () => {
576
+ const ctx = makeCtx(JSON.stringify({
577
+ apiVersion: "apps/v1",
578
+ kind: "Deployment",
579
+ metadata: { name: "app" },
580
+ spec: {
581
+ template: {
582
+ spec: {
583
+ containers: [
584
+ { name: "app", image: "app:1.0", securityContext: {} },
585
+ ],
586
+ },
587
+ },
588
+ },
589
+ }));
590
+ const diags = wk8204.check(ctx);
591
+ expect(diags.length).toBeGreaterThanOrEqual(1);
592
+ expect(diags[0].checkId).toBe("WK8204");
593
+ });
594
+
595
+ test("passes with runAsNonRoot: true", () => {
596
+ const ctx = makeCtx(JSON.stringify({
597
+ apiVersion: "apps/v1",
598
+ kind: "Deployment",
599
+ metadata: { name: "app" },
600
+ spec: {
601
+ template: {
602
+ spec: {
603
+ containers: [
604
+ { name: "app", image: "app:1.0", securityContext: { runAsNonRoot: true } },
605
+ ],
606
+ },
607
+ },
608
+ },
609
+ }));
610
+ const diags = wk8204.check(ctx);
611
+ expect(diags.length).toBe(0);
612
+ });
613
+ });
614
+
615
+ // ── WK8205: Drop capabilities ──────────────────────────────────────
616
+
617
+ describe("WK8205: Drop all capabilities", () => {
618
+ test("flags missing capability drop", () => {
619
+ const ctx = makeCtx(JSON.stringify({
620
+ apiVersion: "apps/v1",
621
+ kind: "Deployment",
622
+ metadata: { name: "app" },
623
+ spec: {
624
+ template: {
625
+ spec: {
626
+ containers: [
627
+ { name: "app", image: "app:1.0", securityContext: {} },
628
+ ],
629
+ },
630
+ },
631
+ },
632
+ }));
633
+ const diags = wk8205.check(ctx);
634
+ expect(diags.length).toBeGreaterThanOrEqual(1);
635
+ expect(diags[0].checkId).toBe("WK8205");
636
+ });
637
+
638
+ test("passes with drop ALL", () => {
639
+ const ctx = makeCtx(JSON.stringify({
640
+ apiVersion: "apps/v1",
641
+ kind: "Deployment",
642
+ metadata: { name: "app" },
643
+ spec: {
644
+ template: {
645
+ spec: {
646
+ containers: [
647
+ { name: "app", image: "app:1.0", securityContext: { capabilities: { drop: ["ALL"] } } },
648
+ ],
649
+ },
650
+ },
651
+ },
652
+ }));
653
+ const diags = wk8205.check(ctx);
654
+ expect(diags.length).toBe(0);
655
+ });
656
+ });
657
+
658
+ // ── WK8207: hostNetwork ────────────────────────────────────────────
659
+
660
+ describe("WK8207: hostNetwork", () => {
661
+ test("flags hostNetwork: true", () => {
662
+ const ctx = makeCtx(`
663
+ apiVersion: apps/v1
664
+ kind: Deployment
665
+ metadata:
666
+ name: app
667
+ spec:
668
+ template:
669
+ spec:
670
+ hostNetwork: true
671
+ containers:
672
+ - name: app
673
+ image: app:1.0
674
+ `);
675
+ const diags = wk8207.check(ctx);
676
+ expect(diags.length).toBe(1);
677
+ expect(diags[0].checkId).toBe("WK8207");
678
+ });
679
+
680
+ test("passes without hostNetwork", () => {
681
+ const ctx = makeCtx(`
682
+ apiVersion: apps/v1
683
+ kind: Deployment
684
+ metadata:
685
+ name: app
686
+ spec:
687
+ template:
688
+ spec:
689
+ containers:
690
+ - name: app
691
+ image: app:1.0
692
+ `);
693
+ const diags = wk8207.check(ctx);
694
+ expect(diags.length).toBe(0);
695
+ });
696
+ });
697
+
698
+ // ── WK8208: hostPID ────────────────────────────────────────────────
699
+
700
+ describe("WK8208: hostPID", () => {
701
+ test("flags hostPID: true", () => {
702
+ const ctx = makeCtx(`
703
+ apiVersion: apps/v1
704
+ kind: Deployment
705
+ metadata:
706
+ name: app
707
+ spec:
708
+ template:
709
+ spec:
710
+ hostPID: true
711
+ containers:
712
+ - name: app
713
+ image: app:1.0
714
+ `);
715
+ const diags = wk8208.check(ctx);
716
+ expect(diags.length).toBe(1);
717
+ expect(diags[0].checkId).toBe("WK8208");
718
+ });
719
+
720
+ test("passes without hostPID", () => {
721
+ const ctx = makeCtx(`
722
+ apiVersion: apps/v1
723
+ kind: Deployment
724
+ metadata:
725
+ name: app
726
+ spec:
727
+ template:
728
+ spec:
729
+ containers:
730
+ - name: app
731
+ image: app:1.0
732
+ `);
733
+ const diags = wk8208.check(ctx);
734
+ expect(diags.length).toBe(0);
735
+ });
736
+ });
737
+
738
+ // ── WK8209: hostIPC ────────────────────────────────────────────────
739
+
740
+ describe("WK8209: hostIPC", () => {
741
+ test("flags hostIPC: true", () => {
742
+ const ctx = makeCtx(`
743
+ apiVersion: apps/v1
744
+ kind: Deployment
745
+ metadata:
746
+ name: app
747
+ spec:
748
+ template:
749
+ spec:
750
+ hostIPC: true
751
+ containers:
752
+ - name: app
753
+ image: app:1.0
754
+ `);
755
+ const diags = wk8209.check(ctx);
756
+ expect(diags.length).toBe(1);
757
+ expect(diags[0].checkId).toBe("WK8209");
758
+ });
759
+
760
+ test("passes without hostIPC", () => {
761
+ const ctx = makeCtx(`
762
+ apiVersion: apps/v1
763
+ kind: Deployment
764
+ metadata:
765
+ name: app
766
+ spec:
767
+ template:
768
+ spec:
769
+ containers:
770
+ - name: app
771
+ image: app:1.0
772
+ `);
773
+ const diags = wk8209.check(ctx);
774
+ expect(diags.length).toBe(0);
775
+ });
776
+ });
777
+
778
+ // ── WK8301: Probes required ────────────────────────────────────────
779
+
780
+ describe("WK8301: Probes required", () => {
781
+ test("flags container without probes", () => {
782
+ const ctx = makeCtx(JSON.stringify({
783
+ apiVersion: "apps/v1",
784
+ kind: "Deployment",
785
+ metadata: { name: "app" },
786
+ spec: {
787
+ template: {
788
+ spec: {
789
+ containers: [{ name: "app", image: "app:1.0" }],
790
+ },
791
+ },
792
+ },
793
+ }));
794
+ const diags = wk8301.check(ctx);
795
+ expect(diags.length).toBeGreaterThanOrEqual(1);
796
+ expect(diags[0].checkId).toBe("WK8301");
797
+ });
798
+
799
+ test("passes with both probes", () => {
800
+ const ctx = makeCtx(JSON.stringify({
801
+ apiVersion: "apps/v1",
802
+ kind: "Deployment",
803
+ metadata: { name: "app" },
804
+ spec: {
805
+ template: {
806
+ spec: {
807
+ containers: [
808
+ {
809
+ name: "app",
810
+ image: "app:1.0",
811
+ livenessProbe: { httpGet: { path: "/healthz", port: 8080 } },
812
+ readinessProbe: { httpGet: { path: "/readyz", port: 8080 } },
813
+ },
814
+ ],
815
+ },
816
+ },
817
+ },
818
+ }));
819
+ const diags = wk8301.check(ctx);
820
+ expect(diags.length).toBe(0);
821
+ });
822
+
823
+ test("skips Job (probes not needed)", () => {
824
+ const ctx = makeCtx(JSON.stringify({
825
+ apiVersion: "batch/v1",
826
+ kind: "Job",
827
+ metadata: { name: "job" },
828
+ spec: {
829
+ template: {
830
+ spec: {
831
+ containers: [{ name: "worker", image: "worker:1.0" }],
832
+ },
833
+ },
834
+ },
835
+ }));
836
+ const diags = wk8301.check(ctx);
837
+ expect(diags.length).toBe(0);
838
+ });
839
+
840
+ test("skips CronJob", () => {
841
+ const ctx = makeCtx(JSON.stringify({
842
+ apiVersion: "batch/v1",
843
+ kind: "CronJob",
844
+ metadata: { name: "cron" },
845
+ spec: {
846
+ schedule: "0 * * * *",
847
+ jobTemplate: {
848
+ spec: {
849
+ template: {
850
+ spec: {
851
+ containers: [{ name: "cron", image: "cron:1.0" }],
852
+ },
853
+ },
854
+ },
855
+ },
856
+ },
857
+ }));
858
+ const diags = wk8301.check(ctx);
859
+ expect(diags.length).toBe(0);
860
+ });
861
+ });
862
+
863
+ // ── WK8302: Single replica ─────────────────────────────────────────
864
+
865
+ describe("WK8302: Single replica deployment", () => {
866
+ test("flags replicas: 1", () => {
867
+ const ctx = makeCtx(`
868
+ apiVersion: apps/v1
869
+ kind: Deployment
870
+ metadata:
871
+ name: app
872
+ spec:
873
+ replicas: 1
874
+ template:
875
+ spec:
876
+ containers:
877
+ - name: app
878
+ image: app:1.0
879
+ `);
880
+ const diags = wk8302.check(ctx);
881
+ expect(diags.length).toBeGreaterThanOrEqual(1);
882
+ expect(diags[0].checkId).toBe("WK8302");
883
+ });
884
+
885
+ test("passes with replicas: 3", () => {
886
+ const ctx = makeCtx(`
887
+ apiVersion: apps/v1
888
+ kind: Deployment
889
+ metadata:
890
+ name: app
891
+ spec:
892
+ replicas: 3
893
+ template:
894
+ spec:
895
+ containers:
896
+ - name: app
897
+ image: app:1.0
898
+ `);
899
+ const diags = wk8302.check(ctx);
900
+ expect(diags.length).toBe(0);
901
+ });
902
+ });
903
+
904
+ // ── WK8303: PDB missing ───────────────────────────────────────────
905
+
906
+ describe("WK8303: HA Deployment without PDB", () => {
907
+ test("flags HA Deployment without PDB", () => {
908
+ const ctx = makeCtx(`
909
+ apiVersion: apps/v1
910
+ kind: Deployment
911
+ metadata:
912
+ name: app
913
+ labels:
914
+ app: my-app
915
+ spec:
916
+ replicas: 3
917
+ selector:
918
+ matchLabels:
919
+ app: my-app
920
+ template:
921
+ metadata:
922
+ labels:
923
+ app: my-app
924
+ spec:
925
+ containers:
926
+ - name: app
927
+ image: app:1.0
928
+ `);
929
+ const diags = wk8303.check(ctx);
930
+ expect(diags.length).toBeGreaterThanOrEqual(1);
931
+ expect(diags[0].checkId).toBe("WK8303");
932
+ });
933
+
934
+ test("passes with PDB present", () => {
935
+ const ctx = makeCtx(`
936
+ apiVersion: apps/v1
937
+ kind: Deployment
938
+ metadata:
939
+ name: app
940
+ labels:
941
+ app: my-app
942
+ spec:
943
+ replicas: 3
944
+ selector:
945
+ matchLabels:
946
+ app: my-app
947
+ template:
948
+ metadata:
949
+ labels:
950
+ app: my-app
951
+ spec:
952
+ containers:
953
+ - name: app
954
+ image: app:1.0
955
+ ---
956
+ apiVersion: policy/v1
957
+ kind: PodDisruptionBudget
958
+ metadata:
959
+ name: app-pdb
960
+ spec:
961
+ minAvailable: 1
962
+ selector:
963
+ matchLabels:
964
+ app: my-app
965
+ `);
966
+ const diags = wk8303.check(ctx);
967
+ expect(diags.length).toBe(0);
968
+ });
969
+ });