@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.
- package/dist/archive-render.js +76 -0
- package/dist/codegen/docs-cli.js +3 -0
- package/dist/codegen/docs.js +113 -0
- package/dist/codegen/generate-cli.js +19 -0
- package/dist/codegen/generate.js +210 -0
- package/dist/codegen/naming.js +29 -0
- package/dist/codegen/package.js +44 -0
- package/dist/codegen/values-type.js +87 -0
- package/dist/commands.js +413 -0
- package/dist/components/builders.js +9 -0
- package/dist/components/capability-plugin.js +17 -0
- package/dist/components/helm-upgrade.js +109 -0
- package/dist/components/index.js +11 -0
- package/dist/composites/catalog.js +1119 -0
- package/dist/composites/helm-batch-job.js +148 -0
- package/dist/composites/helm-crd-lifecycle.js +145 -0
- package/dist/composites/helm-cron-job.js +130 -0
- package/dist/composites/helm-daemon-set.js +125 -0
- package/dist/composites/helm-external-secret.js +63 -0
- package/dist/composites/helm-library.js +24 -0
- package/dist/composites/helm-microservice.js +260 -0
- package/dist/composites/helm-monitored-service.js +188 -0
- package/dist/composites/helm-namespace-env.js +116 -0
- package/dist/composites/helm-secure-ingress.js +86 -0
- package/dist/composites/helm-stateful-service.js +160 -0
- package/dist/composites/helm-web-app.js +201 -0
- package/dist/composites/helm-worker.js +164 -0
- package/dist/composites/index.js +13 -0
- package/dist/config.js +161 -0
- package/dist/coverage.js +37 -0
- package/dist/deep-observe-hooks.js +19 -0
- package/dist/deep-observe.js +55 -0
- package/dist/describe-resources.js +119 -0
- package/dist/describe-stack-status.js +66 -0
- package/dist/detect.js +11 -0
- package/dist/generated/index.js +11 -0
- package/dist/helpers.js +84 -0
- package/dist/import/generator.js +292 -0
- package/dist/import/parser.js +141 -0
- package/dist/import/template-stripper.js +86 -0
- package/dist/index.js +55 -0
- package/dist/integrity.json +2 -2
- package/dist/intrinsics.js +482 -0
- package/dist/lint/audit-catalog.js +33 -0
- package/dist/lint/audit-lineage.js +53 -0
- package/dist/lint/post-synth/helm-helpers.js +60 -0
- package/dist/lint/post-synth/index.js +48 -0
- package/dist/lint/post-synth/whm005-no-empty-wrapper.js +47 -0
- package/dist/lint/post-synth/whm101.js +38 -0
- package/dist/lint/post-synth/whm102.js +26 -0
- package/dist/lint/post-synth/whm103.js +51 -0
- package/dist/lint/post-synth/whm104.js +29 -0
- package/dist/lint/post-synth/whm105.js +23 -0
- package/dist/lint/post-synth/whm201.js +29 -0
- package/dist/lint/post-synth/whm202.js +42 -0
- package/dist/lint/post-synth/whm203.js +32 -0
- package/dist/lint/post-synth/whm204.js +51 -0
- package/dist/lint/post-synth/whm301.js +31 -0
- package/dist/lint/post-synth/whm302.js +33 -0
- package/dist/lint/post-synth/whm401.js +52 -0
- package/dist/lint/post-synth/whm402.js +38 -0
- package/dist/lint/post-synth/whm403.js +38 -0
- package/dist/lint/post-synth/whm404.js +30 -0
- package/dist/lint/post-synth/whm405.js +47 -0
- package/dist/lint/post-synth/whm406.js +27 -0
- package/dist/lint/post-synth/whm407.js +72 -0
- package/dist/lint/post-synth/whm501.js +89 -0
- package/dist/lint/post-synth/whm502.js +85 -0
- package/dist/lint/post-synth/whm503.js +82 -0
- package/dist/lint/post-synth/whm504.js +47 -0
- package/dist/lint/rules/chart-metadata.js +53 -0
- package/dist/lint/rules/no-hardcoded-image.js +50 -0
- package/dist/lint/rules/values-no-helm-tpl.js +78 -0
- package/dist/lint/rules/values-no-secrets.js +65 -0
- package/dist/list-artifacts.js +134 -0
- package/dist/lsp/completions.d.ts.map +1 -1
- package/dist/lsp/completions.js +18 -0
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/lsp/hover.js +40 -0
- package/dist/manifest.json +1 -1
- package/dist/op/activities/cluster-probe.js +116 -0
- package/dist/op/activities/helm.js +449 -0
- package/dist/op/activities/index.js +10 -0
- package/dist/op/builders.js +45 -0
- package/dist/package-cli.js +21 -0
- package/dist/pinnability/actions.js +207 -0
- package/dist/pinnability/classify.js +416 -0
- package/dist/pinnability/conditions.js +0 -0
- package/dist/pinnability/index.js +16 -0
- package/dist/pinnability/localize.js +518 -0
- package/dist/pinnability/render-stream.js +53 -0
- package/dist/pinnability/values.js +151 -0
- package/dist/plugin.js +458 -0
- package/dist/project-codegen.js +215 -0
- package/dist/release-observe.js +287 -0
- package/dist/render-diff.js +279 -0
- package/dist/render-digest.js +0 -0
- package/dist/render-records.js +29 -0
- package/dist/render-store.js +401 -0
- package/dist/render-wrapper.js +164 -0
- package/dist/render.js +394 -0
- package/dist/resources.js +109 -0
- package/dist/serializer.js +903 -0
- package/dist/validate-cli.js +17 -0
- package/dist/validate.js +30 -0
- package/dist/values-probe.js +504 -0
- package/dist/wrapper-chart.js +95 -0
- package/package.json +11 -11
- package/src/codegen/package.ts +2 -2
- package/src/lsp/completions.ts +2 -1
- 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
|
+
}
|