syncable_cli/analyzer/kubelint/templates/
envvar.rs1use crate::analyzer::kubelint::context::Object;
4use crate::analyzer::kubelint::context::object::EnvVarSource;
5use crate::analyzer::kubelint::extract;
6use crate::analyzer::kubelint::templates::{CheckFunc, ParameterDesc, Template, TemplateError};
7use crate::analyzer::kubelint::types::{Diagnostic, ObjectKindsDesc};
8use regex::Regex;
9
10pub struct EnvVarSecretTemplate;
12
13impl Template for EnvVarSecretTemplate {
14 fn key(&self) -> &str {
15 "env-var-secret"
16 }
17
18 fn human_name(&self) -> &str {
19 "Environment Variable Secret"
20 }
21
22 fn description(&self) -> &str {
23 "Detects environment variables that may contain secrets"
24 }
25
26 fn supported_object_kinds(&self) -> ObjectKindsDesc {
27 ObjectKindsDesc::default()
28 }
29
30 fn parameters(&self) -> Vec<ParameterDesc> {
31 Vec::new()
32 }
33
34 fn instantiate(
35 &self,
36 _params: &serde_yaml::Value,
37 ) -> Result<Box<dyn CheckFunc>, TemplateError> {
38 Ok(Box::new(EnvVarSecretCheck))
39 }
40}
41
42struct EnvVarSecretCheck;
43
44impl CheckFunc for EnvVarSecretCheck {
45 fn check(&self, object: &Object) -> Vec<Diagnostic> {
46 let mut diagnostics = Vec::new();
47
48 let secret_name_pattern =
50 Regex::new(r"(?i)(password|secret|key|token|credential|api_key|apikey|auth)").unwrap();
51
52 if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) {
53 for container in extract::container::all_containers(pod_spec) {
54 for env_var in &container.env {
55 if secret_name_pattern.is_match(&env_var.name) {
57 if env_var.value.is_some() && env_var.value_from.is_none() {
59 diagnostics.push(Diagnostic {
60 message: format!(
61 "Container '{}' has environment variable '{}' that appears to \
62 contain a secret as a plain value",
63 container.name, env_var.name
64 ),
65 remediation: Some(
66 "Use a Kubernetes Secret with secretKeyRef instead of \
67 hardcoding sensitive values in environment variables."
68 .to_string(),
69 ),
70 });
71 }
72 }
73 }
74 }
75 }
76
77 diagnostics
78 }
79}
80
81pub struct ReadSecretFromEnvVarTemplate;
83
84impl Template for ReadSecretFromEnvVarTemplate {
85 fn key(&self) -> &str {
86 "read-secret-from-env-var"
87 }
88
89 fn human_name(&self) -> &str {
90 "Read Secret From Env Var"
91 }
92
93 fn description(&self) -> &str {
94 "Detects when secrets are exposed through environment variables"
95 }
96
97 fn supported_object_kinds(&self) -> ObjectKindsDesc {
98 ObjectKindsDesc::default()
99 }
100
101 fn parameters(&self) -> Vec<ParameterDesc> {
102 Vec::new()
103 }
104
105 fn instantiate(
106 &self,
107 _params: &serde_yaml::Value,
108 ) -> Result<Box<dyn CheckFunc>, TemplateError> {
109 Ok(Box::new(ReadSecretFromEnvVarCheck))
110 }
111}
112
113struct ReadSecretFromEnvVarCheck;
114
115impl CheckFunc for ReadSecretFromEnvVarCheck {
116 fn check(&self, object: &Object) -> Vec<Diagnostic> {
117 let mut diagnostics = Vec::new();
118
119 if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) {
120 for container in extract::container::all_containers(pod_spec) {
121 for env_var in &container.env {
122 if let Some(EnvVarSource::SecretKeyRef { .. }) = &env_var.value_from {
124 diagnostics.push(Diagnostic {
125 message: format!(
126 "Container '{}' reads secret into environment variable '{}'",
127 container.name, env_var.name
128 ),
129 remediation: Some(
130 "Consider mounting secrets as files instead of exposing \
131 them as environment variables. Environment variables can \
132 be logged or exposed through /proc."
133 .to_string(),
134 ),
135 });
136 }
137 }
138 }
139 }
140
141 diagnostics
142 }
143}
144
145pub struct DuplicateEnvVarTemplate;
147
148impl Template for DuplicateEnvVarTemplate {
149 fn key(&self) -> &str {
150 "duplicate-env-var"
151 }
152
153 fn human_name(&self) -> &str {
154 "Duplicate Environment Variable"
155 }
156
157 fn description(&self) -> &str {
158 "Detects duplicate environment variable definitions"
159 }
160
161 fn supported_object_kinds(&self) -> ObjectKindsDesc {
162 ObjectKindsDesc::default()
163 }
164
165 fn parameters(&self) -> Vec<ParameterDesc> {
166 Vec::new()
167 }
168
169 fn instantiate(
170 &self,
171 _params: &serde_yaml::Value,
172 ) -> Result<Box<dyn CheckFunc>, TemplateError> {
173 Ok(Box::new(DuplicateEnvVarCheck))
174 }
175}
176
177struct DuplicateEnvVarCheck;
178
179impl CheckFunc for DuplicateEnvVarCheck {
180 fn check(&self, object: &Object) -> Vec<Diagnostic> {
181 use std::collections::HashSet;
182 let mut diagnostics = Vec::new();
183
184 if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) {
185 for container in extract::container::all_containers(pod_spec) {
186 let mut seen: HashSet<&str> = HashSet::new();
187 for env_var in &container.env {
188 if !seen.insert(&env_var.name) {
189 diagnostics.push(Diagnostic {
190 message: format!(
191 "Container '{}' has duplicate environment variable '{}'",
192 container.name, env_var.name
193 ),
194 remediation: Some(
195 "Remove duplicate environment variable definitions. \
196 Only the last definition will be used."
197 .to_string(),
198 ),
199 });
200 }
201 }
202 }
203 }
204
205 diagnostics
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use crate::analyzer::kubelint::parser::yaml::parse_yaml;
213
214 #[test]
215 fn test_env_var_secret_detected() {
216 let yaml = r#"
217apiVersion: apps/v1
218kind: Deployment
219metadata:
220 name: secret-in-env
221spec:
222 template:
223 spec:
224 containers:
225 - name: app
226 image: myapp:1.0
227 env:
228 - name: DB_PASSWORD
229 value: "supersecret123"
230"#;
231 let objects = parse_yaml(yaml).unwrap();
232 let check = EnvVarSecretCheck;
233 let diagnostics = check.check(&objects[0]);
234 assert_eq!(diagnostics.len(), 1);
235 assert!(diagnostics[0].message.contains("DB_PASSWORD"));
236 }
237
238 #[test]
239 fn test_env_var_secret_ref_ok() {
240 let yaml = r#"
241apiVersion: apps/v1
242kind: Deployment
243metadata:
244 name: secret-ref
245spec:
246 template:
247 spec:
248 containers:
249 - name: app
250 image: myapp:1.0
251 env:
252 - name: DB_PASSWORD
253 valueFrom:
254 secretKeyRef:
255 name: db-secret
256 key: password
257"#;
258 let objects = parse_yaml(yaml).unwrap();
259 let check = EnvVarSecretCheck;
260 let diagnostics = check.check(&objects[0]);
261 assert!(diagnostics.is_empty());
262 }
263
264 #[test]
265 fn test_duplicate_env_var_detected() {
266 let yaml = r#"
267apiVersion: apps/v1
268kind: Deployment
269metadata:
270 name: dup-env
271spec:
272 template:
273 spec:
274 containers:
275 - name: app
276 image: myapp:1.0
277 env:
278 - name: FOO
279 value: "bar"
280 - name: FOO
281 value: "baz"
282"#;
283 let objects = parse_yaml(yaml).unwrap();
284 let check = DuplicateEnvVarCheck;
285 let diagnostics = check.check(&objects[0]);
286 assert_eq!(diagnostics.len(), 1);
287 assert!(diagnostics[0].message.contains("duplicate"));
288 }
289
290 #[test]
291 fn test_read_secret_from_env_var_detected() {
292 let yaml = r#"
293apiVersion: apps/v1
294kind: Deployment
295metadata:
296 name: secret-env
297spec:
298 template:
299 spec:
300 containers:
301 - name: app
302 image: myapp:1.0
303 env:
304 - name: DB_PASSWORD
305 valueFrom:
306 secretKeyRef:
307 name: db-secret
308 key: password
309"#;
310 let objects = parse_yaml(yaml).unwrap();
311 let check = ReadSecretFromEnvVarCheck;
312 let diagnostics = check.check(&objects[0]);
313 assert_eq!(diagnostics.len(), 1);
314 assert!(diagnostics[0].message.contains("reads secret"));
315 }
316}