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 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
268pub 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 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
323pub 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
379fn 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
388fn 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}