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 if let Some(value) = values.get(path) {
128 if is_truthy(value) {
129 let line = values.line_for_path(path).unwrap_or(1);
130 failures.push(CheckFailure::new(
131 "HL4002",
132 Severity::Error,
133 format!("Privileged mode enabled at '{}'", path),
134 "values.yaml",
135 line,
136 RuleCategory::Security,
137 ));
138 }
139 }
140 }
141 }
142 }
143
144 for template in ctx.templates {
146 for token in &template.tokens {
147 if let TemplateToken::Text { content, line } = token {
148 if content.contains("privileged: true") {
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
162 failures
163 }
164}
165
166pub struct HL4003;
168
169impl Rule for HL4003 {
170 fn code(&self) -> &'static str {
171 "HL4003"
172 }
173
174 fn severity(&self) -> Severity {
175 Severity::Warning
176 }
177
178 fn name(&self) -> &'static str {
179 "hostpath-volume"
180 }
181
182 fn description(&self) -> &'static str {
183 "Using hostPath volumes can expose host filesystem"
184 }
185
186 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
187 let mut failures = Vec::new();
188
189 for template in ctx.templates {
190 for token in &template.tokens {
191 if let TemplateToken::Text { content, line } = token {
192 if content.contains("hostPath:") {
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
206 failures
207 }
208}
209
210pub struct HL4004;
212
213impl Rule for HL4004 {
214 fn code(&self) -> &'static str {
215 "HL4004"
216 }
217
218 fn severity(&self) -> Severity {
219 Severity::Warning
220 }
221
222 fn name(&self) -> &'static str {
223 "host-network"
224 }
225
226 fn description(&self) -> &'static str {
227 "Using host network can bypass network policies"
228 }
229
230 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
231 let mut failures = Vec::new();
232
233 if let Some(values) = ctx.values {
235 for path in &values.defined_paths {
236 if path.to_lowercase().contains("hostnetwork") {
237 if let Some(value) = values.get(path) {
238 if is_truthy(value) {
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 }
253
254 for template in ctx.templates {
256 for token in &template.tokens {
257 if let TemplateToken::Text { content, line } = token {
258 if content.contains("hostNetwork: true") {
259 failures.push(CheckFailure::new(
260 "HL4004",
261 Severity::Warning,
262 "Pod uses host network. This bypasses network policies",
263 &template.path,
264 *line,
265 RuleCategory::Security,
266 ));
267 }
268 }
269 }
270 }
271
272 failures
273 }
274}
275
276pub struct HL4005;
278
279impl Rule for HL4005 {
280 fn code(&self) -> &'static str {
281 "HL4005"
282 }
283
284 fn severity(&self) -> Severity {
285 Severity::Warning
286 }
287
288 fn name(&self) -> &'static str {
289 "host-pid"
290 }
291
292 fn description(&self) -> &'static str {
293 "Using host PID namespace can expose host processes"
294 }
295
296 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
297 let mut failures = Vec::new();
298
299 for template in ctx.templates {
300 for token in &template.tokens {
301 if let TemplateToken::Text { content, line } = token {
302 if content.contains("hostPID: true") {
303 failures.push(CheckFailure::new(
304 "HL4005",
305 Severity::Warning,
306 "Pod uses host PID namespace. This can expose host processes",
307 &template.path,
308 *line,
309 RuleCategory::Security,
310 ));
311 }
312 }
313 }
314 }
315
316 failures
317 }
318}
319
320pub struct HL4006;
322
323impl Rule for HL4006 {
324 fn code(&self) -> &'static str {
325 "HL4006"
326 }
327
328 fn severity(&self) -> Severity {
329 Severity::Info
330 }
331
332 fn name(&self) -> &'static str {
333 "missing-security-context"
334 }
335
336 fn description(&self) -> &'static str {
337 "Container or pod is missing securityContext"
338 }
339
340 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
341 let mut failures = Vec::new();
342
343 if let Some(values) = ctx.values {
345 let has_security_context = values
346 .defined_paths
347 .iter()
348 .any(|p| p.to_lowercase().contains("securitycontext"));
349
350 if !has_security_context {
351 failures.push(CheckFailure::new(
352 "HL4006",
353 Severity::Info,
354 "No securityContext configuration found in values.yaml",
355 "values.yaml",
356 1,
357 RuleCategory::Security,
358 ));
359 }
360 }
361
362 failures
363 }
364}
365
366pub struct HL4011;
368
369impl Rule for HL4011 {
370 fn code(&self) -> &'static str {
371 "HL4011"
372 }
373
374 fn severity(&self) -> Severity {
375 Severity::Warning
376 }
377
378 fn name(&self) -> &'static str {
379 "secret-in-env"
380 }
381
382 fn description(&self) -> &'static str {
383 "Sensitive value passed via environment variable instead of mounted secret"
384 }
385
386 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
387 let mut failures = Vec::new();
388
389 let sensitive_patterns = [
391 "PASSWORD",
392 "SECRET",
393 "TOKEN",
394 "API_KEY",
395 "APIKEY",
396 "PRIVATE_KEY",
397 "CREDENTIALS",
398 ];
399
400 for template in ctx.templates {
401 for token in &template.tokens {
402 if let TemplateToken::Text { content, line } = token {
403 for pattern in &sensitive_patterns {
405 let search = format!("name: {}", pattern);
406 let search_lower = format!("name: {}", pattern.to_lowercase());
407 if (content.contains(&search) || content.contains(&search_lower))
408 && content.contains("value:")
409 && !content.contains("valueFrom:")
410 && !content.contains("secretKeyRef:")
411 {
412 failures.push(CheckFailure::new(
413 "HL4011",
414 Severity::Warning,
415 format!(
416 "Environment variable matching '{}' should use secretKeyRef instead of direct value",
417 pattern
418 ),
419 &template.path,
420 *line,
421 RuleCategory::Security,
422 ));
423 }
424 }
425 }
426 }
427 }
428
429 failures
430 }
431}
432
433pub struct HL4012;
435
436impl Rule for HL4012 {
437 fn code(&self) -> &'static str {
438 "HL4012"
439 }
440
441 fn severity(&self) -> Severity {
442 Severity::Error
443 }
444
445 fn name(&self) -> &'static str {
446 "hardcoded-credentials"
447 }
448
449 fn description(&self) -> &'static str {
450 "Hardcoded credentials or secrets detected in templates"
451 }
452
453 fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
454 let mut failures = Vec::new();
455
456 let credential_types = [
458 ("password:", "password"),
459 ("secret:", "secret"),
460 ("apikey:", "API key"),
461 ("token:", "token"),
462 ];
463
464 for template in ctx.templates {
465 for token in &template.tokens {
466 if let TemplateToken::Text { content, line } = token {
467 let lower_content = content.to_lowercase();
468
469 for (pattern, cred_type) in &credential_types {
470 if lower_content.contains(pattern) {
472 let has_template_var = content.contains("{{") && content.contains("}}");
474 let is_empty = content.contains("\"\"") || content.contains("''");
475
476 if !has_template_var && !is_empty {
477 let parts: Vec<&str> = content.split(':').collect();
479 if parts.len() >= 2 {
480 let value_part = parts[1].trim();
481 if !value_part.is_empty()
482 && !value_part.starts_with('{')
483 && !value_part.starts_with('$')
484 && value_part != "\"\""
485 && value_part != "''"
486 {
487 failures.push(CheckFailure::new(
488 "HL4012",
489 Severity::Error,
490 format!(
491 "Possible hardcoded {} detected. Use Secrets instead",
492 cred_type
493 ),
494 &template.path,
495 *line,
496 RuleCategory::Security,
497 ));
498 break;
499 }
500 }
501 }
502 }
503 }
504 }
505 }
506 }
507
508 failures
509 }
510}
511
512fn is_truthy(value: &serde_yaml::Value) -> bool {
514 match value {
515 serde_yaml::Value::Bool(b) => *b,
516 serde_yaml::Value::String(s) => {
517 let lower = s.to_lowercase();
518 lower == "true" || lower == "yes" || lower == "1"
519 }
520 serde_yaml::Value::Number(n) => n.as_i64().map(|i| i != 0).unwrap_or(false),
521 _ => false,
522 }
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528
529 #[test]
530 fn test_is_truthy() {
531 assert!(is_truthy(&serde_yaml::Value::Bool(true)));
532 assert!(!is_truthy(&serde_yaml::Value::Bool(false)));
533 assert!(is_truthy(&serde_yaml::Value::String("true".to_string())));
534 assert!(is_truthy(&serde_yaml::Value::String("yes".to_string())));
535 assert!(!is_truthy(&serde_yaml::Value::String("false".to_string())));
536 assert!(is_truthy(&serde_yaml::Value::Number(1.into())));
537 assert!(!is_truthy(&serde_yaml::Value::Number(0.into())));
538 }
539
540 #[test]
541 fn test_rules_exist() {
542 let all_rules = rules();
543 assert!(!all_rules.is_empty());
544 }
545}