@intentius/chant-lexicon-helm 0.124.30 → 0.124.32

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 (111) hide show
  1. package/dist/archive-render.js +76 -0
  2. package/dist/codegen/docs-cli.js +3 -0
  3. package/dist/codegen/docs.js +113 -0
  4. package/dist/codegen/generate-cli.js +19 -0
  5. package/dist/codegen/generate.js +210 -0
  6. package/dist/codegen/naming.js +29 -0
  7. package/dist/codegen/package.js +44 -0
  8. package/dist/codegen/values-type.js +87 -0
  9. package/dist/commands.js +413 -0
  10. package/dist/components/builders.js +9 -0
  11. package/dist/components/capability-plugin.js +17 -0
  12. package/dist/components/helm-upgrade.js +109 -0
  13. package/dist/components/index.js +11 -0
  14. package/dist/composites/catalog.js +1119 -0
  15. package/dist/composites/helm-batch-job.js +148 -0
  16. package/dist/composites/helm-crd-lifecycle.js +145 -0
  17. package/dist/composites/helm-cron-job.js +130 -0
  18. package/dist/composites/helm-daemon-set.js +125 -0
  19. package/dist/composites/helm-external-secret.js +63 -0
  20. package/dist/composites/helm-library.js +24 -0
  21. package/dist/composites/helm-microservice.js +260 -0
  22. package/dist/composites/helm-monitored-service.js +188 -0
  23. package/dist/composites/helm-namespace-env.js +116 -0
  24. package/dist/composites/helm-secure-ingress.js +86 -0
  25. package/dist/composites/helm-stateful-service.js +160 -0
  26. package/dist/composites/helm-web-app.js +201 -0
  27. package/dist/composites/helm-worker.js +164 -0
  28. package/dist/composites/index.js +13 -0
  29. package/dist/config.js +161 -0
  30. package/dist/coverage.js +37 -0
  31. package/dist/deep-observe-hooks.js +19 -0
  32. package/dist/deep-observe.js +55 -0
  33. package/dist/describe-resources.js +119 -0
  34. package/dist/describe-stack-status.js +66 -0
  35. package/dist/detect.js +11 -0
  36. package/dist/generated/index.js +11 -0
  37. package/dist/helpers.js +84 -0
  38. package/dist/import/generator.js +292 -0
  39. package/dist/import/parser.js +141 -0
  40. package/dist/import/template-stripper.js +86 -0
  41. package/dist/index.js +55 -0
  42. package/dist/integrity.json +2 -2
  43. package/dist/intrinsics.js +482 -0
  44. package/dist/lint/audit-catalog.js +33 -0
  45. package/dist/lint/audit-lineage.js +53 -0
  46. package/dist/lint/post-synth/helm-helpers.js +60 -0
  47. package/dist/lint/post-synth/index.js +48 -0
  48. package/dist/lint/post-synth/whm005-no-empty-wrapper.js +47 -0
  49. package/dist/lint/post-synth/whm101.js +38 -0
  50. package/dist/lint/post-synth/whm102.js +26 -0
  51. package/dist/lint/post-synth/whm103.js +51 -0
  52. package/dist/lint/post-synth/whm104.js +29 -0
  53. package/dist/lint/post-synth/whm105.js +23 -0
  54. package/dist/lint/post-synth/whm201.js +29 -0
  55. package/dist/lint/post-synth/whm202.js +42 -0
  56. package/dist/lint/post-synth/whm203.js +32 -0
  57. package/dist/lint/post-synth/whm204.js +51 -0
  58. package/dist/lint/post-synth/whm301.js +31 -0
  59. package/dist/lint/post-synth/whm302.js +33 -0
  60. package/dist/lint/post-synth/whm401.js +52 -0
  61. package/dist/lint/post-synth/whm402.js +38 -0
  62. package/dist/lint/post-synth/whm403.js +38 -0
  63. package/dist/lint/post-synth/whm404.js +30 -0
  64. package/dist/lint/post-synth/whm405.js +47 -0
  65. package/dist/lint/post-synth/whm406.js +27 -0
  66. package/dist/lint/post-synth/whm407.js +72 -0
  67. package/dist/lint/post-synth/whm501.js +89 -0
  68. package/dist/lint/post-synth/whm502.js +85 -0
  69. package/dist/lint/post-synth/whm503.js +82 -0
  70. package/dist/lint/post-synth/whm504.js +47 -0
  71. package/dist/lint/rules/chart-metadata.js +53 -0
  72. package/dist/lint/rules/no-hardcoded-image.js +50 -0
  73. package/dist/lint/rules/values-no-helm-tpl.js +78 -0
  74. package/dist/lint/rules/values-no-secrets.js +65 -0
  75. package/dist/list-artifacts.js +134 -0
  76. package/dist/lsp/completions.d.ts.map +1 -1
  77. package/dist/lsp/completions.js +18 -0
  78. package/dist/lsp/hover.d.ts.map +1 -1
  79. package/dist/lsp/hover.js +40 -0
  80. package/dist/manifest.json +1 -1
  81. package/dist/op/activities/cluster-probe.js +116 -0
  82. package/dist/op/activities/helm.js +449 -0
  83. package/dist/op/activities/index.js +10 -0
  84. package/dist/op/builders.js +45 -0
  85. package/dist/package-cli.js +21 -0
  86. package/dist/pinnability/actions.js +207 -0
  87. package/dist/pinnability/classify.js +416 -0
  88. package/dist/pinnability/conditions.js +0 -0
  89. package/dist/pinnability/index.js +16 -0
  90. package/dist/pinnability/localize.js +518 -0
  91. package/dist/pinnability/render-stream.js +53 -0
  92. package/dist/pinnability/values.js +151 -0
  93. package/dist/plugin.js +458 -0
  94. package/dist/project-codegen.js +215 -0
  95. package/dist/release-observe.js +287 -0
  96. package/dist/render-diff.js +279 -0
  97. package/dist/render-digest.js +0 -0
  98. package/dist/render-records.js +29 -0
  99. package/dist/render-store.js +401 -0
  100. package/dist/render-wrapper.js +164 -0
  101. package/dist/render.js +394 -0
  102. package/dist/resources.js +109 -0
  103. package/dist/serializer.js +903 -0
  104. package/dist/validate-cli.js +17 -0
  105. package/dist/validate.js +30 -0
  106. package/dist/values-probe.js +504 -0
  107. package/dist/wrapper-chart.js +95 -0
  108. package/package.json +11 -11
  109. package/src/codegen/package.ts +2 -2
  110. package/src/lsp/completions.ts +2 -1
  111. package/src/lsp/hover.ts +2 -1
