@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.
- package/dist/integrity.json +32 -0
- package/dist/manifest.json +8 -0
- package/dist/meta.json +1413 -0
- package/dist/rules/hardcoded-namespace.ts +56 -0
- package/dist/rules/k8s-helpers.ts +149 -0
- package/dist/rules/wk8005.ts +59 -0
- package/dist/rules/wk8006.ts +68 -0
- package/dist/rules/wk8041.ts +73 -0
- package/dist/rules/wk8042.ts +48 -0
- package/dist/rules/wk8101.ts +65 -0
- package/dist/rules/wk8102.ts +42 -0
- package/dist/rules/wk8103.ts +45 -0
- package/dist/rules/wk8104.ts +69 -0
- package/dist/rules/wk8105.ts +45 -0
- package/dist/rules/wk8201.ts +55 -0
- package/dist/rules/wk8202.ts +46 -0
- package/dist/rules/wk8203.ts +46 -0
- package/dist/rules/wk8204.ts +54 -0
- package/dist/rules/wk8205.ts +56 -0
- package/dist/rules/wk8207.ts +45 -0
- package/dist/rules/wk8208.ts +45 -0
- package/dist/rules/wk8209.ts +45 -0
- package/dist/rules/wk8301.ts +51 -0
- package/dist/rules/wk8302.ts +46 -0
- package/dist/rules/wk8303.ts +96 -0
- package/dist/skills/chant-k8s.md +433 -0
- package/dist/types/index.d.ts +2934 -0
- package/package.json +30 -0
- package/src/actions/actions.test.ts +83 -0
- package/src/actions/apps.ts +23 -0
- package/src/actions/batch.ts +9 -0
- package/src/actions/core.ts +62 -0
- package/src/actions/index.ts +50 -0
- package/src/actions/networking.ts +15 -0
- package/src/actions/rbac.ts +13 -0
- package/src/codegen/docs-cli.ts +3 -0
- package/src/codegen/docs.ts +1147 -0
- package/src/codegen/generate-cli.ts +41 -0
- package/src/codegen/generate-lexicon.ts +69 -0
- package/src/codegen/generate-typescript.ts +97 -0
- package/src/codegen/generate.ts +144 -0
- package/src/codegen/naming.test.ts +63 -0
- package/src/codegen/naming.ts +187 -0
- package/src/codegen/package.ts +56 -0
- package/src/codegen/patches.ts +108 -0
- package/src/codegen/snapshot.test.ts +95 -0
- package/src/codegen/typecheck.test.ts +24 -0
- package/src/codegen/typecheck.ts +4 -0
- package/src/codegen/versions.ts +43 -0
- package/src/composites/autoscaled-service.ts +236 -0
- package/src/composites/composites.test.ts +1109 -0
- package/src/composites/cron-workload.ts +167 -0
- package/src/composites/index.ts +14 -0
- package/src/composites/namespace-env.ts +163 -0
- package/src/composites/node-agent.ts +224 -0
- package/src/composites/stateful-app.ts +134 -0
- package/src/composites/web-app.ts +180 -0
- package/src/composites/worker-pool.ts +230 -0
- package/src/coverage.test.ts +27 -0
- package/src/coverage.ts +35 -0
- package/src/crd/loader.ts +112 -0
- package/src/crd/parser.test.ts +217 -0
- package/src/crd/parser.ts +279 -0
- package/src/crd/types.ts +54 -0
- package/src/default-labels.test.ts +111 -0
- package/src/default-labels.ts +122 -0
- package/src/generated/index.d.ts +2934 -0
- package/src/generated/index.ts +203 -0
- package/src/generated/lexicon-k8s.json +1413 -0
- package/src/generated/runtime.ts +4 -0
- package/src/import/generator.test.ts +121 -0
- package/src/import/generator.ts +285 -0
- package/src/import/parser.test.ts +156 -0
- package/src/import/parser.ts +204 -0
- package/src/import/roundtrip.test.ts +86 -0
- package/src/index.ts +38 -0
- package/src/lint/post-synth/k8s-helpers.test.ts +219 -0
- package/src/lint/post-synth/k8s-helpers.ts +149 -0
- package/src/lint/post-synth/post-synth.test.ts +969 -0
- package/src/lint/post-synth/wk8005.ts +59 -0
- package/src/lint/post-synth/wk8006.ts +68 -0
- package/src/lint/post-synth/wk8041.ts +73 -0
- package/src/lint/post-synth/wk8042.ts +48 -0
- package/src/lint/post-synth/wk8101.ts +65 -0
- package/src/lint/post-synth/wk8102.ts +42 -0
- package/src/lint/post-synth/wk8103.ts +45 -0
- package/src/lint/post-synth/wk8104.ts +69 -0
- package/src/lint/post-synth/wk8105.ts +45 -0
- package/src/lint/post-synth/wk8201.ts +55 -0
- package/src/lint/post-synth/wk8202.ts +46 -0
- package/src/lint/post-synth/wk8203.ts +46 -0
- package/src/lint/post-synth/wk8204.ts +54 -0
- package/src/lint/post-synth/wk8205.ts +56 -0
- package/src/lint/post-synth/wk8207.ts +45 -0
- package/src/lint/post-synth/wk8208.ts +45 -0
- package/src/lint/post-synth/wk8209.ts +45 -0
- package/src/lint/post-synth/wk8301.ts +51 -0
- package/src/lint/post-synth/wk8302.ts +46 -0
- package/src/lint/post-synth/wk8303.ts +96 -0
- package/src/lint/rules/hardcoded-namespace.ts +56 -0
- package/src/lint/rules/rules.test.ts +69 -0
- package/src/lsp/completions.test.ts +64 -0
- package/src/lsp/completions.ts +20 -0
- package/src/lsp/hover.test.ts +69 -0
- package/src/lsp/hover.ts +68 -0
- package/src/package-cli.ts +28 -0
- package/src/plugin.test.ts +209 -0
- package/src/plugin.ts +915 -0
- package/src/serializer.test.ts +275 -0
- package/src/serializer.ts +278 -0
- package/src/spec/fetch.test.ts +24 -0
- package/src/spec/fetch.ts +68 -0
- package/src/spec/parse.test.ts +102 -0
- package/src/spec/parse.ts +477 -0
- package/src/testdata/manifests/configmap.yaml +7 -0
- package/src/testdata/manifests/deployment.yaml +22 -0
- package/src/testdata/manifests/full-app.yaml +61 -0
- package/src/testdata/manifests/secret.yaml +7 -0
- package/src/testdata/manifests/service.yaml +15 -0
- package/src/validate-cli.ts +21 -0
- package/src/validate.test.ts +29 -0
- package/src/validate.ts +46 -0
- package/src/variables.ts +36 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* WK8001: Hardcoded Namespace
|
|
6
|
+
*
|
|
7
|
+
* Detects hardcoded namespace strings in Kubernetes resource constructors.
|
|
8
|
+
* Namespaces should be parameterized or derived from configuration, not
|
|
9
|
+
* hardcoded as string literals.
|
|
10
|
+
*
|
|
11
|
+
* Bad: new Deployment({ metadata: { namespace: "production" } })
|
|
12
|
+
* Good: new Deployment({ metadata: { namespace: config.namespace } })
|
|
13
|
+
*/
|
|
14
|
+
export const hardcodedNamespaceRule: LintRule = {
|
|
15
|
+
id: "WK8001",
|
|
16
|
+
severity: "warning",
|
|
17
|
+
category: "correctness",
|
|
18
|
+
description:
|
|
19
|
+
"Detects hardcoded namespace strings — namespaces should be parameterized",
|
|
20
|
+
|
|
21
|
+
check(context: LintContext): LintDiagnostic[] {
|
|
22
|
+
const { sourceFile } = context;
|
|
23
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
24
|
+
|
|
25
|
+
function visit(node: ts.Node): void {
|
|
26
|
+
// Look for property assignments like `namespace: "production"`
|
|
27
|
+
if (
|
|
28
|
+
ts.isPropertyAssignment(node) &&
|
|
29
|
+
ts.isIdentifier(node.name) &&
|
|
30
|
+
node.name.text === "namespace" &&
|
|
31
|
+
ts.isStringLiteral(node.initializer)
|
|
32
|
+
) {
|
|
33
|
+
const value = node.initializer.text;
|
|
34
|
+
// Skip empty strings — those are likely intentional placeholders
|
|
35
|
+
if (value !== "") {
|
|
36
|
+
const { line, character } =
|
|
37
|
+
sourceFile.getLineAndCharacterOfPosition(
|
|
38
|
+
node.initializer.getStart(),
|
|
39
|
+
);
|
|
40
|
+
diagnostics.push({
|
|
41
|
+
file: sourceFile.fileName,
|
|
42
|
+
line: line + 1,
|
|
43
|
+
column: character + 1,
|
|
44
|
+
ruleId: "WK8001",
|
|
45
|
+
severity: "warning",
|
|
46
|
+
message: `Hardcoded namespace "${value}" detected. Use a variable or configuration parameter instead.`,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
ts.forEachChild(node, visit);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
visit(sourceFile);
|
|
54
|
+
return diagnostics;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for Kubernetes post-synthesis lint rules.
|
|
3
|
+
*
|
|
4
|
+
* Provides YAML parsing for multi-document K8s manifests and container
|
|
5
|
+
* extraction logic that handles all common workload types.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { parseYAML } from "@intentius/chant/yaml";
|
|
9
|
+
export { getPrimaryOutput } from "@intentius/chant/lint/post-synth";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A parsed Kubernetes manifest (loosely typed).
|
|
13
|
+
*/
|
|
14
|
+
export interface K8sManifest {
|
|
15
|
+
apiVersion?: string;
|
|
16
|
+
kind?: string;
|
|
17
|
+
metadata?: {
|
|
18
|
+
name?: string;
|
|
19
|
+
namespace?: string;
|
|
20
|
+
labels?: Record<string, string>;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
};
|
|
23
|
+
spec?: Record<string, unknown>;
|
|
24
|
+
data?: Record<string, unknown>;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A Kubernetes container spec (loosely typed).
|
|
30
|
+
*/
|
|
31
|
+
export interface K8sContainer {
|
|
32
|
+
name?: string;
|
|
33
|
+
image?: string;
|
|
34
|
+
env?: Array<{ name?: string; value?: unknown; valueFrom?: unknown }>;
|
|
35
|
+
ports?: Array<{ name?: string; containerPort?: number; [key: string]: unknown }>;
|
|
36
|
+
resources?: {
|
|
37
|
+
limits?: Record<string, unknown>;
|
|
38
|
+
requests?: Record<string, unknown>;
|
|
39
|
+
};
|
|
40
|
+
securityContext?: Record<string, unknown>;
|
|
41
|
+
livenessProbe?: unknown;
|
|
42
|
+
readinessProbe?: unknown;
|
|
43
|
+
imagePullPolicy?: string;
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Split a multi-document YAML string on `---` boundaries and parse each
|
|
49
|
+
* document into a K8sManifest.
|
|
50
|
+
*/
|
|
51
|
+
export function parseK8sManifests(yaml: string): K8sManifest[] {
|
|
52
|
+
const documents = yaml.split(/\n---\n/);
|
|
53
|
+
const manifests: K8sManifest[] = [];
|
|
54
|
+
|
|
55
|
+
for (const doc of documents) {
|
|
56
|
+
const trimmed = doc.trim();
|
|
57
|
+
if (trimmed === "" || trimmed === "---") continue;
|
|
58
|
+
try {
|
|
59
|
+
const parsed = parseYAML(trimmed);
|
|
60
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
61
|
+
manifests.push(parsed as K8sManifest);
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
// Skip unparseable documents
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return manifests;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Extract the pod spec from any workload manifest.
|
|
73
|
+
*
|
|
74
|
+
* Handles:
|
|
75
|
+
* - Pod: spec directly
|
|
76
|
+
* - Deployment, StatefulSet, DaemonSet: spec.template.spec
|
|
77
|
+
* - Job: spec.template.spec
|
|
78
|
+
* - CronJob: spec.jobTemplate.spec.template.spec
|
|
79
|
+
*/
|
|
80
|
+
export function extractPodSpec(
|
|
81
|
+
manifest: K8sManifest,
|
|
82
|
+
): Record<string, unknown> | null {
|
|
83
|
+
const kind = manifest.kind;
|
|
84
|
+
const spec = manifest.spec;
|
|
85
|
+
if (!spec) return null;
|
|
86
|
+
|
|
87
|
+
switch (kind) {
|
|
88
|
+
case "Pod":
|
|
89
|
+
return spec as Record<string, unknown>;
|
|
90
|
+
|
|
91
|
+
case "Deployment":
|
|
92
|
+
case "StatefulSet":
|
|
93
|
+
case "DaemonSet":
|
|
94
|
+
case "Job": {
|
|
95
|
+
const template = spec.template as Record<string, unknown> | undefined;
|
|
96
|
+
return (template?.spec as Record<string, unknown>) ?? null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
case "CronJob": {
|
|
100
|
+
const jobTemplate = spec.jobTemplate as Record<string, unknown> | undefined;
|
|
101
|
+
const jobSpec = jobTemplate?.spec as Record<string, unknown> | undefined;
|
|
102
|
+
const template = jobSpec?.template as Record<string, unknown> | undefined;
|
|
103
|
+
return (template?.spec as Record<string, unknown>) ?? null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
default:
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Extract all containers (including init containers) from a workload manifest.
|
|
113
|
+
*/
|
|
114
|
+
export function extractContainers(manifest: K8sManifest): K8sContainer[] {
|
|
115
|
+
const podSpec = extractPodSpec(manifest);
|
|
116
|
+
if (!podSpec) return [];
|
|
117
|
+
|
|
118
|
+
const containers: K8sContainer[] = [];
|
|
119
|
+
|
|
120
|
+
if (Array.isArray(podSpec.containers)) {
|
|
121
|
+
for (const c of podSpec.containers) {
|
|
122
|
+
if (typeof c === "object" && c !== null) {
|
|
123
|
+
containers.push(c as K8sContainer);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (Array.isArray(podSpec.initContainers)) {
|
|
129
|
+
for (const c of podSpec.initContainers) {
|
|
130
|
+
if (typeof c === "object" && c !== null) {
|
|
131
|
+
containers.push(c as K8sContainer);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return containers;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Workload kinds that contain pod templates.
|
|
141
|
+
*/
|
|
142
|
+
export const WORKLOAD_KINDS = new Set([
|
|
143
|
+
"Pod",
|
|
144
|
+
"Deployment",
|
|
145
|
+
"StatefulSet",
|
|
146
|
+
"DaemonSet",
|
|
147
|
+
"Job",
|
|
148
|
+
"CronJob",
|
|
149
|
+
]);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WK8005: Hardcoded Secrets in Environment Variables
|
|
3
|
+
*
|
|
4
|
+
* Detects container environment variables with names suggesting sensitive
|
|
5
|
+
* values (password, token, key, secret) that use hardcoded string values
|
|
6
|
+
* instead of secretKeyRef or configMapKeyRef.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
10
|
+
import { getPrimaryOutput, parseK8sManifests, extractContainers, WORKLOAD_KINDS } from "./k8s-helpers";
|
|
11
|
+
|
|
12
|
+
const SENSITIVE_NAME_PATTERN = /password|token|key|secret/i;
|
|
13
|
+
|
|
14
|
+
export const wk8005: PostSynthCheck = {
|
|
15
|
+
id: "WK8005",
|
|
16
|
+
description: "Hardcoded secrets in env vars — sensitive environment variables should use secretKeyRef",
|
|
17
|
+
|
|
18
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
19
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
20
|
+
|
|
21
|
+
for (const [, output] of ctx.outputs) {
|
|
22
|
+
const yaml = getPrimaryOutput(output);
|
|
23
|
+
const manifests = parseK8sManifests(yaml);
|
|
24
|
+
|
|
25
|
+
for (const manifest of manifests) {
|
|
26
|
+
if (!manifest.kind || !WORKLOAD_KINDS.has(manifest.kind)) continue;
|
|
27
|
+
|
|
28
|
+
const containers = extractContainers(manifest);
|
|
29
|
+
const resourceName = manifest.metadata?.name ?? manifest.kind;
|
|
30
|
+
|
|
31
|
+
for (const container of containers) {
|
|
32
|
+
if (!Array.isArray(container.env)) continue;
|
|
33
|
+
|
|
34
|
+
for (const envVar of container.env) {
|
|
35
|
+
if (
|
|
36
|
+
typeof envVar.name === "string" &&
|
|
37
|
+
SENSITIVE_NAME_PATTERN.test(envVar.name) &&
|
|
38
|
+
envVar.value !== undefined &&
|
|
39
|
+
envVar.value !== null &&
|
|
40
|
+
typeof envVar.value === "string" &&
|
|
41
|
+
envVar.value !== "" &&
|
|
42
|
+
!envVar.valueFrom
|
|
43
|
+
) {
|
|
44
|
+
diagnostics.push({
|
|
45
|
+
checkId: "WK8005",
|
|
46
|
+
severity: "error",
|
|
47
|
+
message: `Container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" has hardcoded value for sensitive env var "${envVar.name}" — use secretKeyRef instead`,
|
|
48
|
+
entity: resourceName,
|
|
49
|
+
lexicon: "k8s",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return diagnostics;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WK8006: No :latest or Untagged Images
|
|
3
|
+
*
|
|
4
|
+
* Container images should use explicit version tags for reproducibility.
|
|
5
|
+
* Flags images using the `:latest` tag or no tag at all.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
9
|
+
import { getPrimaryOutput, parseK8sManifests, extractContainers, WORKLOAD_KINDS } from "./k8s-helpers";
|
|
10
|
+
|
|
11
|
+
export const wk8006: PostSynthCheck = {
|
|
12
|
+
id: "WK8006",
|
|
13
|
+
description: "No :latest or untagged images — container images should use explicit version tags",
|
|
14
|
+
|
|
15
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
16
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
17
|
+
|
|
18
|
+
for (const [, output] of ctx.outputs) {
|
|
19
|
+
const yaml = getPrimaryOutput(output);
|
|
20
|
+
const manifests = parseK8sManifests(yaml);
|
|
21
|
+
|
|
22
|
+
for (const manifest of manifests) {
|
|
23
|
+
if (!manifest.kind || !WORKLOAD_KINDS.has(manifest.kind)) continue;
|
|
24
|
+
|
|
25
|
+
const containers = extractContainers(manifest);
|
|
26
|
+
const resourceName = manifest.metadata?.name ?? manifest.kind;
|
|
27
|
+
|
|
28
|
+
for (const container of containers) {
|
|
29
|
+
const image = container.image;
|
|
30
|
+
if (typeof image !== "string" || image === "") continue;
|
|
31
|
+
|
|
32
|
+
// Split off digest (@sha256:...) — those are pinned and fine
|
|
33
|
+
if (image.includes("@")) continue;
|
|
34
|
+
|
|
35
|
+
// Extract the tag portion (after the last colon that isn't part of a port)
|
|
36
|
+
// Images can be: name, name:tag, registry:port/name, registry:port/name:tag
|
|
37
|
+
const slashIndex = image.lastIndexOf("/");
|
|
38
|
+
const afterSlash = slashIndex >= 0 ? image.slice(slashIndex + 1) : image;
|
|
39
|
+
const colonIndex = afterSlash.lastIndexOf(":");
|
|
40
|
+
|
|
41
|
+
if (colonIndex === -1) {
|
|
42
|
+
// No tag at all
|
|
43
|
+
diagnostics.push({
|
|
44
|
+
checkId: "WK8006",
|
|
45
|
+
severity: "warning",
|
|
46
|
+
message: `Container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" uses untagged image "${image}" — specify an explicit version tag`,
|
|
47
|
+
entity: resourceName,
|
|
48
|
+
lexicon: "k8s",
|
|
49
|
+
});
|
|
50
|
+
} else {
|
|
51
|
+
const tag = afterSlash.slice(colonIndex + 1);
|
|
52
|
+
if (tag === "latest") {
|
|
53
|
+
diagnostics.push({
|
|
54
|
+
checkId: "WK8006",
|
|
55
|
+
severity: "warning",
|
|
56
|
+
message: `Container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" uses :latest tag on image "${image}" — use a specific version tag`,
|
|
57
|
+
entity: resourceName,
|
|
58
|
+
lexicon: "k8s",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return diagnostics;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WK8041: Hardcoded API Keys
|
|
3
|
+
*
|
|
4
|
+
* Detects well-known API key patterns in container environment variable values.
|
|
5
|
+
* Catches Stripe keys, GitHub PATs, AWS access keys, Google API keys, and
|
|
6
|
+
* other common credential formats.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
10
|
+
import { getPrimaryOutput, parseK8sManifests, extractContainers, WORKLOAD_KINDS } from "./k8s-helpers";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Known API key patterns and their descriptions.
|
|
14
|
+
*/
|
|
15
|
+
const API_KEY_PATTERNS: Array<{ pattern: RegExp; label: string }> = [
|
|
16
|
+
{ pattern: /^sk_live_/, label: "Stripe secret key" },
|
|
17
|
+
{ pattern: /^pk_live_/, label: "Stripe publishable key" },
|
|
18
|
+
{ pattern: /^sk_test_/, label: "Stripe test secret key" },
|
|
19
|
+
{ pattern: /^pk_test_/, label: "Stripe test publishable key" },
|
|
20
|
+
{ pattern: /^ghp_[A-Za-z0-9_]{36,}/, label: "GitHub personal access token" },
|
|
21
|
+
{ pattern: /^gho_[A-Za-z0-9_]{36,}/, label: "GitHub OAuth token" },
|
|
22
|
+
{ pattern: /^ghs_[A-Za-z0-9_]{36,}/, label: "GitHub app installation token" },
|
|
23
|
+
{ pattern: /^ghu_[A-Za-z0-9_]{36,}/, label: "GitHub user-to-server token" },
|
|
24
|
+
{ pattern: /^ghr_[A-Za-z0-9_]{36,}/, label: "GitHub refresh token" },
|
|
25
|
+
{ pattern: /^AKIA[A-Z0-9]{16}$/, label: "AWS access key ID" },
|
|
26
|
+
{ pattern: /^AIza[A-Za-z0-9_-]{35}$/, label: "Google API key" },
|
|
27
|
+
{ pattern: /^xox[bprs]-[A-Za-z0-9-]+/, label: "Slack token" },
|
|
28
|
+
{ pattern: /^SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}$/, label: "SendGrid API key" },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export const wk8041: PostSynthCheck = {
|
|
32
|
+
id: "WK8041",
|
|
33
|
+
description: "Hardcoded API keys — detects well-known API key patterns in env var values",
|
|
34
|
+
|
|
35
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
36
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
37
|
+
|
|
38
|
+
for (const [, output] of ctx.outputs) {
|
|
39
|
+
const yaml = getPrimaryOutput(output);
|
|
40
|
+
const manifests = parseK8sManifests(yaml);
|
|
41
|
+
|
|
42
|
+
for (const manifest of manifests) {
|
|
43
|
+
if (!manifest.kind || !WORKLOAD_KINDS.has(manifest.kind)) continue;
|
|
44
|
+
|
|
45
|
+
const containers = extractContainers(manifest);
|
|
46
|
+
const resourceName = manifest.metadata?.name ?? manifest.kind;
|
|
47
|
+
|
|
48
|
+
for (const container of containers) {
|
|
49
|
+
if (!Array.isArray(container.env)) continue;
|
|
50
|
+
|
|
51
|
+
for (const envVar of container.env) {
|
|
52
|
+
if (typeof envVar.value !== "string" || envVar.value === "") continue;
|
|
53
|
+
|
|
54
|
+
for (const { pattern, label } of API_KEY_PATTERNS) {
|
|
55
|
+
if (pattern.test(envVar.value)) {
|
|
56
|
+
diagnostics.push({
|
|
57
|
+
checkId: "WK8041",
|
|
58
|
+
severity: "error",
|
|
59
|
+
message: `Container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" has a ${label} hardcoded in env var "${envVar.name ?? "(unnamed)"}" — use a Secret reference instead`,
|
|
60
|
+
entity: resourceName,
|
|
61
|
+
lexicon: "k8s",
|
|
62
|
+
});
|
|
63
|
+
break; // One match per env var is sufficient
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return diagnostics;
|
|
72
|
+
},
|
|
73
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WK8042: Private Keys in ConfigMap
|
|
3
|
+
*
|
|
4
|
+
* Detects PEM-encoded private keys stored in ConfigMap data values.
|
|
5
|
+
* Private keys should be stored in Secrets, not ConfigMaps, since
|
|
6
|
+
* ConfigMaps are not encrypted at rest by default.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
10
|
+
import { getPrimaryOutput, parseK8sManifests } from "./k8s-helpers";
|
|
11
|
+
|
|
12
|
+
const PRIVATE_KEY_PATTERN = /-----BEGIN\s+[\w\s]*PRIVATE KEY-----/;
|
|
13
|
+
|
|
14
|
+
export const wk8042: PostSynthCheck = {
|
|
15
|
+
id: "WK8042",
|
|
16
|
+
description: "Private keys in ConfigMap — private keys should be stored in Secrets, not ConfigMaps",
|
|
17
|
+
|
|
18
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
19
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
20
|
+
|
|
21
|
+
for (const [, output] of ctx.outputs) {
|
|
22
|
+
const yaml = getPrimaryOutput(output);
|
|
23
|
+
const manifests = parseK8sManifests(yaml);
|
|
24
|
+
|
|
25
|
+
for (const manifest of manifests) {
|
|
26
|
+
if (manifest.kind !== "ConfigMap") continue;
|
|
27
|
+
|
|
28
|
+
const resourceName = manifest.metadata?.name ?? "ConfigMap";
|
|
29
|
+
const data = manifest.data;
|
|
30
|
+
if (typeof data !== "object" || data === null) continue;
|
|
31
|
+
|
|
32
|
+
for (const [key, value] of Object.entries(data)) {
|
|
33
|
+
if (typeof value === "string" && PRIVATE_KEY_PATTERN.test(value)) {
|
|
34
|
+
diagnostics.push({
|
|
35
|
+
checkId: "WK8042",
|
|
36
|
+
severity: "error",
|
|
37
|
+
message: `ConfigMap "${resourceName}" contains a private key in data field "${key}" — use a Secret resource instead`,
|
|
38
|
+
entity: resourceName,
|
|
39
|
+
lexicon: "k8s",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return diagnostics;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WK8101: Deployment Selector Must Match Template Labels
|
|
3
|
+
*
|
|
4
|
+
* A Deployment's spec.selector.matchLabels must be a subset of
|
|
5
|
+
* spec.template.metadata.labels. If they don't match, the Deployment
|
|
6
|
+
* controller cannot find the Pods it creates, causing a runtime failure.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
10
|
+
import { getPrimaryOutput, parseK8sManifests } from "./k8s-helpers";
|
|
11
|
+
|
|
12
|
+
export const wk8101: PostSynthCheck = {
|
|
13
|
+
id: "WK8101",
|
|
14
|
+
description: "Deployment selector must match template labels — mismatched selectors cause runtime failures",
|
|
15
|
+
|
|
16
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
17
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
18
|
+
|
|
19
|
+
for (const [, output] of ctx.outputs) {
|
|
20
|
+
const yaml = getPrimaryOutput(output);
|
|
21
|
+
const manifests = parseK8sManifests(yaml);
|
|
22
|
+
|
|
23
|
+
for (const manifest of manifests) {
|
|
24
|
+
if (manifest.kind !== "Deployment") continue;
|
|
25
|
+
|
|
26
|
+
const resourceName = manifest.metadata?.name ?? "Deployment";
|
|
27
|
+
const spec = manifest.spec;
|
|
28
|
+
if (!spec) continue;
|
|
29
|
+
|
|
30
|
+
const selector = spec.selector as Record<string, unknown> | undefined;
|
|
31
|
+
const matchLabels = selector?.matchLabels as Record<string, string> | undefined;
|
|
32
|
+
if (!matchLabels || typeof matchLabels !== "object") continue;
|
|
33
|
+
|
|
34
|
+
const template = spec.template as Record<string, unknown> | undefined;
|
|
35
|
+
const templateMetadata = template?.metadata as Record<string, unknown> | undefined;
|
|
36
|
+
const templateLabels = templateMetadata?.labels as Record<string, string> | undefined;
|
|
37
|
+
|
|
38
|
+
if (!templateLabels || typeof templateLabels !== "object") {
|
|
39
|
+
diagnostics.push({
|
|
40
|
+
checkId: "WK8101",
|
|
41
|
+
severity: "error",
|
|
42
|
+
message: `Deployment "${resourceName}" has spec.selector.matchLabels but no spec.template.metadata.labels`,
|
|
43
|
+
entity: resourceName,
|
|
44
|
+
lexicon: "k8s",
|
|
45
|
+
});
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const [key, value] of Object.entries(matchLabels)) {
|
|
50
|
+
if (templateLabels[key] !== value) {
|
|
51
|
+
diagnostics.push({
|
|
52
|
+
checkId: "WK8101",
|
|
53
|
+
severity: "error",
|
|
54
|
+
message: `Deployment "${resourceName}" selector label "${key}=${value}" does not match template label "${key}=${templateLabels[key] ?? '(missing)'}"`,
|
|
55
|
+
entity: resourceName,
|
|
56
|
+
lexicon: "k8s",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return diagnostics;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WK8102: Resources Should Have Metadata Labels
|
|
3
|
+
*
|
|
4
|
+
* All Kubernetes resources should have metadata.labels for organizational
|
|
5
|
+
* purposes. Labels enable filtering, selection, and operational tooling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
9
|
+
import { getPrimaryOutput, parseK8sManifests } from "./k8s-helpers";
|
|
10
|
+
|
|
11
|
+
export const wk8102: PostSynthCheck = {
|
|
12
|
+
id: "WK8102",
|
|
13
|
+
description: "Resources should have metadata labels — labels enable filtering and operational tooling",
|
|
14
|
+
|
|
15
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
16
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
17
|
+
|
|
18
|
+
for (const [, output] of ctx.outputs) {
|
|
19
|
+
const yaml = getPrimaryOutput(output);
|
|
20
|
+
const manifests = parseK8sManifests(yaml);
|
|
21
|
+
|
|
22
|
+
for (const manifest of manifests) {
|
|
23
|
+
if (!manifest.kind || !manifest.metadata) continue;
|
|
24
|
+
|
|
25
|
+
const resourceName = manifest.metadata.name ?? manifest.kind;
|
|
26
|
+
const labels = manifest.metadata.labels;
|
|
27
|
+
|
|
28
|
+
if (!labels || typeof labels !== "object" || Object.keys(labels).length === 0) {
|
|
29
|
+
diagnostics.push({
|
|
30
|
+
checkId: "WK8102",
|
|
31
|
+
severity: "warning",
|
|
32
|
+
message: `${manifest.kind} "${resourceName}" has no metadata.labels — add labels for organizational purposes`,
|
|
33
|
+
entity: resourceName,
|
|
34
|
+
lexicon: "k8s",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return diagnostics;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WK8103: Containers Must Have Name
|
|
3
|
+
*
|
|
4
|
+
* Every container in a pod spec must have a `name` field. This is required
|
|
5
|
+
* by the Kubernetes API and will be rejected at apply time if missing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
9
|
+
import { getPrimaryOutput, parseK8sManifests, extractContainers, WORKLOAD_KINDS } from "./k8s-helpers";
|
|
10
|
+
|
|
11
|
+
export const wk8103: PostSynthCheck = {
|
|
12
|
+
id: "WK8103",
|
|
13
|
+
description: "Containers must have name — the name field is required by the Kubernetes API",
|
|
14
|
+
|
|
15
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
16
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
17
|
+
|
|
18
|
+
for (const [, output] of ctx.outputs) {
|
|
19
|
+
const yaml = getPrimaryOutput(output);
|
|
20
|
+
const manifests = parseK8sManifests(yaml);
|
|
21
|
+
|
|
22
|
+
for (const manifest of manifests) {
|
|
23
|
+
if (!manifest.kind || !WORKLOAD_KINDS.has(manifest.kind)) continue;
|
|
24
|
+
|
|
25
|
+
const containers = extractContainers(manifest);
|
|
26
|
+
const resourceName = manifest.metadata?.name ?? manifest.kind;
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < containers.length; i++) {
|
|
29
|
+
const container = containers[i];
|
|
30
|
+
if (!container.name || typeof container.name !== "string" || container.name.trim() === "") {
|
|
31
|
+
diagnostics.push({
|
|
32
|
+
checkId: "WK8103",
|
|
33
|
+
severity: "error",
|
|
34
|
+
message: `${manifest.kind} "${resourceName}" has a container at index ${i} without a name — the name field is required`,
|
|
35
|
+
entity: resourceName,
|
|
36
|
+
lexicon: "k8s",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return diagnostics;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WK8104: Ports Should Be Named
|
|
3
|
+
*
|
|
4
|
+
* Container ports and Service ports should have names. Named ports enable
|
|
5
|
+
* referencing by name in Service targetPort and NetworkPolicy rules,
|
|
6
|
+
* improving maintainability.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
10
|
+
import { getPrimaryOutput, parseK8sManifests, extractContainers, WORKLOAD_KINDS } from "./k8s-helpers";
|
|
11
|
+
|
|
12
|
+
export const wk8104: PostSynthCheck = {
|
|
13
|
+
id: "WK8104",
|
|
14
|
+
description: "Ports should be named — named ports improve Service and NetworkPolicy configuration",
|
|
15
|
+
|
|
16
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
17
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
18
|
+
|
|
19
|
+
for (const [, output] of ctx.outputs) {
|
|
20
|
+
const yaml = getPrimaryOutput(output);
|
|
21
|
+
const manifests = parseK8sManifests(yaml);
|
|
22
|
+
|
|
23
|
+
for (const manifest of manifests) {
|
|
24
|
+
const resourceName = manifest.metadata?.name ?? manifest.kind ?? "(unknown)";
|
|
25
|
+
|
|
26
|
+
// Check container ports in workloads
|
|
27
|
+
if (manifest.kind && WORKLOAD_KINDS.has(manifest.kind)) {
|
|
28
|
+
const containers = extractContainers(manifest);
|
|
29
|
+
for (const container of containers) {
|
|
30
|
+
if (!Array.isArray(container.ports)) continue;
|
|
31
|
+
for (const port of container.ports) {
|
|
32
|
+
if (typeof port === "object" && port !== null && !port.name) {
|
|
33
|
+
diagnostics.push({
|
|
34
|
+
checkId: "WK8104",
|
|
35
|
+
severity: "warning",
|
|
36
|
+
message: `Container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" has an unnamed port (containerPort: ${port.containerPort ?? "?"}) — add a name for clarity`,
|
|
37
|
+
entity: resourceName,
|
|
38
|
+
lexicon: "k8s",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check Service ports
|
|
46
|
+
if (manifest.kind === "Service") {
|
|
47
|
+
const spec = manifest.spec;
|
|
48
|
+
if (!spec) continue;
|
|
49
|
+
const ports = spec.ports as Array<Record<string, unknown>> | undefined;
|
|
50
|
+
if (!Array.isArray(ports)) continue;
|
|
51
|
+
|
|
52
|
+
for (const port of ports) {
|
|
53
|
+
if (typeof port === "object" && port !== null && !port.name) {
|
|
54
|
+
diagnostics.push({
|
|
55
|
+
checkId: "WK8104",
|
|
56
|
+
severity: "warning",
|
|
57
|
+
message: `Service "${resourceName}" has an unnamed port (port: ${port.port ?? "?"}) — add a name for clarity`,
|
|
58
|
+
entity: resourceName,
|
|
59
|
+
lexicon: "k8s",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return diagnostics;
|
|
68
|
+
},
|
|
69
|
+
};
|