syncable_cli/analyzer/helmlint/rules/
hl4xxx.rs1use crate::analyzer::helmlint::parser::template::TemplateToken;
6use crate::analyzer::helmlint::rules::{LintContext, Rule};
7use crate::analyzer::helmlint::types::{CheckFailure, RuleCategory, Severity};
8
9pub fn rules() -> Vec<Box<dyn Rule>> {
11 vec![
12 Box::new(HL4001),
13 Box::new(HL4002),
14 Box::new(HL4003),
15 Box::new(HL4004),
16 Box::new(HL4005),
17 Box::new(HL4006),
18 Box::new(HL4011),
19 Box::new(HL4012),
20 ]
21}
22
23pub struct HL4001;
25
26impl Rule for HL4001 {
27 fn code(&self) -> &'static str {
28 "HL4001"
29 }
30
31 fn severity(&self) -> Severity {
32 Severity::Warning
33 }
34
35 fn name(&self) -> &'static str {
36 "container-runs-as-root"
37 }
38
39 fn description(&self) -> &'static str {
40 "Container may run as root user"
41 }
42
43 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
44 let mut failures = Vec::new();
45
46 if let Some(values) = ctx.values {
48 let has_run_as_non_root = values
50 .defined_paths
51 .iter()
52 .any(|p| p.to_lowercase().contains("runasnonroot"));
53
54 let has_run_as_user = values
55 .defined_paths
56 .iter()
57 .any(|p| p.to_lowercase().contains("runasuser"));
58
59 if !has_run_as_non_root && !has_run_as_user {
60 failures.push(CheckFailure::new(
61 "HL4001",
62 Severity::Warning,
63 "No runAsNonRoot or runAsUser setting found. Container may run as root",
64 "values.yaml",
65 1,
66 RuleCategory::Security,
67 ));
68 }
69 }
70
71 for template in ctx.templates {
73 let content = template
74 .tokens
75 .iter()
76 .filter_map(|t| match t {
77 TemplateToken::Text { content, .. } => Some(content.as_str()),
78 _ => None,
79 })
80 .collect::<Vec<_>>()
81 .join("");
82
83 if content.contains("runAsUser: 0") || content.contains("runAsUser:0") {
85 failures.push(CheckFailure::new(
86 "HL4001",
87 Severity::Warning,
88 "Container is configured to run as root (runAsUser: 0)",
89 &template.path,
90 1,
91 RuleCategory::Security,
92 ));
93 }
94 }
95
96 failures
97 }
98}
99
100pub struct HL4002;
102
103impl Rule for HL4002 {
104 fn code(&self) -> &'static str {
105 "HL4002"
106 }
107
108 fn severity(&self) -> Severity {
109 Severity::Error
110 }
111
112 fn name(&self) -> &'static str {
113 "privileged-container"
114 }
115
116 fn description(&self) -> &'static str {
117 "Container runs in privileged mode"
118 }
119
120 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
121 let mut failures = Vec::new();
122
123 if let Some(values) = ctx.values {
125 for path in &values.defined_paths {
126 if path.to_lowercase().contains("privileged")
127 && let Some(value) = values.get(path)
128 && is_truthy(value)
129 {
130 let line = values.line_for_path(path).unwrap_or(1);
131 failures.push(CheckFailure::new(
132 "HL4002",
133 Severity::Error,
134 format!("Privileged mode enabled at '{}'", path),
135 "values.yaml",
136 line,
137 RuleCategory::Security,
138 ));
139 }
140 }
141 }
142
143 for template in ctx.templates {
145 for token in &template.tokens {
146 if let TemplateToken::Text { content, line } = token
147 && content.contains("privileged: true")
148 {
149 failures.push(CheckFailure::new(
150 "HL4002",
151 Severity::Error,
152 "Container is configured with privileged: true",
153 &template.path,
154 *line,
155 RuleCategory::Security,
156 ));
157 }
158 }
159 }
160
161 failures
162 }
163}
164
165pub struct HL4003;
167
168impl Rule for HL4003 {
169 fn code(&self) -> &'static str {
170 "HL4003"
171 }
172
173 fn severity(&self) -> Severity {
174 Severity::Warning
175 }
176
177 fn name(&self) -> &'static str {
178 "hostpath-volume"
179 }
180
181 fn description(&self) -> &'static str {
182 "Using hostPath volumes can expose host filesystem"
183 }
184
185 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
186 let mut failures = Vec::new();
187
188 for template in ctx.templates {
189 for token in &template.tokens {
190 if let TemplateToken::Text { content, line } = token
191 && content.contains("hostPath:")
192 {
193 failures.push(CheckFailure::new(
194 "HL4003",
195 Severity::Warning,
196 "Using hostPath volume mount. This can expose the host filesystem to the container",
197 &template.path,
198 *line,
199 RuleCategory::Security,
200 ));
201 }
202 }
203 }
204
205 failures
206 }
207}
208
209pub struct HL4004;
211
212impl Rule for HL4004 {
213 fn code(&self) -> &'static str {
214 "HL4004"
215 }
216
217 fn severity(&self) -> Severity {
218 Severity::Warning
219 }
220
221 fn name(&self) -> &'static str {
222 "host-network"
223 }
224
225 fn description(&self) -> &'static str {
226 "Using host network can bypass network policies"
227 }
228
229 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
230 let mut failures = Vec::new();
231
232 if let Some(values) = ctx.values {
234 for path in &values.defined_paths {
235 if path.to_lowercase().contains("hostnetwork")
236 && let Some(value) = values.get(path)
237 && is_truthy(value)
238 {
239 let line = values.line_for_path(path).unwrap_or(1);
240 failures.push(CheckFailure::new(
241 "HL4004",
242 Severity::Warning,
243 format!("Host network enabled at '{}'", path),
244 "values.yaml",
245 line,
246 RuleCategory::Security,
247 ));
248 }
249 }
250 }
251
252 for template in ctx.templates {
254 for token in &template.tokens {
255 if let TemplateToken::Text { content, line } = token
256 && content.contains("hostNetwork: true")
257 {
258 failures.push(CheckFailure::new(
259 "HL4004",
260 Severity::Warning,
261 "Pod uses host network. This bypasses network policies",
262 &template.path,
263 *line,
264 RuleCategory::Security,
265 ));
266 }
267 }
268 }
269
270 failures
271 }
272}
273
274pub struct HL4005;
276
277impl Rule for HL4005 {
278 fn code(&self) -> &'static str {
279 "HL4005"
280 }
281
282 fn severity(&self) -> Severity {
283 Severity::Warning
284 }
285
286 fn name(&self) -> &'static str {
287 "host-pid"
288 }
289
290 fn description(&self) -> &'static str {
291 "Using host PID namespace can expose host processes"
292 }
293
294 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
295 let mut failures = Vec::new();
296
297 for template in ctx.templates {
298 for token in &template.tokens {
299 if let TemplateToken::Text { content, line } = token
300 && content.contains("hostPID: true")
301 {
302 failures.push(CheckFailure::new(
303 "HL4005",
304 Severity::Warning,
305 "Pod uses host PID namespace. This can expose host processes",
306 &template.path,
307 *line,
308 RuleCategory::Security,
309 ));
310 }
311 }
312 }
313
314 failures
315 }
316}
317
318pub struct HL4006;
320
321impl Rule for HL4006 {
322 fn code(&self) -> &'static str {
323 "HL4006"
324 }
325
326 fn severity(&self) -> Severity {
327 Severity::Info
328 }
329
330 fn name(&self) -> &'static str {
331 "missing-security-context"
332 }
333
334 fn description(&self) -> &'static str {
335 "Container or pod is missing securityContext"
336 }
337
338 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
339 let mut failures = Vec::new();
340
341 if let Some(values) = ctx.values {
343 let has_security_context = values
344 .defined_paths
345 .iter()
346 .any(|p| p.to_lowercase().contains("securitycontext"));
347
348 if !has_security_context {
349 failures.push(CheckFailure::new(
350 "HL4006",
351 Severity::Info,
352 "No securityContext configuration found in values.yaml",
353 "values.yaml",
354 1,
355 RuleCategory::Security,
356 ));
357 }
358 }
359
360 failures
361 }
362}
363
364pub struct HL4011;
366
367impl Rule for HL4011 {
368 fn code(&self) -> &'static str {
369 "HL4011"
370 }
371
372 fn severity(&self) -> Severity {
373 Severity::Warning
374 }
375
376 fn name(&self) -> &'static str {
377 "secret-in-env"
378 }
379
380 fn description(&self) -> &'static str {
381 "Sensitive value passed via environment variable instead of mounted secret"
382 }
383
384 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
385 let mut failures = Vec::new();
386
387 let sensitive_patterns = [
389 "PASSWORD",
390 "SECRET",
391 "TOKEN",
392 "API_KEY",
393 "APIKEY",
394 "PRIVATE_KEY",
395 "CREDENTIALS",
396 ];
397
398 for template in ctx.templates {
399 for token in &template.tokens {
400 if let TemplateToken::Text { content, line } = token {
401 for pattern in &sensitive_patterns {
403 let search = format!("name: {}", pattern);
404 let search_lower = format!("name: {}", pattern.to_lowercase());
405 if (content.contains(&search) || content.contains(&search_lower))
406 && content.contains("value:")
407 && !content.contains("valueFrom:")
408 && !content.contains("secretKeyRef:")
409 {
410 failures.push(CheckFailure::new(
411 "HL4011",
412 Severity::Warning,
413 format!(
414 "Environment variable matching '{}' should use secretKeyRef instead of direct value",
415 pattern
416 ),
417 &template.path,
418 *line,
419 RuleCategory::Security,
420 ));
421 }
422 }
423 }
424 }
425 }
426
427 failures
428 }
429}
430
431pub struct HL4012;
433
434impl Rule for HL4012 {
435 fn code(&self) -> &'static str {
436 "HL4012"
437 }
438
439 fn severity(&self) -> Severity {
440 Severity::Error
441 }
442
443 fn name(&self) -> &'static str {
444 "hardcoded-credentials"
445 }
446
447 fn description(&self) -> &'static str {
448 "Hardcoded credentials or secrets detected in templates"
449 }
450
451 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
452 let mut failures = Vec::new();
453
454 let credential_types = [
456 ("password:", "password"),
457 ("secret:", "secret"),
458 ("apikey:", "API key"),
459 ("token:", "token"),
460 ];
461
462 for template in ctx.templates {
463 for token in &template.tokens {
464 if let TemplateToken::Text { content, line } = token {
465 let lower_content = content.to_lowercase();
466
467 for (pattern, cred_type) in &credential_types {
468 if lower_content.contains(pattern) {
470 let has_template_var = content.contains("{{") && content.contains("}}");
472 let is_empty = content.contains("\"\"") || content.contains("''");
473
474 if !has_template_var && !is_empty {
475 let parts: Vec<&str> = content.split(':').collect();
477 if parts.len() >= 2 {
478 let value_part = parts[1].trim();
479 if !value_part.is_empty()
480 && !value_part.starts_with('{')
481 && !value_part.starts_with('$')
482 && value_part != "\"\""
483 && value_part != "''"
484 {
485 failures.push(CheckFailure::new(
486 "HL4012",
487 Severity::Error,
488 format!(
489 "Possible hardcoded {} detected. Use Secrets instead",
490 cred_type
491 ),
492 &template.path,
493 *line,
494 RuleCategory::Security,
495 ));
496 break;
497 }
498 }
499 }
500 }
501 }
502 }
503 }
504 }
505
506 failures
507 }
508}
509
510fn is_truthy(value: &serde_yaml::Value) -> bool {
512 match value {
513 serde_yaml::Value::Bool(b) => *b,
514 serde_yaml::Value::String(s) => {
515 let lower = s.to_lowercase();
516 lower == "true" || lower == "yes" || lower == "1"
517 }
518 serde_yaml::Value::Number(n) => n.as_i64().map(|i| i != 0).unwrap_or(false),
519 _ => false,
520 }
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526
527 #[test]
528 fn test_is_truthy() {
529 assert!(is_truthy(&serde_yaml::Value::Bool(true)));
530 assert!(!is_truthy(&serde_yaml::Value::Bool(false)));
531 assert!(is_truthy(&serde_yaml::Value::String("true".to_string())));
532 assert!(is_truthy(&serde_yaml::Value::String("yes".to_string())));
533 assert!(!is_truthy(&serde_yaml::Value::String("false".to_string())));
534 assert!(is_truthy(&serde_yaml::Value::Number(1.into())));
535 assert!(!is_truthy(&serde_yaml::Value::Number(0.into())));
536 }
537
538 #[test]
539 fn test_rules_exist() {
540 let all_rules = rules();
541 assert!(!all_rules.is_empty());
542 }
543}