@@ -0,0 +1,27 @@
1
+ /**
2
+ * WHM406: Chart uses `crds/` directory — warn that Helm never upgrades/deletes CRDs.
3
+ *
4
+ * Suggests using `HelmCRDLifecycle` composite for managed CRD lifecycle.
5
+ */
6
+ import { getChartFiles } from "./helm-helpers.js";
7
+ export const whm406 = {
8
+ id: "WHM406",
9
+ description: "CRDs in crds/ directory are never upgraded or deleted by Helm",
10
+ check(ctx) {
11
+ const diagnostics = [];
12
+ for (const [, output] of ctx.outputs) {
13
+ const files = getChartFiles(output);
14
+ const hasCrdDir = Object.keys(files).some((f) => f.startsWith("crds/"));
15
+ if (hasCrdDir) {
16
+ diagnostics.push({
17
+ checkId: "WHM406",
18
+ severity: "info",
19
+ message: "Chart uses crds/ directory — Helm installs CRDs but never upgrades or deletes them. Consider using HelmCRDLifecycle composite for managed CRD lifecycle.",
20
+ entity: "crds/",
21
+ lexicon: "helm",
22
+ });
23
+ }
24
+ }
25
+ return diagnostics;
26
+ },
27
+ };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * WHM407: `kind: Secret` with inline `data:` values and no ExternalSecret/SealedSecret.
3
+ *
4
+ * Warns when secrets contain inline data values (not `.Values` refs) and the chart
5
+ * does not use ExternalSecret or SealedSecret resources.
6
+ */
7
+ import { getChartFiles } from "./helm-helpers.js";
8
+ export const whm407 = {
9
+ id: "WHM407",
10
+ description: "Secrets with inline data should use ExternalSecret or SealedSecret",
11
+ check(ctx) {
12
+ const diagnostics = [];
13
+ for (const [, output] of ctx.outputs) {
14
+ const files = getChartFiles(output);
15
+ // Check if chart uses ExternalSecret or SealedSecret anywhere
16
+ const allContent = Object.values(files).join("\n");
17
+ const hasExternalSecret = allContent.includes("ExternalSecret") || allContent.includes("SealedSecret");
18
+ for (const [filename, content] of Object.entries(files)) {
19
+ if (!filename.startsWith("templates/") || filename.endsWith("_helpers.tpl") || filename.endsWith("NOTES.txt"))
20
+ continue;
21
+ if (filename.includes("tests/"))
22
+ continue;
23
+ if (!content.includes("kind: Secret"))
24
+ continue;
25
+ // Scan for inline data values in data: or stringData: sections
26
+ const lines = content.split("\n");
27
+ let inData = false;
28
+ let dataIndent = -1;
29
+ let hasLiteralValues = false;
30
+ for (const line of lines) {
31
+ const trimmed = line.trim();
32
+ if (!trimmed || trimmed.startsWith("#"))
33
+ continue;
34
+ const indent = line.length - line.trimStart().length;
35
+ if (trimmed === "data:" || trimmed === "stringData:") {
36
+ inData = true;
37
+ dataIndent = indent;
38
+ continue;
39
+ }
40
+ if (inData) {
41
+ // If we've returned to the same or lower indent level, exit data section
42
+ if (indent <= dataIndent && trimmed) {
43
+ inData = false;
44
+ dataIndent = -1;
45
+ continue;
46
+ }
47
+ // Check if value is a literal (not a template expression)
48
+ if (!trimmed.includes("{{") && !trimmed.includes(".Values")) {
49
+ const colonIdx = trimmed.indexOf(":");
50
+ if (colonIdx > 0 && trimmed.length > colonIdx + 1 && trimmed[colonIdx + 1] === " ") {
51
+ const val = trimmed.slice(colonIdx + 1).trim();
52
+ if (val) {
53
+ hasLiteralValues = true;
54
+ }
55
+ }
56
+ }
57
+ }
58
+ }
59
+ if (hasLiteralValues && !hasExternalSecret) {
60
+ diagnostics.push({
61
+ checkId: "WHM407",
62
+ severity: "warning",
63
+ message: `${filename}: Secret with inline data values — consider using ExternalSecret or SealedSecret for secret management`,
64
+ entity: filename,
65
+ lexicon: "helm",
66
+ });
67
+ }
68
+ }
69
+ }
70
+ return diagnostics;
71
+ },
72
+ };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * WHM501: Unused values keys.
3
+ *
4
+ * Parses values.yaml key paths and scans all template files for
5
+ * `.Values.` references. Keys defined but never referenced produce
6
+ * an info diagnostic.
7
+ */
8
+ import { getChartFiles } from "./helm-helpers.js";
9
+ /** Keys that are always implicitly used (e.g. in _helpers.tpl). */
10
+ const IMPLICIT_KEYS = new Set(["nameOverride", "fullnameOverride"]);
11
+ /**
12
+ * Extract all top-level and nested key paths from values.yaml content.
13
+ * Uses indentation to track nesting (2-space indent per level).
14
+ */
15
+ function extractValuePaths(content) {
16
+ const paths = [];
17
+ const stack = [];
18
+ let prevIndent = -1;
19
+ for (const line of content.split("\n")) {
20
+ const trimmed = line.trimStart();
21
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-"))
22
+ continue;
23
+ const indent = line.length - trimmed.length;
24
+ const match = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:/);
25
+ if (!match)
26
+ continue;
27
+ const key = match[1];
28
+ // Adjust stack based on indentation
29
+ while (stack.length > 0 && indent <= prevIndent) {
30
+ stack.pop();
31
+ prevIndent -= 2;
32
+ }
33
+ stack.push(key);
34
+ paths.push(stack.join("."));
35
+ prevIndent = indent;
36
+ }
37
+ return paths;
38
+ }
39
+ export const whm501 = {
40
+ id: "WHM501",
41
+ description: "Detect values keys that are defined but never referenced in templates",
42
+ check(ctx) {
43
+ const diagnostics = [];
44
+ for (const [, output] of ctx.outputs) {
45
+ const files = getChartFiles(output);
46
+ const valuesContent = files["values.yaml"];
47
+ if (!valuesContent || valuesContent.trim() === "{}" || valuesContent.trim() === "")
48
+ continue;
49
+ const definedPaths = extractValuePaths(valuesContent);
50
+ if (definedPaths.length === 0)
51
+ continue;
52
+ // Collect all .Values references from templates
53
+ const referencedPaths = new Set();
54
+ const valuesRegex = /\.Values\.([a-zA-Z0-9_.]+)/g;
55
+ for (const [filename, content] of Object.entries(files)) {
56
+ if (!filename.startsWith("templates/"))
57
+ continue;
58
+ let match;
59
+ while ((match = valuesRegex.exec(content)) !== null) {
60
+ referencedPaths.add(match[1]);
61
+ }
62
+ }
63
+ for (const path of definedPaths) {
64
+ if (IMPLICIT_KEYS.has(path))
65
+ continue;
66
+ // Check if this path or any child is referenced
67
+ const isReferenced = referencedPaths.has(path) ||
68
+ [...referencedPaths].some((ref) => ref.startsWith(path + "."));
69
+ // Check if any parent of this path is referenced (parent consumed entirely)
70
+ const parts = path.split(".");
71
+ const parentReferenced = parts.some((_, i) => {
72
+ if (i === parts.length - 1)
73
+ return false;
74
+ return referencedPaths.has(parts.slice(0, i + 1).join("."));
75
+ });
76
+ if (!isReferenced && !parentReferenced) {
77
+ diagnostics.push({
78
+ checkId: "WHM501",
79
+ severity: "info",
80
+ message: `values.yaml defines "${path}" but it is never referenced in templates`,
81
+ entity: "values.yaml",
82
+ lexicon: "helm",
83
+ });
84
+ }
85
+ }
86
+ }
87
+ return diagnostics;
88
+ },
89
+ };
@@ -0,0 +1,85 @@
1
+ /**
2
+ * WHM502: K8s API version/kind validation.
3
+ *
4
+ * Checks for deprecated or invalid Kubernetes API versions in templates
5
+ * and suggests replacements.
6
+ */
7
+ import { getChartFiles } from "./helm-helpers.js";
8
+ /**
9
+ * Deprecated API versions with their replacements.
10
+ */
11
+ const DEPRECATED_APIS = {
12
+ "extensions/v1beta1": {
13
+ replacement: "networking.k8s.io/v1",
14
+ kinds: ["Ingress"],
15
+ },
16
+ "networking.k8s.io/v1beta1": {
17
+ replacement: "networking.k8s.io/v1",
18
+ kinds: ["Ingress", "IngressClass"],
19
+ },
20
+ "apps/v1beta1": {
21
+ replacement: "apps/v1",
22
+ kinds: ["Deployment", "StatefulSet"],
23
+ },
24
+ "apps/v1beta2": {
25
+ replacement: "apps/v1",
26
+ kinds: ["Deployment", "StatefulSet", "DaemonSet", "ReplicaSet"],
27
+ },
28
+ "rbac.authorization.k8s.io/v1beta1": {
29
+ replacement: "rbac.authorization.k8s.io/v1",
30
+ kinds: ["ClusterRole", "ClusterRoleBinding", "Role", "RoleBinding"],
31
+ },
32
+ "admissionregistration.k8s.io/v1beta1": {
33
+ replacement: "admissionregistration.k8s.io/v1",
34
+ kinds: ["MutatingWebhookConfiguration", "ValidatingWebhookConfiguration"],
35
+ },
36
+ "batch/v1beta1": {
37
+ replacement: "batch/v1",
38
+ kinds: ["CronJob"],
39
+ },
40
+ "policy/v1beta1": {
41
+ replacement: "policy/v1",
42
+ kinds: ["PodDisruptionBudget", "PodSecurityPolicy"],
43
+ },
44
+ "autoscaling/v2beta1": {
45
+ replacement: "autoscaling/v2",
46
+ kinds: ["HorizontalPodAutoscaler"],
47
+ },
48
+ "autoscaling/v2beta2": {
49
+ replacement: "autoscaling/v2",
50
+ kinds: ["HorizontalPodAutoscaler"],
51
+ },
52
+ };
53
+ export const whm502 = {
54
+ id: "WHM502",
55
+ description: "Detect deprecated or invalid Kubernetes API versions",
56
+ check(ctx) {
57
+ const diagnostics = [];
58
+ for (const [, output] of ctx.outputs) {
59
+ const files = getChartFiles(output);
60
+ for (const [filename, content] of Object.entries(files)) {
61
+ if (!filename.startsWith("templates/") || filename.endsWith("_helpers.tpl") || filename.endsWith("NOTES.txt"))
62
+ continue;
63
+ // Extract apiVersion from template
64
+ const apiVersionMatch = content.match(/apiVersion:\s*(.+)/);
65
+ if (!apiVersionMatch)
66
+ continue;
67
+ const apiVersion = apiVersionMatch[1].trim();
68
+ // Skip template expressions
69
+ if (apiVersion.includes("{{"))
70
+ continue;
71
+ const deprecation = DEPRECATED_APIS[apiVersion];
72
+ if (deprecation) {
73
+ diagnostics.push({
74
+ checkId: "WHM502",
75
+ severity: "warning",
76
+ message: `${filename}: apiVersion "${apiVersion}" is deprecated — use "${deprecation.replacement}" instead`,
77
+ entity: filename,
78
+ lexicon: "helm",
79
+ });
80
+ }
81
+ }
82
+ }
83
+ return diagnostics;
84
+ },
85
+ };
@@ -0,0 +1,82 @@
1
+ /**
2
+ * WHM503: Pinned render artifact carries Secret data.
3
+ *
4
+ * WHM407 covers the pre-render concern — a chart TEMPLATE with an inline,
5
+ * literal `data`/`stringData` value baked into `templates/`. This rule
6
+ * covers a different concern (#1241, epic #1228 Phase 3): the STORED,
7
+ * POST-RENDER artifact (`render-store.ts`, #1238). A pinned render is
8
+ * durable, promoted, and pushed to a registry — unlike a normal `helm
9
+ * upgrade`, whose rendered bytes are computed at deploy time and discarded.
10
+ * By the time a render lands in the store, every `.Values` reference and
11
+ * `{{ }}` expression is resolved, so a `kind: Secret` document with
12
+ * populated `data`/`stringData` there is a live credential baked into
13
+ * something that outlives the deploy that produced it.
14
+ *
15
+ * Scans every pinned render recorded so far in this process
16
+ * (`getHelmRenderRecords()` from `../../render`) that carries a
17
+ * `contentDigest`, loads its manifest + canonical content from the render
18
+ * store, and flags any `Secret` document whose `data` or `stringData` is
19
+ * non-empty.
20
+ */
21
+ import yaml from "js-yaml";
22
+ import { getHelmRenderRecords } from "../../render-records.js";
23
+ import { loadRenderManifest, readRenderDocument } from "../../render-store.js";
24
+ /** True when a `data`/`stringData` map carries at least one key. */
25
+ function isPopulated(value) {
26
+ return typeof value === "object" && value !== null && Object.keys(value).length > 0;
27
+ }
28
+ /**
29
+ * Scan a set of `HelmRenderRecord`s for pinned renders whose stored artifact
30
+ * carries a `Secret` document with populated `data`/`stringData`. Exported
31
+ * separately from `whm503` so tests can drive it directly with hand-built
32
+ * records — no real `helm` binary required, unlike a `HelmRender()` call.
33
+ * `opts.root` overrides the render store root (test isolation).
34
+ */
35
+ export function checkRenderRecordsForSecrets(records, opts) {
36
+ const diagnostics = [];
37
+ for (const record of records) {
38
+ if (!record.contentDigest)
39
+ continue; // unpinned — no stored artifact to scan
40
+ const manifest = loadRenderManifest(record.contentDigest, opts);
41
+ if (!manifest)
42
+ continue; // recorded but not (or no longer) in the store
43
+ for (const doc of manifest.documents) {
44
+ if (doc.kind !== "Secret")
45
+ continue;
46
+ const found = readRenderDocument(record.contentDigest, { kind: doc.kind, name: doc.name, namespace: doc.namespace }, opts);
47
+ if (!found)
48
+ continue;
49
+ let parsed;
50
+ try {
51
+ parsed = (yaml.load(found.text) ?? {});
52
+ }
53
+ catch {
54
+ continue; // unparseable — not this check's problem to diagnose
55
+ }
56
+ if (!isPopulated(parsed.data) && !isPopulated(parsed.stringData))
57
+ continue;
58
+ const locator = doc.namespace ? `${doc.namespace}/${doc.name}` : doc.name;
59
+ diagnostics.push({
60
+ checkId: "WHM503",
61
+ severity: "error",
62
+ message: `pinned render "${record.name}" (${record.chart}, ${record.contentDigest}): Secret "${locator}" ` +
63
+ `carries populated data/stringData — a pinned render is stored, promoted, and pushed to a registry, ` +
64
+ `so this bakes a live credential into a durable artifact. Declare the value with runtimeSlot() so ` +
65
+ `it is supplied per environment, or replace this Secret with HelmExternalSecret.`,
66
+ entity: locator,
67
+ lexicon: "helm",
68
+ });
69
+ }
70
+ }
71
+ return diagnostics;
72
+ }
73
+ export const whm503 = {
74
+ id: "WHM503",
75
+ description: "Pinned render artifact must not carry populated Secret data/stringData",
76
+ // This check's data source is the render store (#1238), not `ctx.outputs`
77
+ // (the un-rendered chart template files WHM005-WHM502 read) — see the file
78
+ // doc comment. `ctx` is accepted to satisfy `PostSynthCheck` and is unused.
79
+ check(_ctx) {
80
+ return checkRenderRecordsForSecrets(getHelmRenderRecords());
81
+ },
82
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * WHM504: Dead value assignment (#1252, epic #1228 Phase 7).
3
+ *
4
+ * A supplied value that never survives coalescing is a lie in the config:
5
+ * it reads as if it configures something, and nothing ever sees it. The
6
+ * coalesced-values probe (#1251, ../../values-probe.ts) knows three ways a
7
+ * supplied value dies — shadowed by a later supplied layer, targeting a
8
+ * dependency a `condition:` disabled, or a values map under a top-level key
9
+ * that names no dependency at all (the silently-ignored subchart typo).
10
+ *
11
+ * The check reports over the probe records of the current process
12
+ * (`getValuesProbeRecords`): whatever ran the probe — the pinned-render
13
+ * build path, a test, a direct call — this check turns its dead assignments
14
+ * into diagnostics. No probe run, nothing to report; the probe itself needs
15
+ * the helm binary and the chart source, which a post-synth check must not
16
+ * require on its own.
17
+ */
18
+ import { getValuesProbeRecords } from "../../values-probe.js";
19
+ function describe(dead) {
20
+ switch (dead.reason) {
21
+ case "shadowed":
22
+ return `supplied value "${dead.path}" (${dead.origin}) never survives coalescing — shadowed by ${dead.shadowedBy}`;
23
+ case "disabled-subchart":
24
+ return `supplied value "${dead.path}" (${dead.origin}) targets a disabled subchart — ${dead.shadowedBy}`;
25
+ case "unknown-subchart":
26
+ return `supplied values under "${dead.path}" (${dead.origin}) target no subchart — ${dead.shadowedBy}`;
27
+ }
28
+ }
29
+ export const whm504 = {
30
+ id: "WHM504",
31
+ description: "Detect supplied values that never survive coalescing (shadowed, disabled subchart, unknown subchart)",
32
+ check(_ctx) {
33
+ const diagnostics = [];
34
+ for (const record of getValuesProbeRecords()) {
35
+ for (const dead of record.probe.deadAssignments) {
36
+ diagnostics.push({
37
+ checkId: "WHM504",
38
+ severity: "warning",
39
+ message: describe(dead),
40
+ entity: record.name,
41
+ lexicon: "helm",
42
+ });
43
+ }
44
+ }
45
+ return diagnostics;
46
+ },
47
+ };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * WHM001: Chart Metadata Required
3
+ *
4
+ * Detects Chart constructors missing required fields: name, version, apiVersion.
5
+ *
6
+ * Bad: new Chart({})
7
+ * Good: new Chart({ apiVersion: "v2", name: "my-app", version: "0.1.0" })
8
+ */
9
+ import { ts } from "@intentius/chant/lint/typescript";
10
+ const REQUIRED_FIELDS = ["name", "version", "apiVersion"];
11
+ export const chartMetadataRule = {
12
+ id: "WHM001",
13
+ severity: "error",
14
+ category: "correctness",
15
+ description: "Chart must have name, version, and apiVersion fields",
16
+ check(context) {
17
+ const { sourceFile } = context;
18
+ const diagnostics = [];
19
+ function visit(node) {
20
+ // Look for `new Chart({ ... })`
21
+ if (ts.isNewExpression(node) &&
22
+ ts.isIdentifier(node.expression) &&
23
+ node.expression.text === "Chart" &&
24
+ node.arguments &&
25
+ node.arguments.length > 0) {
26
+ const arg = node.arguments[0];
27
+ if (ts.isObjectLiteralExpression(arg)) {
28
+ const presentKeys = new Set();
29
+ for (const prop of arg.properties) {
30
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
31
+ presentKeys.add(prop.name.text);
32
+ }
33
+ }
34
+ const missing = REQUIRED_FIELDS.filter((f) => !presentKeys.has(f));
35
+ if (missing.length > 0) {
36
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
37
+ diagnostics.push({
38
+ file: sourceFile.fileName,
39
+ line: line + 1,
40
+ column: character + 1,
41
+ ruleId: "WHM001",
42
+ severity: "error",
43
+ message: `Chart is missing required fields: ${missing.join(", ")}`,
44
+ });
45
+ }
46
+ }
47
+ }
48
+ ts.forEachChild(node, visit);
49
+ }
50
+ visit(sourceFile);
51
+ return diagnostics;
52
+ },
53
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * WHM003: Container Images Should Use Values References
3
+ *
4
+ * Detects container image strings that are hardcoded in K8s resource
5
+ * constructors instead of using values references. In Helm charts, images
6
+ * should be parameterized via values.yaml so they can be overridden at
7
+ * install time.
8
+ *
9
+ * Bad: image: "nginx:1.19"
10
+ * Good: image: printf("%s:%s", values.image.repository, values.image.tag)
11
+ */
12
+ import { ts } from "@intentius/chant/lint/typescript";
13
+ /**
14
+ * Pattern matching common container image references.
15
+ * Matches strings like "nginx:1.19", "registry.io/app:latest", etc.
16
+ */
17
+ const IMAGE_PATTERN = /^[a-z0-9][a-z0-9._/-]*:[a-z0-9][a-z0-9._-]*$/i;
18
+ export const noHardcodedImageRule = {
19
+ id: "WHM003",
20
+ severity: "warning",
21
+ category: "correctness",
22
+ description: "Container images should use values references, not hardcoded tags",
23
+ check(context) {
24
+ const { sourceFile } = context;
25
+ const diagnostics = [];
26
+ function visit(node) {
27
+ // Look for property assignments: `image: "nginx:1.19"`
28
+ if (ts.isPropertyAssignment(node) &&
29
+ ts.isIdentifier(node.name) &&
30
+ node.name.text === "image" &&
31
+ ts.isStringLiteral(node.initializer)) {
32
+ const value = node.initializer.text;
33
+ if (IMAGE_PATTERN.test(value)) {
34
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.initializer.getStart());
35
+ diagnostics.push({
36
+ file: sourceFile.fileName,
37
+ line: line + 1,
38
+ column: character + 1,
39
+ ruleId: "WHM003",
40
+ severity: "warning",
41
+ message: `Hardcoded image "${value}" — use values references (e.g. printf("%s:%s", values.image.repository, values.image.tag)) for Helm chart parameterization`,
42
+ });
43
+ }
44
+ }
45
+ ts.forEachChild(node, visit);
46
+ }
47
+ visit(sourceFile);
48
+ return diagnostics;
49
+ },
50
+ };
@@ -0,0 +1,78 @@
1
+ /**
2
+ * WHM004: HelmTpl Expression Has No Effect in Values Constructor
3
+ *
4
+ * Detects Values constructor props that use `v.xxx` (the `values` proxy)
5
+ * or any HelmTpl-like expression. values.yaml is static YAML — it is NOT
6
+ * processed as a Go template by Helm. These expressions silently become ''.
7
+ *
8
+ * Bad: new Values({ host: v.pgHost })
9
+ * Good: new Values({ host: runtimeSlot("Cloud SQL private IP") })
10
+ */
11
+ import { ts } from "@intentius/chant/lint/typescript";
12
+ export const valuesNoHelmTplRule = {
13
+ id: "WHM004",
14
+ severity: "warning",
15
+ category: "correctness",
16
+ description: "HelmTpl expression has no effect in values.yaml — use runtimeSlot() for deploy-time values",
17
+ check(context) {
18
+ const { sourceFile } = context;
19
+ const diagnostics = [];
20
+ function visit(node) {
21
+ if (ts.isNewExpression(node) &&
22
+ ts.isIdentifier(node.expression) &&
23
+ node.expression.text === "Values" &&
24
+ node.arguments &&
25
+ node.arguments.length > 0) {
26
+ const arg = node.arguments[0];
27
+ if (ts.isObjectLiteralExpression(arg)) {
28
+ checkObjectLiteral(arg, [], sourceFile, diagnostics);
29
+ }
30
+ }
31
+ ts.forEachChild(node, visit);
32
+ }
33
+ visit(sourceFile);
34
+ return diagnostics;
35
+ },
36
+ };
37
+ /**
38
+ * Get the root identifier name of a property access / call chain.
39
+ * v.foo → "v"; values.x.pipe("fn") → "values"; runtimeSlot() → "runtimeSlot"
40
+ */
41
+ function getRootIdentifier(node) {
42
+ if (ts.isIdentifier(node))
43
+ return node.text;
44
+ if (ts.isPropertyAccessExpression(node))
45
+ return getRootIdentifier(node.expression);
46
+ if (ts.isCallExpression(node))
47
+ return getRootIdentifier(node.expression);
48
+ return null;
49
+ }
50
+ function isHelmTplExpr(node) {
51
+ const root = getRootIdentifier(node);
52
+ return root === "v" || root === "values";
53
+ }
54
+ function checkObjectLiteral(obj, path, sourceFile, diagnostics) {
55
+ for (const prop of obj.properties) {
56
+ if (!ts.isPropertyAssignment(prop))
57
+ continue;
58
+ const keyName = ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)
59
+ ? prop.name.text
60
+ : undefined;
61
+ const propPath = keyName ? [...path, keyName] : path;
62
+ if (isHelmTplExpr(prop.initializer)) {
63
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(prop.getStart());
64
+ const pathStr = propPath.join(".");
65
+ diagnostics.push({
66
+ file: sourceFile.fileName,
67
+ line: line + 1,
68
+ column: character + 1,
69
+ ruleId: "WHM004",
70
+ severity: "warning",
71
+ message: `HelmTpl expression has no effect in values.yaml (values.yaml is not a Go template). Use runtimeSlot() for deploy-time values or a static default.${pathStr ? ` (path: ${pathStr})` : ""}`,
72
+ });
73
+ }
74
+ else if (ts.isObjectLiteralExpression(prop.initializer)) {
75
+ checkObjectLiteral(prop.initializer, propPath, sourceFile, diagnostics);
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * WHM002: Values Should Not Contain Bare Secrets
3
+ *
4
+ * Detects Values constructor props that contain keys suggesting sensitive
5
+ * data (password, token, key, secret, apiKey) without an `existingSecret`
6
+ * pattern — hardcoded secrets in values.yaml are a security anti-pattern.
7
+ *
8
+ * Bad: new Values({ dbPassword: "hunter2" })
9
+ * Good: new Values({ existingSecret: "", dbPasswordKey: "password" })
10
+ */
11
+ import { ts } from "@intentius/chant/lint/typescript";
12
+ const SENSITIVE_KEY_PATTERN = /^(password|secret|token|apiKey|api_key|private_key|privateKey)$/i;
13
+ const SAFE_PATTERNS = /existing|ref|key_name|keyName|secretName/i;
14
+ export const valuesNoSecretsRule = {
15
+ id: "WHM002",
16
+ severity: "warning",
17
+ category: "security",
18
+ description: "Values should not contain bare secrets — use existingSecret pattern instead",
19
+ check(context) {
20
+ const { sourceFile } = context;
21
+ const diagnostics = [];
22
+ function visit(node) {
23
+ // Look for `new Values({ ... })`
24
+ if (ts.isNewExpression(node) &&
25
+ ts.isIdentifier(node.expression) &&
26
+ node.expression.text === "Values" &&
27
+ node.arguments &&
28
+ node.arguments.length > 0) {
29
+ const arg = node.arguments[0];
30
+ if (ts.isObjectLiteralExpression(arg)) {
31
+ checkObjectLiteral(arg, sourceFile, diagnostics);
32
+ }
33
+ }
34
+ ts.forEachChild(node, visit);
35
+ }
36
+ visit(sourceFile);
37
+ return diagnostics;
38
+ },
39
+ };
40
+ function checkObjectLiteral(obj, sourceFile, diagnostics) {
41
+ for (const prop of obj.properties) {
42
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
43
+ const key = prop.name.text;
44
+ // Check if the key looks sensitive
45
+ if (SENSITIVE_KEY_PATTERN.test(key) && !SAFE_PATTERNS.test(key)) {
46
+ // Check if the value is a string literal (hardcoded secret)
47
+ if (ts.isStringLiteral(prop.initializer) && prop.initializer.text !== "") {
48
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(prop.getStart());
49
+ diagnostics.push({
50
+ file: sourceFile.fileName,
51
+ line: line + 1,
52
+ column: character + 1,
53
+ ruleId: "WHM002",
54
+ severity: "warning",
55
+ message: `Values key "${key}" contains a hardcoded secret — use existingSecret pattern or leave empty as default`,
56
+ });
57
+ }
58
+ }
59
+ // Recurse into nested objects
60
+ if (ts.isObjectLiteralExpression(prop.initializer)) {
61
+ checkObjectLiteral(prop.initializer, sourceFile, diagnostics);
62
+ }
63
+ }
64
+ }
65
+ }