syncable_cli/analyzer/helmlint/rules/
hl2xxx.rs1use crate::analyzer::helmlint::rules::{LintContext, Rule};
6use crate::analyzer::helmlint::types::{CheckFailure, RuleCategory, Severity};
7
8pub 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
20pub 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 let values = match ctx.values {
45 Some(v) => v,
46 None => return failures,
47 };
48
49 for ref_path in &ctx.template_value_refs {
51 let base_path = ref_path.split('.').next().unwrap_or(ref_path);
53 if !values.has_path(base_path) && !values.has_path(ref_path) {
54 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
85pub 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 for path in &values.defined_paths {
115 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 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
144pub 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 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
201pub 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 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 && let Some(value) = values.get(path)
244 && let Some(port) = extract_port_number(value)
245 && !(1..=65535).contains(&port)
246 {
247 let line = values.line_for_path(path).unwrap_or(1);
248 failures.push(CheckFailure::new(
249 "HL2005",
250 Severity::Error,
251 format!(
252 "Invalid port number {} at '{}'. Must be between 1 and 65535",
253 port, path
254 ),
255 "values.yaml",
256 line,
257 RuleCategory::Values,
258 ));
259 }
260 }
261
262 failures
263 }
264}
265
266pub struct HL2007;
268
269impl Rule for HL2007 {
270 fn code(&self) -> &'static str {
271 "HL2007"
272 }
273
274 fn severity(&self) -> Severity {
275 Severity::Warning
276 }
277
278 fn name(&self) -> &'static str {
279 "image-tag-latest"
280 }
281
282 fn description(&self) -> &'static str {
283 "Using 'latest' tag is prone to unexpected changes"
284 }
285
286 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
287 let mut failures = Vec::new();
288
289 let values = match ctx.values {
290 Some(v) => v,
291 None => return failures,
292 };
293
294 for path in &values.defined_paths {
296 let lower_path = path.to_lowercase();
297 if (lower_path.ends_with(".tag") || lower_path.ends_with("imagetag"))
298 && let Some(serde_yaml::Value::String(tag)) = values.get(path)
299 && tag == "latest"
300 {
301 let line = values.line_for_path(path).unwrap_or(1);
302 failures.push(CheckFailure::new(
303 "HL2007",
304 Severity::Warning,
305 format!(
306 "Image tag at '{}' is 'latest'. Pin to a specific version for reproducibility",
307 path
308 ),
309 "values.yaml",
310 line,
311 RuleCategory::Values,
312 ));
313 }
314 }
315
316 failures
317 }
318}
319
320pub struct HL2008;
322
323impl Rule for HL2008 {
324 fn code(&self) -> &'static str {
325 "HL2008"
326 }
327
328 fn severity(&self) -> Severity {
329 Severity::Warning
330 }
331
332 fn name(&self) -> &'static str {
333 "zero-replicas"
334 }
335
336 fn description(&self) -> &'static str {
337 "Replica count is zero which means no pods will be created"
338 }
339
340 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
341 let mut failures = Vec::new();
342
343 let values = match ctx.values {
344 Some(v) => v,
345 None => return failures,
346 };
347
348 for path in &values.defined_paths {
349 let lower_path = path.to_lowercase();
350 if (lower_path.ends_with("replicacount") || lower_path.ends_with("replicas"))
351 && let Some(value) = values.get(path)
352 && let Some(count) = extract_number(value)
353 && count == 0
354 {
355 let line = values.line_for_path(path).unwrap_or(1);
356 failures.push(CheckFailure::new(
357 "HL2008",
358 Severity::Warning,
359 format!(
360 "Replica count at '{}' is 0. No pods will be created by default",
361 path
362 ),
363 "values.yaml",
364 line,
365 RuleCategory::Values,
366 ));
367 }
368 }
369
370 failures
371 }
372}
373
374fn extract_port_number(value: &serde_yaml::Value) -> Option<i64> {
376 match value {
377 serde_yaml::Value::Number(n) => n.as_i64(),
378 serde_yaml::Value::String(s) => s.parse().ok(),
379 _ => None,
380 }
381}
382
383fn extract_number(value: &serde_yaml::Value) -> Option<i64> {
385 match value {
386 serde_yaml::Value::Number(n) => n.as_i64(),
387 serde_yaml::Value::String(s) => s.parse().ok(),
388 _ => None,
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395
396 #[test]
397 fn test_extract_port_number() {
398 assert_eq!(
399 extract_port_number(&serde_yaml::Value::Number(80.into())),
400 Some(80)
401 );
402 assert_eq!(
403 extract_port_number(&serde_yaml::Value::String("8080".to_string())),
404 Some(8080)
405 );
406 assert_eq!(extract_port_number(&serde_yaml::Value::Bool(true)), None);
407 }
408}