syncable_cli/analyzer/kubelint/templates/
envvar.rs

1//! Environment variable check templates.
2
3use 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
10/// Template for detecting secrets in environment variable values.
11pub 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        // Patterns for secret-looking env var names
49        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                    // Check if the env var name suggests it contains a secret
56                    if secret_name_pattern.is_match(&env_var.name) {
57                        // Check if it has a hardcoded value (not from secret or configmap)
58                        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
81/// Template for detecting reading secrets directly from environment variables.
82pub 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                    // Check if the env var references a secret
123                    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
145/// Template for detecting duplicate environment variables.
146pub 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}