syncable_cli/analyzer/helmlint/rules/
hl2xxx.rs

1//! HL2xxx - Values Validation Rules
2//!
3//! Rules for validating values.yaml configuration.
4
5use crate::analyzer::helmlint::rules::{LintContext, Rule};
6use crate::analyzer::helmlint::types::{CheckFailure, RuleCategory, Severity};
7
8/// Get all HL2xxx rules.
9pub fn rules() -> Vec<Box<dyn Rule>> {
10    vec![
11        Box::new(HL2002),
12        Box::new(HL2003),
13        Box::new(HL2004),
14        Box::new(HL2005),
15        Box::new(HL2007),
16        Box::new(HL2008),
17    ]
18}
19
20/// HL2002: Value referenced in template but not defined
21pub struct HL2002;
22
23impl Rule for HL2002 {
24    fn code(&self) -> &'static str {
25        "HL2002"
26    }
27
28    fn severity(&self) -> Severity {
29        Severity::Warning
30    }
31
32    fn name(&self) -> &'static str {
33        "undefined-value"
34    }
35
36    fn description(&self) -> &'static str {
37        "Value is referenced in template but not defined in values.yaml"
38    }
39
40    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
41        let mut failures = Vec::new();
42
43        // Skip if no values file
44        let values = match ctx.values {
45            Some(v) => v,
46            None => return failures,
47        };
48
49        // Check each template reference
50        for ref_path in &ctx.template_value_refs {
51            // Check if base path exists (allow nested access to undefined)
52            let base_path = ref_path.split('.').next().unwrap_or(ref_path);
53            if !values.has_path(base_path) && !values.has_path(ref_path) {
54                // Check if any parent path exists
55                let mut found_parent = false;
56                let parts: Vec<&str> = ref_path.split('.').collect();
57                for i in 1..parts.len() {
58                    let partial = parts[..i].join(".");
59                    if values.has_path(&partial) {
60                        found_parent = true;
61                        break;
62                    }
63                }
64
65                if !found_parent {
66                    failures.push(CheckFailure::new(
67                        "HL2002",
68                        Severity::Warning,
69                        format!(
70                            "Value '.Values.{}' is referenced but not defined in values.yaml",
71                            ref_path
72                        ),
73                        "values.yaml",
74                        1,
75                        RuleCategory::Values,
76                    ));
77                }
78            }
79        }
80
81        failures
82    }
83}
84
85/// HL2003: Value defined but never used
86pub struct HL2003;
87
88impl Rule for HL2003 {
89    fn code(&self) -> &'static str {
90        "HL2003"
91    }
92
93    fn severity(&self) -> Severity {
94        Severity::Info
95    }
96
97    fn name(&self) -> &'static str {
98        "unused-value"
99    }
100
101    fn description(&self) -> &'static str {
102        "Value is defined in values.yaml but never used in templates"
103    }
104
105    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
106        let mut failures = Vec::new();
107
108        let values = match ctx.values {
109            Some(v) => v,
110            None => return failures,
111        };
112
113        // Check each defined value
114        for path in &values.defined_paths {
115            // Skip if any template references this path or a child path
116            let is_used = ctx
117                .template_value_refs
118                .iter()
119                .any(|ref_path| ref_path == path || ref_path.starts_with(&format!("{}.", path)));
120
121            // Also skip if a parent path is referenced (e.g., toYaml .Values.config)
122            let parent_is_used = ctx
123                .template_value_refs
124                .iter()
125                .any(|ref_path| path.starts_with(&format!("{}.", ref_path)));
126
127            if !is_used && !parent_is_used {
128                let line = values.line_for_path(path).unwrap_or(1);
129                failures.push(CheckFailure::new(
130                    "HL2003",
131                    Severity::Info,
132                    format!("Value '{}' is defined but never used in templates", path),
133                    "values.yaml",
134                    line,
135                    RuleCategory::Values,
136                ));
137            }
138        }
139
140        failures
141    }
142}
143
144/// HL2004: Sensitive value not marked as secret
145pub struct HL2004;
146
147impl Rule for HL2004 {
148    fn code(&self) -> &'static str {
149        "HL2004"
150    }
151
152    fn severity(&self) -> Severity {
153        Severity::Warning
154    }
155
156    fn name(&self) -> &'static str {
157        "sensitive-value-exposed"
158    }
159
160    fn description(&self) -> &'static str {
161        "Sensitive value should be handled as a Kubernetes Secret"
162    }
163
164    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
165        let mut failures = Vec::new();
166
167        let values = match ctx.values {
168            Some(v) => v,
169            None => return failures,
170        };
171
172        for path in values.sensitive_paths() {
173            // Check if the value has a non-empty default
174            if let Some(value) = values.get(path) {
175                let has_hardcoded_value = match value {
176                    serde_yaml::Value::String(s) => !s.is_empty() && !s.starts_with("$"),
177                    _ => false,
178                };
179
180                if has_hardcoded_value {
181                    let line = values.line_for_path(path).unwrap_or(1);
182                    failures.push(CheckFailure::new(
183                        "HL2004",
184                        Severity::Warning,
185                        format!(
186                            "Sensitive value '{}' has a hardcoded default. Consider using a Secret reference",
187                            path
188                        ),
189                        "values.yaml",
190                        line,
191                        RuleCategory::Values,
192                    ));
193                }
194            }
195        }
196
197        failures
198    }
199}
200
201/// HL2005: Port number out of valid range
202pub struct HL2005;
203
204impl Rule for HL2005 {
205    fn code(&self) -> &'static str {
206        "HL2005"
207    }
208
209    fn severity(&self) -> Severity {
210        Severity::Error
211    }
212
213    fn name(&self) -> &'static str {
214        "invalid-port"
215    }
216
217    fn description(&self) -> &'static str {
218        "Port number must be between 1 and 65535"
219    }
220
221    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
222        let mut failures = Vec::new();
223
224        let values = match ctx.values {
225            Some(v) => v,
226            None => return failures,
227        };
228
229        // Look for common port patterns
230        let port_patterns = [
231            "port",
232            "containerPort",
233            "targetPort",
234            "hostPort",
235            "nodePort",
236        ];
237
238        for path in &values.defined_paths {
239            let lower_path = path.to_lowercase();
240            let is_port_field = port_patterns.iter().any(|p| lower_path.ends_with(p));
241
242            if is_port_field {
243                if let Some(value) = values.get(path) {
244                    if let Some(port) = extract_port_number(value) {
245                        if !(1..=65535).contains(&port) {
246                            let line = values.line_for_path(path).unwrap_or(1);
247                            failures.push(CheckFailure::new(
248                                "HL2005",
249                                Severity::Error,
250                                format!(
251                                    "Invalid port number {} at '{}'. Must be between 1 and 65535",
252                                    port, path
253                                ),
254                                "values.yaml",
255                                line,
256                                RuleCategory::Values,
257                            ));
258                        }
259                    }
260                }
261            }
262        }
263
264        failures
265    }
266}
267
268/// HL2007: Image tag is 'latest'
269pub struct HL2007;
270
271impl Rule for HL2007 {
272    fn code(&self) -> &'static str {
273        "HL2007"
274    }
275
276    fn severity(&self) -> Severity {
277        Severity::Warning
278    }
279
280    fn name(&self) -> &'static str {
281        "image-tag-latest"
282    }
283
284    fn description(&self) -> &'static str {
285        "Using 'latest' tag is prone to unexpected changes"
286    }
287
288    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
289        let mut failures = Vec::new();
290
291        let values = match ctx.values {
292            Some(v) => v,
293            None => return failures,
294        };
295
296        // Look for image.tag or similar patterns
297        for path in &values.defined_paths {
298            let lower_path = path.to_lowercase();
299            if lower_path.ends_with(".tag") || lower_path.ends_with("imagetag") {
300                if let Some(serde_yaml::Value::String(tag)) = values.get(path) {
301                    if tag == "latest" {
302                        let line = values.line_for_path(path).unwrap_or(1);
303                        failures.push(CheckFailure::new(
304                            "HL2007",
305                            Severity::Warning,
306                            format!(
307                                "Image tag at '{}' is 'latest'. Pin to a specific version for reproducibility",
308                                path
309                            ),
310                            "values.yaml",
311                            line,
312                            RuleCategory::Values,
313                        ));
314                    }
315                }
316            }
317        }
318
319        failures
320    }
321}
322
323/// HL2008: Replica count is zero
324pub struct HL2008;
325
326impl Rule for HL2008 {
327    fn code(&self) -> &'static str {
328        "HL2008"
329    }
330
331    fn severity(&self) -> Severity {
332        Severity::Warning
333    }
334
335    fn name(&self) -> &'static str {
336        "zero-replicas"
337    }
338
339    fn description(&self) -> &'static str {
340        "Replica count is zero which means no pods will be created"
341    }
342
343    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
344        let mut failures = Vec::new();
345
346        let values = match ctx.values {
347            Some(v) => v,
348            None => return failures,
349        };
350
351        for path in &values.defined_paths {
352            let lower_path = path.to_lowercase();
353            if lower_path.ends_with("replicacount") || lower_path.ends_with("replicas") {
354                if let Some(value) = values.get(path) {
355                    if let Some(count) = extract_number(value) {
356                        if count == 0 {
357                            let line = values.line_for_path(path).unwrap_or(1);
358                            failures.push(CheckFailure::new(
359                                "HL2008",
360                                Severity::Warning,
361                                format!(
362                                    "Replica count at '{}' is 0. No pods will be created by default",
363                                    path
364                                ),
365                                "values.yaml",
366                                line,
367                                RuleCategory::Values,
368                            ));
369                        }
370                    }
371                }
372            }
373        }
374
375        failures
376    }
377}
378
379/// Extract a port number from a YAML value.
380fn extract_port_number(value: &serde_yaml::Value) -> Option<i64> {
381    match value {
382        serde_yaml::Value::Number(n) => n.as_i64(),
383        serde_yaml::Value::String(s) => s.parse().ok(),
384        _ => None,
385    }
386}
387
388/// Extract a number from a YAML value.
389fn extract_number(value: &serde_yaml::Value) -> Option<i64> {
390    match value {
391        serde_yaml::Value::Number(n) => n.as_i64(),
392        serde_yaml::Value::String(s) => s.parse().ok(),
393        _ => None,
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400
401    #[test]
402    fn test_extract_port_number() {
403        assert_eq!(
404            extract_port_number(&serde_yaml::Value::Number(80.into())),
405            Some(80)
406        );
407        assert_eq!(
408            extract_port_number(&serde_yaml::Value::String("8080".to_string())),
409            Some(8080)
410        );
411        assert_eq!(extract_port_number(&serde_yaml::Value::Bool(true)), None);
412    }
413}